class DeployView(WidgetWrap): def __init__(self, display_controller, placement_controller, placement_view): self.display_controller = display_controller self.placement_controller = placement_controller self.placement_view = placement_view self.prev_status = None w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): self.deploy_ok_msg = ("\u2713 All the required OpenStack services are " "placed on a machine, and you can now deploy.") self.deploy_button = AttrMap( Button("Deploy", on_press=self.do_deploy), 'button_primary', 'button_primary focus') self.deploy_grid = GridFlow([self.deploy_button], 10, 1, 0, 'center') self.unplaced_msg = "Some required services are still unassigned." self.main_pile = Pile([Divider()]) return self.main_pile def update(self): changed = self.prev_status != self.placement_controller.can_deploy() if self.placement_controller.can_deploy(): if changed: self.show_deploy_button() else: self.main_pile.contents[0] = (Divider(), self.main_pile.options()) if changed: self.display_controller.status_error_message(self.unplaced_msg) self.prev_status = self.placement_controller.can_deploy() def show_deploy_button(self): self.main_pile.contents[0] = (AttrMap(self.deploy_grid, 'deploy_highlight_start'), self.main_pile.options()) self.display_controller.status_info_message(self.deploy_ok_msg) def do_deploy(self, sender): self.placement_view.do_deploy_cb()
class Header(WidgetWrap): TITLE_TEXT = "Ubuntu OpenStack Installer - Dashboard" def __init__(self): self.text = Text(self.TITLE_TEXT) self.widget = Color.frame_header(self.text) self.pile = Pile([self.widget, Text("")]) self.set_show_add_units_hotkey(False) super().__init__(self.pile) def set_openstack_rel(self, release): self.text.set_text("{} ({})".format(self.TITLE_TEXT, release)) def set_show_add_units_hotkey(self, show): self.show_add_units = show self.update() def update(self): if self.show_add_units: add_unit_string = "(A)dd Services \N{BULLET}" else: add_unit_string = "" tw = Color.frame_subheader( Text(add_unit_string + " (H)elp \N{BULLET} " "(R)efresh \N{BULLET} (Q)uit", align="center") ) self.pile.contents[1] = (tw, self.pile.options())
class Header(WidgetWrap): def __init__(self): self.title_widget = Color.frame_header(padding(Text(TITLE_TEXT))) self.pile = Pile([self.title_widget, Text("")]) self.set_show_add_units_hotkey(False) super().__init__(self.pile) def set_show_add_units_hotkey(self, show): self.show_add_units = show self.update() def update(self): if self.show_add_units: add_unit_string = '(A)dd Services \N{BULLET}' else: add_unit_string = '' tw = Color.frame_subheader(Text(add_unit_string + ' (H)elp \N{BULLET} ' '(R)efresh \N{BULLET} (Q)uit', align='center')) self.pile.contents[1] = (tw, self.pile.options())
class ServicesList(WidgetWrap): """A list of services (charm classes) with flexible display options. Note that not all combinations of display options make sense. YMMV. controller - a PlacementController actions - a list of tuples describing buttons. Passed to ServiceWidget. machine - a machine instance to query for constraint checking. If None, no constraint checking is done. If set, only services whose constraints are satisfied by 'machine' are shown. ignore_assigned - bool, whether or not to display services that have already been assigned to a machine (but not yet deployed) ignore_deployed - bool, whether or not to display services that have already been deployed deployed_only - bool, only show deployed services show_constraints - bool, whether or not to display the constraints for the various services show_type - string, one of 'all', 'required' or 'non-required', controls which charm states should be shown. default is 'all'. trace_updates - bool, enable verbose update logging """ def __init__(self, controller, actions, subordinate_actions, machine=None, ignore_assigned=False, ignore_deployed=False, assigned_only=False, deployed_only=False, show_type='all', show_constraints=False, show_placements=False, title="Services", trace_updates=False): self.controller = controller self.actions = actions self.subordinate_actions = subordinate_actions self.service_widgets = [] self.machine = machine self.ignore_assigned = ignore_assigned self.ignore_deployed = ignore_deployed self.assigned_only = assigned_only self.deployed_only = deployed_only self.show_type = show_type self.show_constraints = show_constraints self.show_placements = show_placements self.title = title self.trace = trace_updates w = self.build_widgets() self.update() super().__init__(w) def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self): self.service_pile = Pile( [Text(self.title), Divider(' ')] + self.service_widgets) return self.service_pile def find_service_widget(self, cc): return next((sw for sw in self.service_widgets if sw.charm_class.charm_name == cc.charm_name), None) def update(self): def trace(cc, s): if self.trace: log.debug("{}: {} {}".format(self.title, cc, s)) for cc in self.controller.charm_classes(): if self.machine: if not satisfies(self.machine, cc.constraints)[0] \ or not (self.controller.is_assigned_to(cc, self.machine) or self.controller.is_deployed_to(cc, self.machine)): self.remove_service_widget(cc) trace(cc, "removed because machine doesn't match") continue if self.ignore_assigned and self.assigned_only: raise Exception("Can't both ignore and only show assigned.") if self.ignore_assigned: n = self.controller.assignment_machine_count_for_charm(cc) if n == cc.required_num_units() \ and not cc.allow_multi_units \ and self.controller.is_assigned(cc): self.remove_service_widget(cc) trace(cc, "removed because max units are " "assigned") continue elif self.assigned_only: if not self.controller.is_assigned(cc): self.remove_service_widget(cc) trace( cc, "removed because it is not assigned and " "assigned_only is True") continue if self.ignore_deployed and self.deployed_only: raise Exception("Can't both ignore and only show deployed.") if self.ignore_deployed: n = self.controller.deployment_machine_count_for_charm(cc) if n == cc.required_num_units() \ and self.controller.is_deployed(cc): self.remove_service_widget(cc) trace( cc, "removed because the required number of units" " has been deployed") continue elif self.deployed_only: if not self.controller.is_deployed(cc): self.remove_service_widget(cc) continue state, _, _ = self.controller.get_charm_state(cc) if self.show_type == 'required': if state != CharmState.REQUIRED: self.remove_service_widget(cc) continue elif self.show_type == 'non-required': if state == CharmState.REQUIRED: self.remove_service_widget(cc) trace( cc, "removed because show_type is 'non-required' and" "state is REQUIRED.") continue assigned_or_deployed = (self.controller.is_assigned(cc) or self.controller.is_deployed(cc)) if not cc.allow_multi_units and assigned_or_deployed: self.remove_service_widget(cc) trace( cc, "removed because it doesn't allow multiple units" " and is not assigned or deployed.") continue sw = self.find_service_widget(cc) if sw is None: sw = self.add_service_widget(cc) trace(cc, "added widget") sw.update() def add_service_widget(self, charm_class): if charm_class.subordinate: actions = self.subordinate_actions else: actions = self.actions sw = ServiceWidget(charm_class, self.controller, actions, self.show_constraints, show_placements=self.show_placements) self.service_widgets.append(sw) options = self.service_pile.options() self.service_pile.contents.append((sw, options)) self.service_pile.contents.append( (AttrMap(Padding(Divider('\u23bc'), left=2, right=2), 'label'), options)) return sw def remove_service_widget(self, charm_class): sw = self.find_service_widget(charm_class) if sw is None: return self.service_widgets.remove(sw) sw_idx = 0 for w, opts in self.service_pile.contents: if w == sw: break sw_idx += 1 c = self.service_pile.contents[:sw_idx] + \ self.service_pile.contents[sw_idx + 2:] self.service_pile.contents = c
class MachineWaitView(WidgetWrap): def __init__(self, display_controller, installer, config): self.display_controller = display_controller self.installer = installer self.config = config creds = self.config.getopt('maascreds') if os.getenv("FAKE_API_DATA"): self.maas_client = None self.maas_state = FakeMaasState() else: self.maas_client, self.maas_state = connect_to_maas(creds) self.spinner = Spinner(15, 4) w = self.build_widgets() super().__init__(w) self.update() def build_widgets(self): self.message = Text("Please review available machines in MAAS", align='center') self.button_pile = Pile([]) self.main_pile = Pile([self.message, Divider(), self.button_pile]) return Filler(self.main_pile, valign='middle') def keypress(self, size, key): key = {'tab': 'down', 'shift tab': 'up'}.get(key, key) return super().keypress(size, key) def scroll_down(self): pass def scroll_up(self): pass def selectable(self): return True def get_status(self): " returns (global_ok, [ok, condition])" self.maas_state.invalidate_nodes_cache() machines = self.maas_state.machines(state=MaasMachineStatus.READY) powerable_machines = [m for m in machines if m.power_type is not None] n_powerable = len(powerable_machines) conditions = [(n_powerable >= 1, "At least one machine enlisted with power " "control (currently {})".format(n_powerable))] global_ok = all([ok for ok, _ in conditions]) return global_ok, conditions def update(self): msg = ("Before continuing, ensure that at least one machine is " "enlisted into MAAS:") self.message = Text(self.spinner.next_frame() + ['\n', msg, '\n'], align='center') contents = [(self.message, self.main_pile.options())] global_ok, statuses = self.get_status() status_map = {True: ('success_icon', "\u2713 "), False: ('error_icon', "<!> ")} contents += [(Text([status_map[status], condition], align='center'), self.main_pile.options()) for status, condition in statuses] contents += [(Divider(), self.main_pile.options()), (self.button_pile, self.main_pile.options())] self.main_pile.contents = contents if not global_ok: b = AttrMap(SelectableIcon(" ( Can't Continue ) "), 'disabled_button', 'disabled_button_focus') else: b = AttrMap(Button("Continue", on_press=self.do_continue), 'button_primary', 'button_primary focus') cancel_b = AttrMap(Button("Cancel", on_press=self.do_cancel), 'button_secondary', 'button_secondary focus') self.button_pile.contents = [(Padding(cancel_b, width=24, align='center'), self.button_pile.options()), (Padding(b, width=24, align='center'), self.button_pile.options())] # ensure that the button is always focused: self.main_pile.focus_position = len(self.main_pile.contents) - 1 @utils.async def do_continue(self, *args, **kwargs): self.installer.do_install() def do_cancel(self, *args, **kwargs): raise SystemExit("Installation cancelled.")
class MachinesList(WidgetWrap): """A list of machines with configurable action buttons for each machine. action - a function to call when the machine's button is pressed constraints - a dict of constraints to filter the machines list. only machines matching all the constraints will be shown. show_hardware - bool, whether or not to show the hardware details for each of the machines title_widgets - A Text Widget to be used in place of the default title. show_assignments - bool, whether or not to show the assignments for each of the machines. show_only_ready - bool, only show machines with a ready state. """ def __init__(self, controller, display_controller, constraints=None, show_hardware=False, title_widgets=None, show_assignments=True, show_placeholders=True, show_only_ready=False, show_filter_box=False): self.controller = controller self.display_controller = display_controller self.machine_widgets = [] if constraints is None: self.constraints = {} else: self.constraints = constraints self.show_hardware = show_hardware self.show_assignments = show_assignments self.show_placeholders = show_placeholders self.show_only_ready = show_only_ready self.show_filter_box = show_filter_box self.filter_string = "" w = self.build_widgets(title_widgets) self.update() super().__init__(w) def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self, title_widgets): if title_widgets is None: if len(self.constraints) > 0: cstr = " matching constraints" else: cstr = "" title_widgets = [Text("Machines" + cstr, align='center')] self.filter_edit_box = FilterBox(self.handle_filter_change) header_widgets = title_widgets + [Divider()] if self.show_filter_box: header_widgets.append(self.filter_edit_box) self.header_padding = len(header_widgets) self.machine_pile = Pile(header_widgets + self.machine_widgets) return self.machine_pile def handle_filter_change(self, edit_button, userdata): self.filter_string = userdata self.update() def find_machine_widget(self, m): return next((mw for mw in self.machine_widgets if mw.machine.instance_id == m.instance_id), None) def update(self): machines = self.controller.machines( include_placeholders=self.show_placeholders) if self.show_only_ready: machines = [m for m in machines if m.status == MaasMachineStatus.READY] for mw in self.machine_widgets: machine = next((m for m in machines if mw.machine.instance_id == m.instance_id), None) if machine is None: self.remove_machine(mw.machine) n_satisfying_machines = len(machines) def get_placement_filter_label(d): s = "" for atype, al in d.items(): s += " ".join(["{} {}".format(cc.service_name, cc.display_name) for cc in al]) return s for m in machines: if not satisfies(m, self.constraints)[0]: self.remove_machine(m) n_satisfying_machines -= 1 continue assignment_names = "" ad = self.controller.assignments_for_machine(m) assignment_names = get_placement_filter_label(ad) filter_label = "{} {}".format(m.filter_label(), assignment_names) if self.filter_string != "" and \ self.filter_string not in filter_label: self.remove_machine(m) continue mw = self.find_machine_widget(m) if mw is None: mw = self.add_machine_widget(m) mw.update() self.filter_edit_box.set_info(len(self.machine_widgets), n_satisfying_machines) self.sort_machine_widgets() def add_machine_widget(self, machine): mw = SimpleMachineWidget(machine, self.controller, self.display_controller, self.show_assignments) self.machine_widgets.append(mw) options = self.machine_pile.options() self.machine_pile.contents.append((mw, options)) # NOTE: see the +1: indexing in remove_machine if you re-add # this divider. it should then be +2. # self.machine_pile.contents.append((AttrMap(Padding(Divider('\u23bc'), # left=2, right=2), # 'label'), options)) return mw def remove_machine(self, machine): mw = self.find_machine_widget(machine) if mw is None: return self.machine_widgets.remove(mw) mw_idx = 0 for w, opts in self.machine_pile.contents: if w == mw: break mw_idx += 1 c = self.machine_pile.contents[:mw_idx] + \ self.machine_pile.contents[mw_idx + 1:] self.machine_pile.contents = c def sort_machine_widgets(self): def keyfunc(mw): m = mw.machine hwinfo = " ".join(map(str, [m.arch, m.cpu_cores, m.mem, m.storage])) if str(mw.machine.status) == 'ready': skey = 'A' else: skey = str(mw.machine.status) return skey + mw.machine.hostname + hwinfo self.machine_widgets.sort(key=keyfunc) def wrappedkeyfunc(t): mw, options = t if not isinstance(mw, SimpleMachineWidget): return 'A' return keyfunc(mw) self.machine_pile.contents.sort(key=wrappedkeyfunc) def focus_prev_or_top(self): self.update() try: if self.machine_pile.focus_position <= self.header_padding: self.machine_pile.focus_position = self.header_padding except IndexError: log.debug("index error in machines_list focus_top")
class JujuMachineWidget(WidgetWrap): """A widget displaying a machine and action buttons. juju_machine_id = juju id of the machine md = the machine dict application - the current application for which machines are being shown assign_cb - a function that takes a machine and assignmenttype to perform the button action unassign_cb - a function that takes a machine and removes assignments for the machine controller - a controller object that provides show_pin_chooser show_assignments - display info about which charms are assigned and what assignment type (LXC, KVM, etc) they have. show_pin - show button to pin juju machine to a maas machine """ def __init__(self, juju_machine_id, md, application, assign_cb, unassign_cb, controller, show_assignments=True, show_pin=False): self.juju_machine_id = juju_machine_id self.md = md self.application = application self.is_selected = False self.assign_cb = assign_cb self.unassign_cb = unassign_cb self.controller = controller self.show_assignments = show_assignments self.show_pin = show_pin self.all_assigned = False w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def __repr__(self): return "jujumachinewidget #" + str(self.juju_machine_id) def build_widgets(self): self.action_button_cols = Columns([]) self.action_buttons = [] self.build_unselected_widgets() self.pile = Pile([self.unselected_columns]) return self.pile def build_unselected_widgets(self): cdict = juju.constraints_to_dict(self.md.get('constraints', '')) self.juju_machine_id_button = MenuSelectButton( '{:20s}'.format(self.juju_machine_id), self.show_pin_chooser) self.juju_machine_id_label = Text( "{:20s}".format(self.juju_machine_id)) self.cores_field = IntEdit('', cdict.get('cores', '')) connect_signal(self.cores_field, 'change', self.handle_cores_changed) self.mem_field = Edit('', cdict.get('mem', '')) connect_signal(self.mem_field, 'change', self.handle_mem_changed) self.disk_field = Edit('', cdict.get('root-disk', '')) connect_signal(self.disk_field, 'change', self.handle_disk_changed) if self.show_pin: machine_id_w = self.juju_machine_id_button else: machine_id_w = self.juju_machine_id_label cols = [machine_id_w, self.cores_field, self.mem_field, self.disk_field] cols = [AttrMap(w, 'string_input', 'string_input_focus') for w in cols] cols.append(Text("placeholder")) self.unselected_columns = Columns(cols, dividechars=2) self.update_assignments() return self.unselected_columns def update_assignments(self): assignments = [] mps = self.controller.get_all_assignments(self.juju_machine_id) if len(mps) > 0: if self.show_assignments: ad = defaultdict(list) for application, atype in mps: ad[atype].append(application) astr = " ".join(["{}{}".format( atype_to_label([atype])[0], ",".join([application.service_name for application in al])) for atype, al in ad.items()]) assignments.append(astr) else: if self.show_assignments: assignments.append("-") if any([application == self.application for application, _ in mps]): action = self.do_remove label = "Remove" else: action = self.do_select label = "Select" self.select_button = PlainButton(label, action) cols = [Text(s) for s in assignments] current_assignments = [a for a, _ in mps if a == self.application] if self.all_assigned and len(current_assignments) == 0: cols.append(Text("")) else: cols += [AttrMap(self.select_button, 'text', 'button_secondary focus')] opts = self.unselected_columns.options() self.unselected_columns.contents[4:] = [(w, opts) for w in cols] def update(self): self.update_action_buttons() if self.is_selected: self.update_selected() else: self.update_unselected() def update_selected(self): cn = self.application.service_name msg = Text(" Add {} to machine #{}:".format(cn, self.juju_machine_id)) self.pile.contents = [(msg, self.pile.options()), (self.action_button_cols, self.pile.options()), (Divider(), self.pile.options())] def update_unselected(self): if self.show_pin: pinned_machine = self.controller.get_pin(self.juju_machine_id) if pinned_machine: pin_label = " {} \N{PENCIL}".format(pinned_machine.hostname) else: pin_label = " \N{PENCIL}" self.juju_machine_id_button.set_label('{:20s}'.format( self.juju_machine_id + " " + pin_label)) else: self.juju_machine_id_label.set_text('{:20s}'.format( self.juju_machine_id)) self.pile.contents = [(self.unselected_columns, self.pile.options()), (Divider(), self.pile.options())] self.update_assignments() def update_action_buttons(self): all_actions = [(AssignmentType.BareMetal, 'Add as Bare Metal', self.select_baremetal), (AssignmentType.LXD, 'Add as LXD', self.select_lxd), (AssignmentType.KVM, 'Add as KVM', self.select_kvm)] sc = self.application if sc: allowed_set = set(sc.allowed_assignment_types) allowed_types = set([atype for atype, _, _ in all_actions]) allowed_types = allowed_types.intersection(allowed_set) else: allowed_types = set() # + 1 for the cancel button: if len(self.action_buttons) == len(allowed_types) + 1: return self.action_buttons = [AttrMap(PlainButton(label, on_press=func), 'button_secondary', 'button_secondary focus') for atype, label, func in all_actions if atype in allowed_types] self.action_buttons.append( AttrMap(PlainButton("Cancel", on_press=self.do_cancel), 'button_secondary', 'button_secondary focus')) opts = self.action_button_cols.options() self.action_button_cols.contents = [(b, opts) for b in self.action_buttons] def do_select(self, sender): self.is_selected = True self.update() self.pile.focus_position = 1 self.action_button_cols.focus_position = 0 def do_remove(self, sender): self.unassign_cb(self.juju_machine_id) def do_cancel(self, sender): self.is_selected = False self.update() self.pile.focus_position = 0 def _do_select_assignment(self, atype): self.assign_cb(self.juju_machine_id, atype) self.pile.focus_position = 0 self.is_selected = False self.update() def select_baremetal(self, sender): self._do_select_assignment(AssignmentType.BareMetal) def select_lxd(self, sender): self._do_select_assignment(AssignmentType.LXD) def select_kvm(self, sender): self._do_select_assignment(AssignmentType.KVM) def handle_cores_changed(self, sender, val): if val == '': self.md = self.controller.clear_constraint(self.juju_machine_id, 'cores') else: self.md = self.controller.set_constraint(self.juju_machine_id, 'cores', val) def _format_constraint(self, val): """Ensure that a constraint has a unit. bare numbers are treated as gigabytes""" try: return units.gb_to_human(float(val)) except ValueError: return val def handle_mem_changed(self, sender, val): if val == '': self.md = self.controller.clear_constraint(self.juju_machine_id, 'mem') else: self.md = self.controller.set_constraint( self.juju_machine_id, 'mem', self._format_constraint(val)) def handle_disk_changed(self, sender, val): if val == '': self.md = self.controller.clear_constraint(self.juju_machine_id, 'root-disk') else: self.md = self.controller.set_constraint( self.juju_machine_id, 'root-disk', self._format_constraint(val)) def show_pin_chooser(self, sender): self.controller.show_pin_chooser(self.juju_machine_id)
class ServicesList(WidgetWrap): """A list of services (charm classes) with flexible display options. Note that not all combinations of display options make sense. YMMV. controller - a PlacementController actions - a list of tuples describing buttons. Passed to ServiceWidget. machine - a machine instance to query for constraint checking. If None, no constraint checking is done. If set, only services whose constraints are satisfied by 'machine' are shown. ignore_assigned - bool, whether or not to display services that have already been assigned to a machine (but not yet deployed) ignore_deployed - bool, whether or not to display services that have already been deployed deployed_only - bool, only show deployed services show_constraints - bool, whether or not to display the constraints for the various services show_type - string, one of 'all', 'required' or 'non-required', controls which charm states should be shown. default is 'all'. """ def __init__(self, controller, actions, subordinate_actions, machine=None, ignore_assigned=False, ignore_deployed=False, assigned_only=False, deployed_only=False, show_type='all', show_constraints=False, show_placements=False, title="Services"): self.controller = controller self.actions = actions self.subordinate_actions = subordinate_actions self.service_widgets = [] self.machine = machine self.ignore_assigned = ignore_assigned self.ignore_deployed = ignore_deployed self.assigned_only = assigned_only self.deployed_only = deployed_only self.show_type = show_type self.show_constraints = show_constraints self.show_placements = show_placements self.title = title w = self.build_widgets() self.update() super().__init__(w) def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self): self.service_pile = Pile([Text(self.title), Divider(' ')] + self.service_widgets) return self.service_pile def find_service_widget(self, cc): return next((sw for sw in self.service_widgets if sw.charm_class.charm_name == cc.charm_name), None) def update(self): for cc in self.controller.charm_classes(): if self.machine: if not satisfies(self.machine, cc.constraints)[0] \ or not (self.controller.is_assigned_to(cc, self.machine) or self.controller.is_deployed_to(cc, self.machine)): self.remove_service_widget(cc) continue if self.ignore_assigned and self.assigned_only: raise Exception("Can't both ignore and only show assigned.") if self.ignore_assigned: n = self.controller.assignment_machine_count_for_charm(cc) if n == cc.required_num_units() \ and self.controller.is_assigned(cc): self.remove_service_widget(cc) continue elif self.assigned_only: if not self.controller.is_assigned(cc): self.remove_service_widget(cc) continue if self.ignore_deployed and self.deployed_only: raise Exception("Can't both ignore and only show deployed.") if self.ignore_deployed: n = self.controller.deployment_machine_count_for_charm(cc) if n == cc.required_num_units() \ and self.controller.is_deployed(cc): self.remove_service_widget(cc) continue elif self.deployed_only: if not self.controller.is_deployed(cc): self.remove_service_widget(cc) continue state, _, _ = self.controller.get_charm_state(cc) if self.show_type == 'required': if state != CharmState.REQUIRED: self.remove_service_widget(cc) continue elif self.show_type == 'non-required': if state == CharmState.REQUIRED: self.remove_service_widget(cc) continue if not cc.allow_multi_units and \ not (self.controller.is_assigned(cc) or self.controller.is_deployed(cc)): self.remove_service_widget(cc) continue sw = self.find_service_widget(cc) if sw is None: sw = self.add_service_widget(cc) sw.update() def add_service_widget(self, charm_class): if charm_class.subordinate: actions = self.subordinate_actions else: actions = self.actions sw = ServiceWidget(charm_class, self.controller, actions, self.show_constraints, show_placements=self.show_placements) self.service_widgets.append(sw) options = self.service_pile.options() self.service_pile.contents.append((sw, options)) self.service_pile.contents.append((AttrMap(Padding(Divider('\u23bc'), left=2, right=2), 'label'), options)) return sw def remove_service_widget(self, charm_class): sw = self.find_service_widget(charm_class) if sw is None: return self.service_widgets.remove(sw) sw_idx = 0 for w, opts in self.service_pile.contents: if w == sw: break sw_idx += 1 c = self.service_pile.contents[:sw_idx] + \ self.service_pile.contents[sw_idx + 2:] self.service_pile.contents = c
class ServicesList(WidgetWrap): """A list of services (charm classes) with configurable action buttons for each machine. actions - a list of tuples describing buttons. Passed to ServiceWidget. machine - a machine instance to query for constraint checking show_constraints - bool, whether or not to show the constraints for the various services """ def __init__(self, controller, actions, machine=None, unplaced_only=False, show_type='all', show_constraints=False, title="Services"): self.controller = controller self.actions = actions self.service_widgets = [] self.machine = machine self.unplaced_only = unplaced_only self.show_type = show_type self.show_constraints = show_constraints self.title = title w = self.build_widgets() self.update() super().__init__(w) def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self): self.service_pile = Pile([Text(self.title), Divider(' ')] + self.service_widgets) return self.service_pile def find_service_widget(self, cc): return next((sw for sw in self.service_widgets if sw.charm_class.charm_name == cc.charm_name), None) def update(self): for cc in self.controller.charm_classes(): if self.machine: if not satisfies(self.machine, cc.constraints)[0] \ or not self.controller.is_assigned(cc, self.machine): self.remove_service_widget(cc) continue if self.unplaced_only: n_units = self.controller.machine_count_for_charm(cc) if n_units == cc.required_num_units() \ and cc not in self.controller.unplaced_services: self.remove_service_widget(cc) continue is_required = self.controller.service_is_required(cc) if self.show_type == 'required': if not is_required: self.remove_service_widget(cc) continue elif self.show_type == 'non-required': if is_required: self.remove_service_widget(cc) continue if not cc.allow_multi_units and \ cc not in self.controller.unplaced_services: self.remove_service_widget(cc) continue sw = self.find_service_widget(cc) if sw is None: sw = self.add_service_widget(cc) sw.update() def add_service_widget(self, charm_class): sw = ServiceWidget(charm_class, self.controller, self.actions, self.show_constraints) self.service_widgets.append(sw) options = self.service_pile.options() self.service_pile.contents.append((sw, options)) self.service_pile.contents.append((AttrMap(Padding(Divider('\u23bc'), left=2, right=2), 'label'), options)) return sw def remove_service_widget(self, charm_class): sw = self.find_service_widget(charm_class) if sw is None: return self.service_widgets.remove(sw) sw_idx = 0 for w, opts in self.service_pile.contents: if w == sw: break sw_idx += 1 c = self.service_pile.contents[:sw_idx] + \ self.service_pile.contents[sw_idx + 2:] self.service_pile.contents = c
class JujuMachinesList(ContainerWidgetWrap): """A list of machines in a juju bundle, with configurable action buttons for each machine. application - an application machines - list of machine info dicts assign_cb - a function that takes a machine and assignmenttype to perform the button action unassign_cb - a function that takes a machine and clears assignments controller - a controller object that provides get_placements and get_pin. show_constraints - bool, whether or not to show the constraints for each of the machines title_widgets - A list of widgets to be used in place of the default title. show_assignments - bool, whether or not to show the assignments for each of the machines. show_filter_box - bool, show text box to filter listed machines """ def __init__(self, application, machines, assign_cb, unassign_cb, add_machine_cb, remove_machine_cb, controller, show_constraints=True, title_widgets=None, show_assignments=True, show_filter_box=False, show_pins=False): self.application = application self.machines = machines self.assign_cb = assign_cb self.unassign_cb = unassign_cb self.add_machine_cb = add_machine_cb self.remove_machine_cb = remove_machine_cb self.controller = controller self.machine_widgets = [] self.show_assignments = show_assignments self.all_assigned = False self.show_filter_box = show_filter_box self.show_pins = show_pins self.filter_string = "" w = self.build_widgets(title_widgets) self.update() super().__init__(w) def __repr__(self): return "machineslist" def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self, title_widgets): if title_widgets is None: title_widgets = [Text("Machines", align='center')] self.filter_edit_box = FilterBox(self.handle_filter_change) header_widgets = title_widgets + [Divider()] if self.show_filter_box: header_widgets += [self.filter_edit_box, Divider()] labels = ["ID", "Cores", "Memory (GiB)", "Storage (GiB)"] if self.show_assignments: labels += ["Assignments", ""] else: labels += [""] header_label_col = Columns([Text(m) for m in labels], dividechars=2) header_widgets.append(header_label_col) self.header_padding = len(header_widgets) self.add_new_button = SecondaryButton("Add New Machine", self.do_add_machine) self.add_new_cols = Columns([Text(s) for s in [' ', ' ', ' ', ' ', ' ']] + [self.add_new_button], dividechars=2) self.machine_pile = Pile(header_widgets + self.machine_widgets + [self.add_new_cols]) return self.machine_pile def do_add_machine(self, sender): self.add_machine_cb() self.update() def remove_machine(self, sender): self.remove_machine_cb() self.update() def handle_filter_change(self, edit_button, userdata): self.filter_string = userdata self.update() def find_machine_widget(self, midx): return next((mw for mw in self.machine_widgets if mw.juju_machine_id == midx), None) def update(self): for midx, md in sorted(self.machines.items()): allvalues = ["{}={}".format(k, v) for k, v in md.items()] filter_label = midx + " " + " ".join(allvalues) if self.filter_string != "" and \ self.filter_string not in filter_label: self.remove_machine_widget(midx) continue mw = self.find_machine_widget(midx) if mw is None: mw = self.add_machine_widget(midx, md) mw.all_assigned = self.all_assigned mw.update() n = len(self.machine_widgets) self.filter_edit_box.set_info(n, n) self.sort_machine_widgets() def add_machine_widget(self, midx, md): mw = JujuMachineWidget(midx, md, self.application, self.assign_cb, self.unassign_cb, self.controller, self.show_assignments, self.show_pins) self.machine_widgets.append(mw) options = self.machine_pile.options() self.machine_pile.contents.insert( len(self.machine_pile.contents) - 1, (mw, options)) return mw def remove_machine_widget(self, midx): mw = self.find_machine_widget(midx) if mw is None: return self.machine_widgets.remove(mw) mw_idx = 0 for w, opts in self.machine_pile.contents: if w == mw: break mw_idx += 1 c = self.machine_pile.contents[:mw_idx] + \ self.machine_pile.contents[mw_idx + 1:] self.machine_pile.contents = c def sort_machine_widgets(self): self.machine_widgets.sort(key=attrgetter('juju_machine_id')) def wrappedkeyfunc(t): mw, options = t if isinstance(mw, JujuMachineWidget): return "B{}".format(mw.juju_machine_id) if mw in [self.add_new_cols]: return 'C' return 'A' self.machine_pile.contents.sort(key=wrappedkeyfunc) def focus_prev_or_top(self): self.update() try: if self.machine_pile.focus_position <= self.header_padding: self.machine_pile.focus_position = self.header_padding except IndexError: log.debug("index error in machines_list focus_top")
class CharmstoreColumn(WidgetWrap): def __init__(self, display_controller, placement_controller, placement_view, metadata_controller): self.placement_controller = placement_controller self.display_controller = display_controller self.placement_view = placement_view self.metadata_controller = metadata_controller self.state = CharmstoreColumnUIState.RELATED self.prev_state = None self.current_search_string = "" w = self.build_widgets() super().__init__(w) self._related_charms = [] self._bundle_results = [] self._charm_results = [] self._recommended_charms = [] self.loading = True self.update() def build_widgets(self): self.title = Text('') self.pile = Pile([self.title]) return self.pile def clear_search_results(self): self._bundle_results = [] self._charm_results = [] def handle_search_change(self, s): if s == "": self.state = CharmstoreColumnUIState.RELATED self.clear_search_results() else: self.state = CharmstoreColumnUIState.SEARCH_RESULTS self.current_search_string = s self.loading = True self.update() def remove_existing_charms(self, charms): existing_charms = [CharmStoreID(s.charm_source).as_str_without_rev() for s in self.placement_controller.bundle.services] return [c for c in charms if CharmStoreID(c['Id']).as_str_without_rev() not in existing_charms] def get_filtered_recommendations(self): opts = self.pile.options() if len(self._recommended_charms) == 0: mc = self.metadata_controller self._recommended_charms = mc.get_recommended_charms() return [(CharmWidget(d, self.do_add_charm, recommended=True), opts) for d in self.remove_existing_charms(self._recommended_charms)] def update(self): opts = self.pile.options() extra_widgets = [] recommended_widgets = [] if self.metadata_controller.loaded(): recommended_widgets = self.get_filtered_recommendations() if len(recommended_widgets) > 0: top_w = recommended_widgets[0][0] top_w.set_header("Recommended Charms") self.loading = False series = self.placement_controller.bundle.series bundle_widgets = [(BundleWidget(d, self.do_add_bundle), opts) for d in self._bundle_results if 'bundle-metadata' in d.get('Meta', {}) and 'Series' in d['Meta']['bundle-metadata'] and d['Meta']['bundle-metadata']['Series'] == series] if len(bundle_widgets) > 0: top_w = bundle_widgets[0][0] top_w.set_header("Bundles") filtered_charm_results = self.remove_existing_charms( self._charm_results) filtered_charm_results = filtered_charm_results[:10] charm_widgets = [(CharmWidget(d, self.do_add_charm), opts) for d in filtered_charm_results if 'charm-metadata' in d.get('Meta', {})] if len(charm_widgets) > 0: top_w = charm_widgets[0][0] top_w.set_header("Charms") if self.state == CharmstoreColumnUIState.RELATED: if self.loading: self.title.set_text("\nLoading Recommended " "and Popular Charms…") else: self.title.set_text("") extra_widgets = recommended_widgets else: if self.loading: msg = "\nSearching for '{}'…\n".format( self.current_search_string) self.title.set_text(msg) else: bn = len(self._bundle_results) cn = len(filtered_charm_results) if bn + cn == 0: advice = "" if len(self.current_search_string) < 3: advice = "Try a longer search string." msg = ("\nNo charms found matching '{}' " "{}".format(self.current_search_string, advice)) else: msg = ("\nShowing the top {} bundles and {} " "charms matching {}:" "".format(bn, cn, self.current_search_string)) self.title.set_text(msg) self.pile.contents[1:] = extra_widgets + bundle_widgets + charm_widgets def add_results(self, bundle_results, charm_results): self._bundle_results += bundle_results self._bundle_results = self._bundle_results[:5] self._charm_results += charm_results def focus_prev_or_top(self): if len(self.pile.contents) < 2: return if self.state == CharmstoreColumnUIState.RELATED and self.loading \ and len(self.pile.contents) > 2: self.pile.focus_position = 2 else: self.pile.focus_position = 1 def do_add_charm(self, charm_name, charm_dict): self.placement_view.do_add_charm(charm_name, charm_dict) self.update() def do_add_bundle(self, bundle_dict): self.placement_view.do_add_bundle(bundle_dict) def handle_error(self, e): msg = "Error searching for {}: {}".format(self.current_search_string, e) self.title.set_text(msg)
class JujuMachineWidget(WidgetWrap): """A widget displaying a machine and action buttons. juju_machine_id = juju id of the machine md = the machine dict application - the current application for which machines are being shown assign_cb - a function that takes a machine and assignmenttype to perform the button action unassign_cb - a function that takes a machine and removes assignments for the machine controller - a controller object that provides show_pin_chooser show_assignments - display info about which charms are assigned and what assignment type (LXC, KVM, etc) they have. show_pin - show button to pin juju machine to a maas machine """ def __init__(self, juju_machine_id, md, application, assign_cb, unassign_cb, controller, show_assignments=True, show_pin=False): self.juju_machine_id = juju_machine_id self.md = md self.application = application self.is_selected = False self.assign_cb = assign_cb self.unassign_cb = unassign_cb self.controller = controller self.show_assignments = show_assignments self.show_pin = show_pin self.all_assigned = False w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def __repr__(self): return "jujumachinewidget #" + str(self.juju_machine_id) def build_widgets(self): self.action_button_cols = Columns([]) self.action_buttons = [] self.build_unselected_widgets() self.pile = Pile([self.unselected_columns]) return self.pile def build_unselected_widgets(self): cdict = juju.constraints_to_dict(self.md.get('constraints', '')) self.juju_machine_id_button = MenuSelectButton( '{:20s}'.format(self.juju_machine_id), self.show_pin_chooser) self.juju_machine_id_label = Text("{:20s}".format( self.juju_machine_id)) self.cores_field = IntEdit('', str(cdict.get('cores', ''))) connect_signal(self.cores_field, 'change', self.handle_cores_changed) memval = cdict.get('mem', '') if memval != '': memval = memval / 1024 self.mem_field = Edit('', str(memval)) connect_signal(self.mem_field, 'change', self.handle_mem_changed) diskval = cdict.get('root-disk', '') if diskval != '': diskval = diskval / 1024 self.disk_field = Edit('', str(diskval)) connect_signal(self.disk_field, 'change', self.handle_disk_changed) if self.show_pin: machine_id_w = self.juju_machine_id_button else: machine_id_w = self.juju_machine_id_label cols = [ machine_id_w, self.cores_field, self.mem_field, self.disk_field ] cols = [AttrMap(w, 'string_input', 'string_input_focus') for w in cols] cols.append(Text("placeholder")) self.unselected_columns = Columns(cols, dividechars=2) self.update_assignments() return self.unselected_columns def update_assignments(self): assignments = [] mps = self.controller.get_all_assignments(self.juju_machine_id) if len(mps) > 0: if self.show_assignments: ad = defaultdict(list) for application, atype in mps: ad[atype].append(application) astr = " ".join([ "{}{}".format( atype_to_label([atype])[0], ",".join( [application.service_name for application in al])) for atype, al in ad.items() ]) assignments.append(astr) else: if self.show_assignments: assignments.append("-") if any([application == self.application for application, _ in mps]): action = self.do_remove label = "Remove" else: action = self.do_select label = "Select" self.select_button = PlainButton(label, action) cols = [Text(s) for s in assignments] current_assignments = [a for a, _ in mps if a == self.application] if self.all_assigned and len(current_assignments) == 0: cols.append(Text("")) else: cols += [ AttrMap(self.select_button, 'text', 'button_secondary focus') ] opts = self.unselected_columns.options() self.unselected_columns.contents[4:] = [(w, opts) for w in cols] def update(self): self.update_action_buttons() if self.is_selected: self.update_selected() else: self.update_unselected() def update_selected(self): cn = self.application.service_name msg = Text(" Add {} to machine #{}:".format(cn, self.juju_machine_id)) self.pile.contents = [(msg, self.pile.options()), (self.action_button_cols, self.pile.options()), (Divider(), self.pile.options())] def update_unselected(self): if self.show_pin: pinned_machine = self.controller.get_pin(self.juju_machine_id) if pinned_machine: pin_label = " {} \N{PENCIL}".format(pinned_machine.hostname) else: pin_label = " \N{PENCIL}" self.juju_machine_id_button.set_label( '{:20s}'.format(self.juju_machine_id + " " + pin_label)) else: self.juju_machine_id_label.set_text('{:20s}'.format( self.juju_machine_id)) self.pile.contents = [(self.unselected_columns, self.pile.options()), (Divider(), self.pile.options())] self.update_assignments() def update_action_buttons(self): all_actions = [(AssignmentType.BareMetal, 'Add as Bare Metal', self.select_baremetal), (AssignmentType.LXD, 'Add as LXD', self.select_lxd), (AssignmentType.KVM, 'Add as KVM', self.select_kvm)] sc = self.application if sc: allowed_set = set(sc.allowed_assignment_types) allowed_types = set([atype for atype, _, _ in all_actions]) allowed_types = allowed_types.intersection(allowed_set) else: allowed_types = set() # + 1 for the cancel button: if len(self.action_buttons) == len(allowed_types) + 1: return self.action_buttons = [ AttrMap(PlainButton(label, on_press=func), 'button_secondary', 'button_secondary focus') for atype, label, func in all_actions if atype in allowed_types ] self.action_buttons.append( AttrMap(PlainButton("Cancel", on_press=self.do_cancel), 'button_secondary', 'button_secondary focus')) opts = self.action_button_cols.options() self.action_button_cols.contents = [(b, opts) for b in self.action_buttons] def do_select(self, sender): self.is_selected = True self.update() self.pile.focus_position = 1 self.action_button_cols.focus_position = 0 def do_remove(self, sender): self.unassign_cb(self.juju_machine_id) def do_cancel(self, sender): self.is_selected = False self.update() self.pile.focus_position = 0 def _do_select_assignment(self, atype): self.assign_cb(self.juju_machine_id, atype) self.pile.focus_position = 0 self.is_selected = False self.update() def select_baremetal(self, sender): self._do_select_assignment(AssignmentType.BareMetal) def select_lxd(self, sender): self._do_select_assignment(AssignmentType.LXD) def select_kvm(self, sender): self._do_select_assignment(AssignmentType.KVM) def handle_cores_changed(self, sender, val): if val == '': self.md = self.controller.clear_constraint(self.juju_machine_id, 'cores') else: self.md = self.controller.set_constraint(self.juju_machine_id, 'cores', val) def _format_constraint(self, val): "Store constraints as int values of megabytes" if val.isnumeric(): return int(val) * 1024 else: return units.human_to_mb(val) def handle_mem_changed(self, sender, val): if val == '': self.md = self.controller.clear_constraint(self.juju_machine_id, 'mem') else: try: self.md = self.controller.set_constraint( self.juju_machine_id, 'mem', self._format_constraint(val)) except ValueError: return def handle_disk_changed(self, sender, val): if val == '': self.md = self.controller.clear_constraint(self.juju_machine_id, 'root-disk') else: try: self.md = self.controller.set_constraint( self.juju_machine_id, 'root-disk', self._format_constraint(val)) except ValueError: return def show_pin_chooser(self, sender): self.controller.show_pin_chooser(self.juju_machine_id)
class ServiceWalkthroughView(WidgetWrap): def __init__(self, service, idx, n_total, metadata_controller, callback): self.callback = callback self.service = service self.idx = idx self.n_total = n_total self.n_remaining = n_total - idx - 1 self.metadata_controller = metadata_controller self.info_handled = False w = self.build_widgets() super().__init__(w) self.get_async_info() if self.n_remaining == 0: self.pile.focus_position = len(self.pile.contents) - 1 else: self.pile.focus_position = len(self.pile.contents) - 2 def selectable(self): return True def build_widgets(self): self.description_w = Text("Description Loading…") self.readme_w = Text("README Loading…") self.scale_edit = IntegerEditor(default=self.service.num_units) connect_signal(self.scale_edit._edit, 'change', self.handle_scale_changed) self.skip_rest_button = PlainButton( "Deploy all {} Remaining Applications with Bundle Defaults".format( self.n_remaining), self.do_skip_rest) col = Columns([(6, Text('Units:', align='right')), (15, Color.string_input(self.scale_edit, focus_map='string_input focus'))], dividechars=1) if self.n_remaining == 0: buttons = [ Padding.right_50( Color.button_primary(PlainButton("Deploy and Continue", self.do_deploy), focus_map='button_primary focus')) ] else: buttons = [ Padding.right_50( Color.button_primary(PlainButton( "Deploy and Configure Next Application", self.do_deploy), focus_map='button_primary focus')), Padding.right_50( Color.button_secondary(self.skip_rest_button, focus_map='button_secondary focus')) ] ws = [ Text("{} of {}: {}".format(self.idx + 1, self.n_total, self.service.service_name.upper())), Padding.center(HR()), Padding.center(self.description_w, left=2), Padding.line_break(""), Padding.center(self.readme_w, left=2), Padding.center(HR()) ] if not self.service.subordinate: ws.append(Padding.left(col, left=1)) ws.append(Padding.line_break("")) ws += buttons self.pile = Pile(ws) return Padding.center_90(Filler(self.pile, valign="top")) def get_async_info(self): info = self.metadata_controller.get_charm_info( self.service.csid.as_str_without_rev(), self.handle_info_updated) if info: self.handle_info_updated(info) self.metadata_controller.get_readme(self.service.csid.as_seriesname(), self.handle_readme_updated) def handle_info_updated(self, new_info): if self.info_handled: return self.info_handled = True EventLoop.loop.event_loop._loop.call_soon_threadsafe( self._update_info_on_main_thread, new_info) def _update_info_on_main_thread(self, new_info): self.description_w.set_text( new_info["Meta"]["charm-metadata"]["Summary"]) self._invalidate() self.add_options() def add_options(self): service_id = self.service.csid.as_str_without_rev() options = self.metadata_controller.get_options(service_id) metadata = app.config.get('metadata', None) if metadata is None: return options_whitelist = metadata.get('options-whitelist', None) if options_whitelist is None: return svc_opts_whitelist = options_whitelist.get(self.service.service_name, []) hidden = [n for n in options.keys() if n not in svc_opts_whitelist] log.info("Hiding options not in the whitelist: {}".format(hidden)) for opname in svc_opts_whitelist: opdict = options[opname] self.add_option_widget(opname, opdict) def add_option_widget(self, opname, opdict): cv = self.service.options.get(opname, None) ow = OptionWidget(opname, opdict['Type'], opdict['Description'], opdict['Default'], current_value=cv, value_changed_callback=self.handle_edit) self.pile.contents.insert(7, (ow, self.pile.options())) self._invalidate() return ow def handle_edit(self, opname, value): self.service.options[opname] = value def handle_readme_updated(self, readme_text_f): EventLoop.loop.event_loop._loop.call_soon_threadsafe( self._update_readme_on_main_thread, readme_text_f) def _update_readme_on_main_thread(self, readme_text_f): rls = readme_text_f.result().splitlines() rls = [l for l in rls if not l.startswith("#")] nrls = [] for i in range(len(rls)): if i + 1 == len(rls): break if len(rls[i]) > 0: if rls[i][0] in ['-', '#', '=']: continue if len(rls[i + 1]) > 0: if rls[i + 1][0] in ['-', '=']: continue nrls.append(rls[i]) if len(nrls) == 0: return if nrls[0] == '': nrls = nrls[1:] # split after two paragraphs: if '' in nrls: firstparidx = nrls.index('') else: firstparidx = 1 try: splitidx = nrls.index('', firstparidx + 1) except: splitidx = firstparidx nrls = nrls[:splitidx] self.readme_w.set_text("\n".join(nrls)) self._invalidate() def handle_scale_changed(self, widget, newvalstr): if newvalstr == '': return self.service.num_units = int(newvalstr) def do_deploy(self, arg): self.callback(single_service=self.service) def do_skip_rest(self, arg): self.callback(single_service=None)
class MachinesColumn(WidgetWrap): """Shows machines or a link to MAAS to add more""" def __init__(self, display_controller, placement_controller, placement_view): self.display_controller = display_controller self.placement_controller = placement_controller self.placement_view = placement_view w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): def has_services_p(m): pc = self.placement_controller n = sum( [len(al) for at, al in pc.assignments_for_machine(m).items()]) return n > 0 clear_machine_func = self.placement_view.do_clear_machine show_chooser_func = self.placement_view.do_show_service_chooser self.open_maas_button = AttrMap( Button("Open in Browser", on_press=self.browse_maas), 'button_secondary', 'button_secondary focus') bc = self.placement_view.config.juju_env['bootstrap-config'] maasname = "'{}' <{}>".format(bc['name'], bc['maas-server']) maastitle = "Connected to MAAS {}".format(maasname) tw = Columns([ Text(maastitle), Padding(self.open_maas_button, align='right', width=BUTTON_SIZE, right=2) ]) self.machines_list = MachinesList( self.placement_controller, [(has_services_p, 'Clear All Services', clear_machine_func), (has_services_p, 'Remove Some Services', show_chooser_func)], show_hardware=True, title_widgets=tw) self.machines_list.update() self.machines_list_pile = Pile([self.machines_list, Divider()]) # placeholders replaced in update() with absolute indexes, so # if you change this list, check update(). pl = [ Text(('subheading', "Machines"), align='center'), Divider(), Pile([]), # machines_list Divider() ] self.main_pile = Pile(pl) return self.main_pile def update(self): self.machines_list.update() bc = self.placement_view.config.juju_env['bootstrap-config'] empty_maas_msg = ("There are no available machines.\n" "Open {} to add machines to " "'{}':".format(bc['maas-server'], bc['name'])) self.empty_maas_widgets = Pile([ Text([('error_icon', "\N{WARNING SIGN} "), empty_maas_msg], align='center'), Padding(self.open_maas_button, align='center', width=BUTTON_SIZE) ]) # 2 machines is the subordinate placeholder + juju default: if len(self.placement_controller.machines()) == 2: self.main_pile.contents[2] = (self.empty_maas_widgets, self.main_pile.options()) else: self.main_pile.contents[2] = (self.machines_list_pile, self.main_pile.options()) def browse_maas(self, sender): bc = self.placement_view.config.juju_env['bootstrap-config'] try: p = Popen(["sensible-browser", bc['maas-server']], stdout=PIPE, stderr=PIPE) outs, errs = p.communicate(timeout=5) except TimeoutExpired: # went five seconds without an error, so we assume it's # OK. Don't kill it, just let it go: return e = errs.decode('utf-8') msg = "Error opening '{}' in a browser:\n{}".format(bc['name'], e) w = InfoDialogWidget(msg, self.placement_view.remove_overlay) self.placement_view.show_overlay(w)
class JujuMachinesList(WidgetWrap): """A list of machines in a juju bundle, with configurable action buttons for each machine. application - an application machines - list of machine info dicts assign_cb - a function that takes a machine and assignmenttype to perform the button action unassign_cb - a function that takes a machine and clears assignments controller - a controller object that provides get_placements and get_pin. show_constraints - bool, whether or not to show the constraints for each of the machines title_widgets - A list of widgets to be used in place of the default title. show_assignments - bool, whether or not to show the assignments for each of the machines. show_filter_box - bool, show text box to filter listed machines """ def __init__(self, application, machines, assign_cb, unassign_cb, add_machine_cb, remove_machine_cb, controller, show_constraints=True, title_widgets=None, show_assignments=True, show_filter_box=False, show_pins=False): self.application = application self.machines = machines self.assign_cb = assign_cb self.unassign_cb = unassign_cb self.add_machine_cb = add_machine_cb self.remove_machine_cb = remove_machine_cb self.controller = controller self.machine_widgets = [] self.show_assignments = show_assignments self.all_assigned = False self.show_filter_box = show_filter_box self.show_pins = show_pins self.filter_string = "" w = self.build_widgets(title_widgets) self.update() super().__init__(w) def __repr__(self): return "machineslist" def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self, title_widgets): if title_widgets is None: title_widgets = [Text("Machines", align='center')] self.filter_edit_box = FilterBox(self.handle_filter_change) header_widgets = title_widgets + [Divider()] if self.show_filter_box: header_widgets += [self.filter_edit_box, Divider()] labels = ["ID", "Cores", "Memory (GiB)", "Storage (GiB)"] if self.show_assignments: labels += ["Assignments", ""] else: labels += [""] header_label_col = Columns([Text(m) for m in labels], dividechars=2) header_widgets.append(header_label_col) self.header_padding = len(header_widgets) self.add_new_button = AttrMap( PlainButton("Add New Machine", on_press=self.do_add_machine), 'button_secondary', 'button_secondary focus') self.add_new_cols = Columns([Text(s) for s in [' ', ' ', ' ', ' ', ' ']] + [self.add_new_button], dividechars=2) self.machine_pile = Pile(header_widgets + self.machine_widgets + [self.add_new_cols]) return self.machine_pile def do_add_machine(self, sender): self.add_machine_cb() self.update() def remove_machine(self, sender): self.remove_machine_cb() self.update() def handle_filter_change(self, edit_button, userdata): self.filter_string = userdata self.update() def find_machine_widget(self, midx): return next((mw for mw in self.machine_widgets if mw.juju_machine_id == midx), None) def update(self): for midx, md in sorted(self.machines.items()): allvalues = ["{}={}".format(k, v) for k, v in md.items()] filter_label = midx + " " + " ".join(allvalues) if self.filter_string != "" and \ self.filter_string not in filter_label: self.remove_machine_widget(midx) continue mw = self.find_machine_widget(midx) if mw is None: mw = self.add_machine_widget(midx, md) mw.all_assigned = self.all_assigned mw.update() n = len(self.machine_widgets) self.filter_edit_box.set_info(n, n) self.sort_machine_widgets() def add_machine_widget(self, midx, md): mw = JujuMachineWidget(midx, md, self.application, self.assign_cb, self.unassign_cb, self.controller, self.show_assignments, self.show_pins) self.machine_widgets.append(mw) options = self.machine_pile.options() self.machine_pile.contents.insert( len(self.machine_pile.contents) - 1, (mw, options)) return mw def remove_machine_widget(self, midx): mw = self.find_machine_widget(midx) if mw is None: return self.machine_widgets.remove(mw) mw_idx = 0 for w, opts in self.machine_pile.contents: if w == mw: break mw_idx += 1 c = self.machine_pile.contents[:mw_idx] + \ self.machine_pile.contents[mw_idx + 1:] self.machine_pile.contents = c def sort_machine_widgets(self): self.machine_widgets.sort(key=attrgetter('juju_machine_id')) def wrappedkeyfunc(t): mw, options = t if isinstance(mw, JujuMachineWidget): return "B{}".format(mw.juju_machine_id) if mw in [self.add_new_cols]: return 'C' return 'A' self.machine_pile.contents.sort(key=wrappedkeyfunc) def focus_prev_or_top(self): self.update() try: if self.machine_pile.focus_position <= self.header_padding: self.machine_pile.focus_position = self.header_padding except IndexError: log.debug("index error in machines_list focus_top")
class CloudView(WidgetWrap): def __init__(self, app, public_clouds, custom_clouds, cb=None): self.app = app self.cb = cb self.public_clouds = public_clouds self.custom_clouds = custom_clouds self.config = self.app.config self.buttons_pile_selected = False self.pile = None self.pile_localhost_idx = None self.frame = Frame(body=Padding.center_80( Filler(self._build_widget(), valign='top')), footer=self._build_footer()) super().__init__(self.frame) def keypress(self, size, key): if key in ['tab', 'shift tab']: self._swap_focus() return super().keypress(size, key) def _swap_focus(self): if not self.buttons_pile_selected: self.buttons_pile_selected = True self.frame.focus_position = 'footer' self.buttons_pile.focus_position = 1 else: self.buttons_pile_selected = False self.frame.focus_position = 'body' def _build_buttons(self): cancel = menu_btn(on_press=self.cancel, label="\n QUIT\n") buttons = [ Padding.line_break(""), Color.menu_button(cancel, focus_map='button_primary focus'), ] self.buttons_pile = Pile(buttons) return self.buttons_pile def _build_footer(self): footer_pile = Pile([ Padding.line_break(""), Color.frame_footer( Columns([('fixed', 2, Text("")), ('fixed', 13, self._build_buttons())])) ]) return footer_pile def _get_localhost_widget_idx(self): """ Returns index in pile where localhost widget resides """ if self.pile_localhost_idx: return self.pile_localhost_idx else: for idx, item in enumerate(self.pile.contents): if hasattr(item[0], 'original_widget') and \ isinstance(item[0].original_widget, MenuSelectButton) and \ item[0].original_widget.get_label() == 'localhost': return idx def _enable_localhost_widget(self): """ Sets the proper widget for localhost availability """ idx = self._get_localhost_widget_idx() widget = Color.body(menu_btn(label=cloud_types.LOCALHOST, on_press=self.submit), focus_map='menu_button focus') self._update_pile_at_idx(idx, widget) del self.pile.contents[idx + 1] def _update_pile_at_idx(self, idx, item): """ In place updates a widget in the self.pile contents """ self.pile_localhost_idx = idx self.pile.contents[idx] = (item, self.pile.options()) def _add_item(self, item): if not self.pile: self.pile = Pile([item]) else: self.pile.contents.append((item, self.pile.options())) def _build_widget(self): if len(self.public_clouds) > 0: self._add_item(Text("Public Clouds")) self._add_item(HR()) for item in self.public_clouds: self._add_item( Color.body(menu_btn(label=item, on_press=self.submit), focus_map='menu_button focus')) self._add_item(Padding.line_break("")) if len(self.custom_clouds) > 0: self._add_item(Text("Your Clouds")) self._add_item(HR()) for item in self.custom_clouds: self._add_item( Color.body(menu_btn(label=item, on_press=self.submit), focus_map='menu_button focus')) self._add_item(Padding.line_break("")) new_clouds = juju.get_compatible_clouds( ['localhost', 'maas', 'vsphere']) if new_clouds: self._add_item(Text("Configure a New Cloud")) self._add_item(HR()) for item in sorted(new_clouds): if item == 'localhost': self._add_item( Color.info_context(menu_btn( label=cloud_types.LOCALHOST, on_press=None), focus_map='disabled_button')) self._add_item( Color.info_context( Padding.center_90( Text("LXD not found, please install with " "`sudo snap install lxd && lxd init` " "and wait for this message to disappear.") ))) else: self._add_item( Color.body(menu_btn(label=item, on_press=self.submit), focus_map='menu_button focus')) self.pile.focus_position = 2 return self.pile def submit(self, result): self.cb(result.label) def cancel(self, btn): EventLoop.exit(0)
class NetworkSetDefaultRouteView(BaseView): def __init__(self, model, family, signal): self.model = model self.family = family self.signal = signal self.default_gateway_w = None self.gateway_options = Pile(self._build_default_routes()) body = [ Padding.center_79(Text("Please set the default gateway:")), Padding.line_break(""), Padding.center_79(self.gateway_options), Padding.line_break(""), Padding.fixed_10(self._build_buttons()) ] super().__init__(ListBox(body)) def _build_default_routes(self): ''' iterate through interfaces collecting any uniq provider (aka, gateway) and associate the interface name with the gateway then generate a line per key in the gateway dict and display the keys. Upon selection of the gateway entry (ip) then we set model.set_default_gateway(ip) if manual is selected, then we update the second entry into a IPAddressEditor and accept the value, submitting it to the model. ''' providers = {} for iface in self.model.get_all_interfaces(): if self.family == netifaces.AF_INET: ip_providers = iface.ip4_providers elif self.family == netifaces.AF_INET6: ip_providers = iface.ip6_providers for provider in ip_providers: log.debug('ipv4 provider: {}'.format(provider)) gw = provider if gw in providers: providers[gw].append(iface.ifname) else: providers[gw] = [iface.ifname] log.debug('gateway providers: {}'.format(providers)) items = [] items.append( Padding.center_79( Color.menu_button(menu_btn(label="None", on_press=self.done), focus_map="menu_button focus"))) for (gw, ifaces) in providers.items(): if gw is None: continue items.append( Padding.center_79( Color.menu_button(menu_btn(label="{gw} ({ifaces})".format( gw=gw, ifaces=(",".join(ifaces))), on_press=self.done), focus_map="menu_button focus"))) items.append( Padding.center_79( Color.menu_button(menu_btn( label="Specify the default route manually", on_press=self.show_edit_default_route), focus_map="menu_button focus"))) return items def _build_buttons(self): cancel = cancel_btn(on_press=self.cancel) done = done_btn(on_press=self.done) buttons = [ Color.button(done, focus_map='button focus'), Color.button(cancel, focus_map='button focus') ] return Pile(buttons) def show_edit_default_route(self, btn): log.debug("Re-rendering specify default route") self.default_gateway_w = StringEditor( caption="Default gateway will be ") self.gateway_options.contents[-1] = (Padding.center_50( Color.string_input(self.default_gateway_w, focus_map="string_input focus")), self.gateway_options.options()) # self.signal.emit_signal('refresh') def done(self, result): log.debug("changing default gw: {}".format(result)) gw_func = None if self.family == netifaces.AF_INET: gw_func = self.model.set_default_v4_gateway elif self.family == netifaces.AF_INET6: gw_func = self.model.set_default_v6_gateway if self.default_gateway_w and self.default_gateway_w.value: try: gw_func(None, self.default_gateway_w.value) except ValueError: # FIXME: raise UX error message self.default_gateway_w.edit_text = "" else: gw_ip_from_label = result.label.split(" ")[0] log.debug("default gw entered: {}".format(gw_ip_from_label)) try: if gw_ip_from_label.startswith('None'): gw_func(None, None) else: gw_func(None, gw_ip_from_label) except ValueError: # FIXME: raise UX error message pass self.signal.prev_signal() def cancel(self, button): self.signal.prev_signal()
class SimpleServiceWidget(WidgetWrap): """A widget displaying a service as a button service - the service to display placement_controller - a PlacementController instance display_controller - a PlacerView instance callback - a function to be called when either of the buttons is pressed. The service will be passed to the function as userdata. show_placements - display the machine(s) currently assigned to host this service, both planned deployments (aka 'assignments', and already-deployed, called 'deployments'). """ def __init__(self, service, placement_controller, display_controller, show_placements=False): self.service = service self.placement_controller = placement_controller self.display_controller = display_controller self.show_placements = show_placements self.state = ServiceWidgetState.UNSELECTED w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): self.button = MenuSelectButton("I AM A SERVICE", self.do_select) self.action_button_cols = Columns([], dividechars=1) self.action_buttons = [] self.pile = Pile([self.button]) return self.pile def get_markup(self): if self.service.subordinate: return [ self.service.service_name + " (subordinate)\n " + self.service.charm_source ], [] nr = self.service.required_num_units() pl = "s" if nr > 1 else "" title_markup = [ self.service.service_name + "\n {} unit{}: ".format(nr, pl) + self.service.charm_source ] info_markup = [] if not self.display_controller.has_maas: return title_markup, info_markup pd = self.placement_controller.get_assignments(self.service) nplaced = sum([len(pd[k]) for k in pd]) if nr - nplaced > 0: pl = "" if nr - nplaced > 1: pl = "s" info_str = (" {} unit{} will be auto-placed " "by Juju\n".format(nr - nplaced, pl)) info_markup.append(info_str) def string_for_placement_dict(d): if self.display_controller.has_maas: defstring = "Bare Metal (Default)" else: defstring = "LXD (Default)" s = [] for atype, ml in sorted(d.items()): if atype == AssignmentType.DEFAULT: aname = defstring else: aname = atype.name hostnames = [m.hostname for m in ml] s.append(" {}: {}".format(aname, ", ".join(hostnames))) if len(s) == 0: return [] return "\n".join(s) ad = self.placement_controller.get_assignments(self.service) info_markup += string_for_placement_dict(ad) return title_markup, info_markup def update_choosing(self): title_markup, _ = self.get_markup() msg = Padding(Text(title_markup), left=2, right=2, align='center') self.pile.contents = [(msg, self.pile.options()), (self.action_button_cols, self.pile.options()), (Divider(), self.pile.options())] def update_default(self): title_markup, info_markup = self.get_markup() self.button.set_label(title_markup + ["\n"] + info_markup) if self.state == ServiceWidgetState.SELECTED: b = AttrMap(self.button, 'deploy_highlight_start', 'button_secondary focus') else: b = AttrMap(self.button, 'text', 'button_secondary focus') self.pile.contents = [(b, self.pile.options()), (Divider(), self.pile.options())] def update(self): self.service = next( (s for s in self.placement_controller.bundle.services if s.service_name == self.service.service_name), self.service) self.update_action_buttons() if self.state == ServiceWidgetState.CHOOSING: self.update_choosing() else: self.update_default() def keypress(self, size, key): if key == 'backspace': self.display_controller.remove_service(self.service) elif key == '+': if not self.service.subordinate: self.display_controller.scale_service(self.service, 1) elif key == '-': if not self.service.subordinate: self.display_controller.scale_service(self.service, -1) return super().keypress(size, key) def update_action_buttons(self): all_actions = [] if self.display_controller.has_maas: all_actions = [('Choose Placement', self.handle_placement_button_pressed)] all_actions += [('Edit Relations', self.handle_relation_button_pressed), ('Edit Options', self.handle_options_button_pressed)] self.action_buttons = [ AttrMap(PlainButton(label, on_press=func), 'button_secondary', 'button_secondary focus') for label, func in all_actions ] self.action_buttons.append( AttrMap(PlainButton("Cancel", on_press=self.do_cancel), 'button_secondary', 'button_secondary focus')) opts = self.action_button_cols.options() self.action_button_cols.contents = [(b, opts) for b in self.action_buttons] def do_select(self, sender): self.display_controller.clear_selections() if self.state == ServiceWidgetState.SELECTED: self.state = ServiceWidgetState.UNSELECTED self.display_controller.set_selected_service(None) else: self.display_controller.set_selected_service(self.service) self.state = ServiceWidgetState.CHOOSING self.pile.focus_position = 1 self.action_button_cols.focus_position = 0 self.update() def do_cancel(self, sender): self.state = ServiceWidgetState.UNSELECTED self.update() self.display_controller.show_default_view() self.pile.focus_position = 0 def handle_placement_button_pressed(self, sender): self.state = ServiceWidgetState.SELECTED self.update() self.display_controller.edit_placement() self.pile.focus_position = 0 def handle_relation_button_pressed(self, sender): self.state = ServiceWidgetState.SELECTED self.update() self.display_controller.edit_relations() self.pile.focus_position = 0 def handle_options_button_pressed(self, sender): self.state = ServiceWidgetState.SELECTED self.update() self.display_controller.edit_options() self.pile.focus_position = 0
class DeployView(WidgetWrap): def __init__(self, display_controller, placement_controller, placement_view): self.display_controller = display_controller self.placement_controller = placement_controller self.placement_view = placement_view self.prev_status = None w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): self.deploy_ok_msg = ("\u2713 All the core OpenStack services are " "placed on a machine, and you can now deploy.") self.deploy_button = AttrMap(Button("Deploy", on_press=self.do_deploy), 'button', 'button_focus') self.deploy_grid = GridFlow([self.deploy_button], 10, 1, 0, 'center') self.unplaced_msg = "Some core services are still unplaced." self.main_pile = Pile([Divider()]) return self.main_pile def update(self): changed = self.prev_status != self.placement_controller.can_deploy() if self.placement_controller.can_deploy(): if changed: self.show_deploy_button() else: self.main_pile.contents[0] = (Divider(), self.main_pile.options()) if changed: self.display_controller.error_message(self.unplaced_msg) self.prev_status = self.placement_controller.can_deploy() def show_deploy_button(self): self.main_pile.contents[0] = (AttrMap(self.deploy_grid, 'deploy_highlight_start'), self.main_pile.options()) def fade_timeout(loop, step): if step == 1: self.display_controller.loop.set_alarm_in(5, fade_timeout, 2) new_attr = 'deploy_highlight_end' else: new_attr = '' self.main_pile.contents[0] = (AttrMap(self.deploy_grid, new_attr), self.main_pile.options()) self.display_controller.loop.set_alarm_in(4, fade_timeout, 1) self.display_controller.info_message(self.deploy_ok_msg) def do_deploy(self, sender): self.display_controller.commit_placement()
class ServicesList(WidgetWrap): """A list of services with flexible display options. Note that not all combinations of display options make sense. YMMV. controller - a PlacementController display_controller - a PlacerView machine - a machine instance to query for constraint checking. If None, no constraint checking is done. If set, only services whose constraints are satisfied by 'machine' are shown. """ def __init__(self, placement_controller, display_controller, show_placements=False, title="Services"): self.placement_controller = placement_controller self.display_controller = display_controller self.service_widgets = [] self.show_placements = show_placements self.title = title w = self.build_widgets() self.update() super().__init__(w) def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self): widgets = [] if self.title: widgets = [Text(self.title), Divider(' ')] widgets += self.service_widgets self.service_pile = Pile(widgets) return self.service_pile def focus_top_or_next(self): return self._setfocus(top=False) def focus_top(self): return self._setfocus(top=True) def _setfocus(self, top): try: if top: self.service_pile.focus_position = 0 else: pos = self.service_pile.focus_position if pos + 1 >= len(self.service_widgets): return False self.service_pile.focus_position = pos + 1 except IndexError: log.debug("caught indexerror in servicescolumn.focus_next") return False return True def focused_service_widget(self): if len(self.service_widgets) > 0: return self.service_pile.focus return None def find_service_widget(self, s): return next((sw for sw in self.service_widgets if sw.service.service_name == s.service_name), None) def update(self): for s in self.placement_controller.services(): sw = self.find_service_widget(s) if sw is None: sw = self.add_service_widget(s) sw.update() allnames = [ s.service_name for s in self.placement_controller.services() ] for sw in self.service_widgets: if sw.service.service_name not in allnames: self.remove_service_widget(sw.service) self.sort_service_widgets() def add_service_widget(self, service): sw = SimpleServiceWidget(service, self.placement_controller, self.display_controller, show_placements=self.show_placements) self.service_widgets.append(sw) options = self.service_pile.options() self.service_pile.contents.append((sw, options)) # NOTE: see the + 1: indexing in remove_service_widget if you # re-add this divider. it should then be +2. # self.service_pile.contents.append((AttrMap(Padding(Divider('\u23bc'), # left=2, right=2), # 'label'), options)) return sw def remove_service_widget(self, service): sw = self.find_service_widget(service) if sw is None: return self.service_widgets.remove(sw) sw_idx = 0 for w, opts in self.service_pile.contents: if w == sw: break sw_idx += 1 c = self.service_pile.contents[:sw_idx] + \ self.service_pile.contents[sw_idx + 1:] self.service_pile.contents = c def sort_service_widgets(self): def keyfunc(sw): s = sw.service if s.subordinate: skey = 'z' else: skey = s.service_name return skey self.service_widgets.sort(key=keyfunc) def wrappedkeyfunc(t): mw, options = t if not isinstance(mw, SimpleServiceWidget): return 'A' return keyfunc(mw) self.service_pile.contents.sort(key=wrappedkeyfunc) def select_service(self, service_name): idx = 0 for w, opts in self.service_pile.contents: if w.service.service_name == service_name: self.service_pile.focus_position = idx return idx += 1
class MachinesColumn(WidgetWrap): """Shows machines or a link to MAAS to add more""" def __init__(self, display_controller, placement_controller, placement_view): self.display_controller = display_controller self.placement_controller = placement_controller self.placement_view = placement_view w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): self.machines_list = MachinesList(self.placement_controller, self.display_controller, None, show_hardware=True, show_assignments=False, show_placeholders=False, show_only_ready=True, title_widgets=[]) self.machines_list.update() self.machines_list_pile = Pile([self.machines_list, Divider()]) return self.machines_list_pile def update(self): self.machines_list.update() maasinfo = self.placement_controller.maasinfo empty_maas_msg = ("There are no available machines.\n" "Open {} to add machines to " "'{}':".format(maasinfo['server_name'], maasinfo['server_hostname'])) self.empty_maas_widgets = Pile([Text([('error_icon', "\N{WARNING SIGN} "), empty_maas_msg], align='center')]) # 2 machines is the subordinate placeholder + juju default: opts = self.machines_list_pile.options() if len(self.placement_controller.machines()) == 2: self.machines_list_pile.contents[0] = (self.empty_maas_widgets, opts) else: self.machines_list_pile.contents[0] = (self.machines_list, opts) def clear_selections(self): for mw in self.machines_list.machine_widgets: mw.is_selected = False def focus_prev_or_top(self): self.update() try: self.machines_list_pile.focus_position = 0 self.machines_list.focus_prev_or_top() except IndexError: log.debug("caught indexerror in machinesColumn focus_prev_or_top") pass
def render_text(text, K): # XXX: make this code lazy-reader reader-proxy aware txts = [make_text(t, K.width) for t in text.split('\n')] K.txts = txts piles = [] p = Pile([]) for t in txts[:]: #if 'What emerges' in t.text: pu.db p.contents.append((t, p.options())) t_size = t.rows((K.width,)) #if piles and h(piles[-1]) > height: pu.db while h(p, K) > K.height: # Add a new pile, and send the trimmings in there piles.append(p) d = h(t, K) - (h(p, K) - K.height) #if d <= 0: pu.db # start the next column p_new = Pile([]) t_extra = trim(t, d, K.width) # TODO: include diff status in here, and line numbers p_new.contents.append((t_extra, p.options())) p = p_new t = t_extra #if piles and h(piles[-1]) > height: # # ACK! # break if h(p, K) == K.height: piles.append(p) # start the next column p = Pile([]) # all done, don't forget the last pile which we haven't added to the list yet piles.append(p) palette = [ ('black', 'light gray', 'black'), ('heading', 'black', 'light gray'), ('important', 'black', 'light cyan'), ('line', 'black', 'light gray'), ('options', 'dark gray', 'black'), ('focus heading', 'white', 'dark red'), ('focus line', 'black', 'dark red'), ('diff old', 'dark red', 'black'), ('diff new', 'dark green', 'black'), ('focus options', 'black', 'light gray'), ('pg normal', 'white', 'black', 'standout'), ('pg complete', 'white', 'dark magenta'), ('selected', 'white', 'dark blue')] #piles = urwid.ListBox(urwid.SimpleFocusListWalker(piles)) #cols = piles #fill = cols dc = int(K.max_width / K.width) # number of displayed columns while len(piles) < int(dc): piles.append(Pile([])) cols = urwid.Columns(piles[:dc], dividechars=1, min_width=K.width) K.cols = cols col_widths = cols.column_widths(K.screen.get_cols_rows()) K.displayed_columns = len( col_widths ) def tmp_generator(): for x in piles: yield urwid.Columns([x], dividechars=1, min_width=K.width) K.reader = LazyReader(tmp_generator()) # XXX: I need to subclass columns, and make it so the keypress function # "rolls" the piles under the hood, and re-renders all the widgets. # # self.contents.append(self.contents.pop(0)) # #cols.box_columns.extend(cols.widget_list) #grid = urwid.GridFlow(txts, cell_width=20, h_sep=4, v_sep=0, align='left') fill = urwid.Filler(cols, 'top', top=K.top_margin) K.total_cols = len(piles) # XXX: this is not the full story, it ignores the borders between columns c_columns = [sum(col_widths[:i+1]) for i in range(K.displayed_columns)] border = (K.max_width - c_columns[-1]) / K.displayed_columns def xpos_to_col(pos): for i,c in enumerate(c_columns): if pos < (c + i * border): return i K.xpos_to_col = xpos_to_col pbar = ProgressBar('pg normal', 'pg complete', K.displayed_columns, K.total_cols) K.pbar = pbar p = urwid.ListBox(urwid.SimpleListWalker([pbar])) all = Pile([ fill, (1, p), ]) cmd_line_text = urwid.Edit(K.fname) K.cmd_line_text = cmd_line_text #cmd_line_prompt = urwid.Text('hi there') #cmd_line_combined = urwid.Filler([cmd_line_prompt, cmd_line_text]) #all = urwid.Frame(body=all, footer=cmd_line_combined) K.all = urwid.Frame(body=all, footer=cmd_line_text) K.loop = urwid.MainLoop(K.all, palette, K.screen, unhandled_input=show_or_exit) K.loop.exit = urwid.Text(" Help? ") #IPython.embed() if K.args.diff: set_cmd("set ft=diff".split(), K) elif have_pygments: set_cmd(("set ft=" + lexer.name.split()[0].lower()).split(), K) if K.args.quick: K.loop.set_alarm_in(1, lambda x,y: quit()) pbh = progress_bar_handler(p) K.pbh = pbh next(pbh) try: K.loop.run() except KeyboardInterrupt: pass
class MachinesList(WidgetWrap): """A list of machines with configurable action buttons for each machine. actions - a list of ('label', function) pairs that wil be used to create buttons for each machine. The machine will be passed to the function as userdata. constraints - a dict of constraints to filter the machines list. only machines matching all the constraints will be shown. show_hardware - bool, whether or not to show the hardware details for each of the machines title_widgets - A Text Widget to be used in place of the default title. show_assignments - bool, whether or not to show the assignments for each of the machines. """ def __init__(self, controller, actions, constraints=None, show_hardware=False, title_widgets=None, show_assignments=True): self.controller = controller self.actions = actions self.machine_widgets = [] if constraints is None: self.constraints = {} else: self.constraints = constraints self.show_hardware = show_hardware self.show_assignments = show_assignments self.filter_string = "" w = self.build_widgets(title_widgets) self.update() super().__init__(w) def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self, title_widgets): if title_widgets is None: if len(self.constraints) > 0: cstr = " matching constraints" else: cstr = "" title_widgets = Text("Machines" + cstr, align='center') self.filter_edit_box = FilterBox(self.handle_filter_change) self.machine_pile = Pile([title_widgets, Divider(), self.filter_edit_box] + self.machine_widgets) return self.machine_pile def handle_filter_change(self, edit_button, userdata): self.filter_string = userdata self.update() def find_machine_widget(self, m): return next((mw for mw in self.machine_widgets if mw.machine.instance_id == m.instance_id), None) def update(self): machines = self.controller.machines() for mw in self.machine_widgets: machine = next((m for m in machines if mw.machine.instance_id == m.instance_id), None) if machine is None: self.remove_machine(mw.machine) n_satisfying_machines = len(machines) for m in machines: if not satisfies(m, self.constraints)[0]: self.remove_machine(m) n_satisfying_machines -= 1 continue assignment_names = "" ad = self.controller.assignments_for_machine(m) for atype, al in ad.items(): assignment_names += " ".join(["{} {}".format(cc.charm_name, cc.display_name) for cc in al]) filter_label = "{} {}".format(m.filter_label(), assignment_names) if self.filter_string != "" and \ self.filter_string not in filter_label: self.remove_machine(m) continue mw = self.find_machine_widget(m) if mw is None: mw = self.add_machine_widget(m) mw.update() self.filter_edit_box.set_info(len(self.machine_widgets), n_satisfying_machines) def add_machine_widget(self, machine): mw = MachineWidget(machine, self.controller, self.actions, self.show_hardware, self.show_assignments) self.machine_widgets.append(mw) options = self.machine_pile.options() self.machine_pile.contents.append((mw, options)) self.machine_pile.contents.append((AttrMap(Padding(Divider('\u23bc'), left=2, right=2), 'label'), options)) return mw def remove_machine(self, machine): mw = self.find_machine_widget(machine) if mw is None: return self.machine_widgets.remove(mw) mw_idx = 0 for w, opts in self.machine_pile.contents: if w == mw: break mw_idx += 1 c = self.machine_pile.contents[:mw_idx] + \ self.machine_pile.contents[mw_idx + 2:] self.machine_pile.contents = c
class ServicesList(WidgetWrap): """A list of services with flexible display options. Note that not all combinations of display options make sense. YMMV. controller - a PlacementController display_controller - a PlacerView machine - a machine instance to query for constraint checking. If None, no constraint checking is done. If set, only services whose constraints are satisfied by 'machine' are shown. """ def __init__(self, placement_controller, display_controller, show_placements=False, title="Services"): self.placement_controller = placement_controller self.display_controller = display_controller self.service_widgets = [] self.show_placements = show_placements self.title = title w = self.build_widgets() self.update() super().__init__(w) def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self): widgets = [] if self.title: widgets = [Text(self.title), Divider(' ')] widgets += self.service_widgets self.service_pile = Pile(widgets) return self.service_pile def focus_top_or_next(self): return self._setfocus(top=False) def focus_top(self): return self._setfocus(top=True) def _setfocus(self, top): try: if top: self.service_pile.focus_position = 0 else: pos = self.service_pile.focus_position if pos + 1 >= len(self.service_widgets): return False self.service_pile.focus_position = pos + 1 except IndexError: log.debug("caught indexerror in servicescolumn.focus_next") return False return True def focused_service_widget(self): if len(self.service_widgets) > 0: return self.service_pile.focus return None def find_service_widget(self, s): return next((sw for sw in self.service_widgets if sw.service.service_name == s.service_name), None) def update(self): for s in self.placement_controller.services(): sw = self.find_service_widget(s) if sw is None: sw = self.add_service_widget(s) sw.update() allnames = [s.service_name for s in self.placement_controller.services()] for sw in self.service_widgets: if sw.service.service_name not in allnames: self.remove_service_widget(sw.service) self.sort_service_widgets() def add_service_widget(self, service): sw = SimpleServiceWidget(service, self.placement_controller, self.display_controller, show_placements=self.show_placements) self.service_widgets.append(sw) options = self.service_pile.options() self.service_pile.contents.append((sw, options)) # NOTE: see the + 1: indexing in remove_service_widget if you # re-add this divider. it should then be +2. # self.service_pile.contents.append((AttrMap(Padding(Divider('\u23bc'), # left=2, right=2), # 'label'), options)) return sw def remove_service_widget(self, service): sw = self.find_service_widget(service) if sw is None: return self.service_widgets.remove(sw) sw_idx = 0 for w, opts in self.service_pile.contents: if w == sw: break sw_idx += 1 c = self.service_pile.contents[:sw_idx] + \ self.service_pile.contents[sw_idx + 1:] self.service_pile.contents = c def sort_service_widgets(self): def keyfunc(sw): s = sw.service if s.subordinate: skey = 'z' else: skey = s.service_name return skey self.service_widgets.sort(key=keyfunc) def wrappedkeyfunc(t): mw, options = t if not isinstance(mw, SimpleServiceWidget): return 'A' return keyfunc(mw) self.service_pile.contents.sort(key=wrappedkeyfunc) def select_service(self, service_name): idx = 0 for w, opts in self.service_pile.contents: if w.service.service_name == service_name: self.service_pile.focus_position = idx return idx += 1
class MachinesList(WidgetWrap): """A list of machines with configurable action buttons for each machine. select_cb - a function that takes a machine and assignmenttype to perform the button action unselect_cb - a function that takes a machine and clears assignments target_info - a string that describes what you're picking a machine for current_pin_cb - a function that takes a machine and returns None if the machine is not currently selected, or returns a context value to show the user about the current selection (like a juju machine ID that represents the current pin) constraints - a dict of constraints to filter the machines list. only machines matching all the constraints will be shown. show_hardware - bool, whether or not to show the hardware details for each of the machines title_widgets - A Text Widget to be used in place of the default title. show_only_ready - bool, only show machines with a ready state. show_filter_box - bool, show text box to filter listed machines """ def __init__(self, select_cb, unselect_cb, target_info, current_pin_cb, constraints=None, show_hardware=False, title_widgets=None, show_only_ready=False, show_filter_box=False): self.select_cb = select_cb self.unselect_cb = unselect_cb self.target_info = target_info self.current_pin_cb = current_pin_cb self.n_selected = 0 self.machine_widgets = [] if constraints is None: self.constraints = {} else: self.constraints = constraints self.show_hardware = show_hardware self.show_only_ready = show_only_ready self.show_filter_box = show_filter_box self.filter_string = "" self.loading = False w = self.build_widgets(title_widgets) self.update() super().__init__(w) def __repr__(self): return "machineslist" def selectable(self): # overridden to ensure that we can arrow through the buttons # shouldn't be necessary according to documented behavior of # Pile & Columns, but discovered via trial & error. return True def build_widgets(self, title_widgets): if title_widgets is None: if len(self.constraints) > 0: cstr = " matching constraints '{}'".format( constraints_from_dict(self.constraints)) else: cstr = "" title_widgets = [Text("Machines" + cstr, align='center')] self.filter_edit_box = FilterBox(self.handle_filter_change) header_widgets = title_widgets + [Divider()] if self.show_filter_box: header_widgets += [self.filter_edit_box, Divider()] labels = ["FQDN", "Cores", "Memory (GiB)", "Storage (GiB)", ""] header_label_col = Columns([Text(m) for m in labels]) header_widgets.append(header_label_col) self.header_padding = len(header_widgets) self.machine_pile = Pile(header_widgets + self.machine_widgets) return self.machine_pile def handle_filter_change(self, edit_button, userdata): self.filter_string = userdata self.update() def find_machine_widget(self, m): return next((mw for mw in self.machine_widgets if mw.machine.instance_id == m.instance_id), None) def update(self): if app.maas.client: machines = app.maas.client.get_machines() else: machines = None if machines is None: if not self.loading: self.loading = True p = (Text("\n\nLoading...", align='center'), self.machine_pile.options()) self.machine_pile.contents.append(p) return if self.loading: self.loading = False self.machine_pile.contents = self.machine_pile.contents[:-1] if self.show_only_ready: machines = [m for m in machines if m.status == MaasMachineStatus.READY] for mw in self.machine_widgets: machine = next((m for m in machines if mw.machine.instance_id == m.instance_id), None) if machine is None: self.remove_machine(mw.machine) n_satisfying_machines = len(machines) def get_placement_filter_label(d): s = "" for atype, al in d.items(): s += " ".join(["{} {}".format(cc.service_name, cc.display_name) for cc in al]) return s for m in machines: if not satisfies(m, self.constraints)[0]: self.remove_machine(m) n_satisfying_machines -= 1 continue filter_label = m.filter_label() if self.filter_string != "" and \ self.filter_string not in filter_label: self.remove_machine(m) continue mw = self.find_machine_widget(m) if mw is None: mw = self.add_machine_widget(m) mw.update() self.filter_edit_box.set_info(len(self.machine_widgets), n_satisfying_machines) self.sort_machine_widgets() def add_machine_widget(self, machine): mw = MachineWidget(machine, self.handle_select, self.handle_unselect, self.target_info, self.current_pin_cb) self.machine_widgets.append(mw) options = self.machine_pile.options() self.machine_pile.contents.append((mw, options)) return mw def remove_machine(self, machine): mw = self.find_machine_widget(machine) if mw is None: return self.machine_widgets.remove(mw) mw_idx = 0 for w, opts in self.machine_pile.contents: if w == mw: break mw_idx += 1 c = self.machine_pile.contents[:mw_idx] + \ self.machine_pile.contents[mw_idx + 1:] self.machine_pile.contents = c def sort_machine_widgets(self): def keyfunc(mw): m = mw.machine hwinfo = " ".join(map(str, [m.arch, m.cpu_cores, m.mem, m.storage])) if str(mw.machine.status) == 'ready': skey = 'A' else: skey = str(mw.machine.status) return skey + mw.machine.hostname + hwinfo self.machine_widgets.sort(key=keyfunc) def wrappedkeyfunc(t): mw, options = t if not isinstance(mw, MachineWidget): return 'A' return keyfunc(mw) self.machine_pile.contents.sort(key=wrappedkeyfunc) def focus_prev_or_top(self): self.update() try: if self.machine_pile.focus_position <= self.header_padding: self.machine_pile.focus_position = self.header_padding except IndexError: log.debug("index error in machines_list focus_top") def handle_select(self, machine): self.select_cb(machine) self.n_selected += 1 self.update() def handle_unselect(self, machine): self.unselect_cb(machine) self.n_selected -= 1 self.update()
class NewCloudView(WidgetWrap): def __init__(self, app, cloud, schema, cb): self.app = app self.cloud = cloud self.input_items = schema self.cb = cb self.frame = Frame(body=self._build_widget(), footer=self._build_footer()) self.buttons_selected = False super().__init__(self.frame) def _build_widget(self): total_items = [Text( "Enter your {} credentials:".format(self.cloud.upper()))] total_items += [HR()] for k in self.input_items.keys(): display = k if k.startswith('_'): # Don't treat 'private' keys as input continue if k.startswith('@'): # Strip public, not storable attribute display = k[1:] col = Columns( [ ('weight', 0.5, Text(display, align='right')), Color.string_input(self.input_items[k], focus_map='string_input focus') ], dividechars=1 ) total_items.append(col) total_items.append(Padding.line_break("")) total_items.append(Text("")) self.pile = Pile(total_items) return Padding.center_60(Filler(self.pile, valign="top")) def _build_footer(self): cancel = menu_btn(on_press=self.cancel, label="\n BACK\n") confirm = menu_btn(on_press=self.submit, label="\n ADD CREDENTIAL\n") self.buttons = Columns([ ('fixed', 2, Text("")), ('fixed', 13, Color.menu_button( cancel, focus_map='button_primary focus')), Text(""), ('fixed', 20, Color.menu_button( confirm, focus_map='button_primary focus')), ('fixed', 2, Text("")) ]) footer = Color.frame_footer(Pile([ Padding.line_break(""), self.buttons ])) return footer def _swap_focus(self): if not self.buttons_selected: self.buttons_selected = True self.frame.focus_position = 'footer' self.buttons.focus_position = 3 else: self.buttons_selected = False self.frame.focus_position = 'body' def keypress(self, size, key): if key in ['tab', 'shift tab']: self._swap_focus() rv = super().keypress(size, key) return rv def validate(self): """ Will provide an error text if any fields are blank """ values = [i.value for i in self.input_items.values() if isinstance(i, StringEditor)] if None in values: self.pile.contents[-1] = ( Padding.center_60( Color.error_major( Text("Please fill all required fields."))), self.pile.options()) return False return True def cancel(self, btn): self.cb(back=True) def submit(self, result): if self.validate(): self.cb(self.input_items)
class ApplicationConfigureView(WidgetWrap): def __init__(self, application, metadata_controller, controller): self.controller = controller self.application = application self.options_copy = self.application.options.copy() self.metadata_controller = metadata_controller self.widgets = self.build_widgets() self.description_w = Text("") self.showing_all = False self.buttons_selected = False self.frame = Frame(body=self.build_widgets(), footer=self.build_footer()) super().__init__(self.frame) self.metadata_controller.get_readme( self.application.csid.as_seriesname(), partial(self._handle_readme_load)) def _handle_readme_load(self, readme_f): EventLoop.loop.event_loop._loop.call_soon_threadsafe( partial(self._update_readme_on_main_thread, readme_f.result())) def _update_readme_on_main_thread(self, readme): rt = self._trim_readme(readme) self.description_w.set_text(rt) def _trim_readme(self, readme): rls = readme.splitlines() rls = [l for l in rls if not l.startswith("#")] nrls = [] for i in range(len(rls)): if i + 1 == len(rls): break if len(rls[i]) > 0: if rls[i][0] in ['-', '#', '=']: continue if len(rls[i + 1]) > 0: if rls[i + 1][0] in ['-', '=']: continue nrls.append(rls[i]) if len(nrls) == 0: return if nrls[0] == '': nrls = nrls[1:] # split after two paragraphs: if '' in nrls: firstparidx = nrls.index('') else: firstparidx = 1 try: splitidx = nrls.index('', firstparidx + 1) except: splitidx = firstparidx nrls = nrls[:splitidx] return "\n".join(nrls) def selectable(self): return True def keypress(self, size, key): # handle keypress first, then get new focus widget rv = super().keypress(size, key) if key in ['tab', 'shift tab']: self._swap_focus() return rv def _swap_focus(self): if not self.buttons_selected: self.buttons_selected = True self.frame.focus_position = 'footer' self.buttons.focus_position = 3 else: self.buttons_selected = False self.frame.focus_position = 'body' def build_widgets(self): ws = [Text("Configure {}".format( self.application.service_name))] num_unit_ow = OptionWidget("Units", "int", "How many units to deploy.", self.application.orig_num_units, current_value=self.application.num_units, value_changed_callback=self.handle_scale) ws.append(num_unit_ow) ws += self.get_whitelisted_option_widgets() self.toggle_show_all_button_index = len(ws) + 1 self.toggle_show_all_button = PlainButton( "Show Advanced Configuration", self.do_toggle_show_all_config) ws += [HR(), Columns([('weight', 1, Text(" ")), (36, Color.button_secondary( self.toggle_show_all_button))])] self.pile = Pile(ws) return Padding.center_90(Filler(self.pile, valign="top")) def build_footer(self): cancel = menu_btn(on_press=self.do_cancel, label="\n BACK\n") confirm = menu_btn(on_press=self.do_commit, label="\n APPLY CHANGES\n") self.buttons = Columns([ ('fixed', 2, Text("")), ('fixed', 13, Color.menu_button( cancel, focus_map='button_primary focus')), Text(""), ('fixed', 20, Color.menu_button( confirm, focus_map='button_primary focus')), ('fixed', 2, Text("")) ]) footer = Pile([ HR(top=0), Padding.center_90(self.description_w), Padding.line_break(""), Color.frame_footer(Pile([ Padding.line_break(""), self.buttons])) ]) return footer def get_whitelisted_option_widgets(self): service_id = self.application.csid.as_str_without_rev() options = self.metadata_controller.get_options(service_id) svc_opts_whitelist = utils.get_options_whitelist( self.application.service_name) hidden = [n for n in options.keys() if n not in svc_opts_whitelist] log.info("Hiding options not in the whitelist: {}".format(hidden)) return self._get_option_widgets(svc_opts_whitelist, options) def get_non_whitelisted_option_widgets(self): service_id = self.application.csid.as_str_without_rev() options = self.metadata_controller.get_options(service_id) svc_opts_whitelist = utils.get_options_whitelist( self.application.service_name) hidden = [n for n in options.keys() if n not in svc_opts_whitelist] return self._get_option_widgets(hidden, options) def _get_option_widgets(self, opnames, options): ws = [] for opname in opnames: opdict = options[opname] cv = self.application.options.get(opname, None) ow = OptionWidget(opname, opdict['Type'], opdict['Description'], opdict['Default'], current_value=cv, value_changed_callback=self.handle_edit) ws.append(ow) return ws def do_toggle_show_all_config(self, sender): if not self.showing_all: new_ows = self.get_non_whitelisted_option_widgets() header = Text("Advanced Configuration Options") opts = self.pile.options() self.pile.contents.append((header, opts)) for ow in new_ows: self.pile.contents.append((ow, opts)) self.toggle_show_all_button.set_label( "Hide Advanced Configuration") self.showing_all = True else: i = self.toggle_show_all_button_index self.pile.contents = self.pile.contents[:i + 1] self.toggle_show_all_button.set_label( "Show Advanced Configuration") self.showing_all = False def handle_edit(self, opname, value): self.options_copy[opname] = value def handle_scale(self, opname, scale): self.application.num_units = scale def do_cancel(self, sender): self.controller.handle_sub_view_done() def do_commit(self, sender): self.application.options = self.options_copy self.controller.handle_sub_view_done()
class RelationsColumn(WidgetWrap): """UI to edit relations of a service """ def __init__(self, display_controller, placement_controller, placement_view, metadata_controller): self.placement_controller = placement_controller self.metadata_controller = metadata_controller self.service = None self.provides = set() self.requires = set() self.placement_view = placement_view w = self.build_widgets() super().__init__(w) self.update() def build_widgets(self): self.title = Text('') self.relation_widgets = [] self.pile = Pile([Divider(), self.title] + self.relation_widgets) return self.pile def refresh(self): self.set_service(self.service) def set_service(self, service): self.service = service self.metadata_controller.add_charm(service.csid.as_str_without_rev()) self.pile.contents = self.pile.contents[:2] self.provides = set() self.requires = set() self.relation_widgets = [] def update(self): if self.service is None: return self.title.set_text(('body', "Edit relations for {}".format( self.service.service_name))) if len(self.relation_widgets) == 0: self.title.set_text(('body', "Loading Relations...")) else: self.title.set_text(('body', "Edit Relations: (Changes are " "saved immediately)")) p = set(self.metadata_controller.get_provides( self.service.csid.as_str_without_rev())) r = set(self.metadata_controller.get_requires( self.service.csid.as_str_without_rev())) new_provides = p - self.provides self.provides.update(p) new_requires = r - self.requires self.requires.update(r) mc = self.metadata_controller args = [(relname, iface, RelationType.Provides, mc.get_services_for_iface(iface, RelationType.Requires)) for relname, iface in sorted(new_provides)] args += [(relname, iface, RelationType.Requires, mc.get_services_for_iface(iface, RelationType.Provides)) for relname, iface in sorted(new_requires)] for relname, iface, reltype, matches in args: if len(matches) == 0: rw = NoRelationWidget(relname, iface, reltype) self.relation_widgets.append(rw) self.pile.contents.append((rw, self.pile.options())) for tgt_relname, tgt_service in matches: if tgt_service.service_name == self.service.service_name: continue rw = RelationWidget(self.service.service_name, relname, iface, reltype, tgt_service.service_name, tgt_relname, self.placement_controller, self.do_select) self.relation_widgets.append(rw) self.pile.contents.append((rw, self.pile.options())) for w, _ in self.pile.contents[2:]: w.update() self.sort_relation_widgets() def focus_prev_or_top(self): # ? self.pile.focus_position = len(self.pile.contents) - 1 if len(self.pile.contents) <= 2: return pos = self.pile.focus_position if pos < 2: self.pile.focus_position = 2 def do_select(self, source_relname, tgt_service_name, tgt_relation_name): self.placement_controller.toggle_relation(self.service.service_name, source_relname, tgt_service_name, tgt_relation_name) self.update() def sort_relation_widgets(self): def keyfunc(rw): return str(rw.reltype) + rw.source_relname self.relation_widgets.sort(key=keyfunc) def wrappedkeyfunc(t): rw, options = t if isinstance(rw, RelationWidget): return keyfunc(rw) if isinstance(rw, NoRelationWidget): return 'z' + keyfunc(rw) return 'A' self.pile.contents.sort(key=wrappedkeyfunc)
class OptionsColumn(WidgetWrap): """UI to edit options of a service """ def __init__(self, display_controller, placement_controller, placement_view, metadata_controller): self.placement_controller = placement_controller self.metadata_controller = metadata_controller self.service = None self.filter_string = "" self.placement_view = placement_view w = self.build_widgets() super().__init__(w) self.update() def build_widgets(self): self.title = Text('') self.option_widgets = [] self.pile = Pile([Divider(), self.title] + self.option_widgets) return self.pile def refresh(self): self.set_service(self.service) def set_service(self, service): self.service = service self.metadata_controller.add_charm(service.csid.as_str_without_rev()) self.pile.contents = self.pile.contents[:2] self.option_widgets = [] def update(self): if self.service is None: return self.title.set_text(('body', "Edit Options for {}".format( self.service.service_name))) if len(self.option_widgets) == 0: if self.filter_string != "": self.title.set_text( ('body', "No options match '{}'".format(self.filter_string))) else: self.title.set_text(('body', "Loading Options...")) else: self.title.set_text(('body', "Edit Options: (Changes are " "saved immediately)")) mc = self.metadata_controller options = mc.get_options(self.service.csid.as_str_without_rev()) for opname, opdict in sorted(options.items()): if self.filter_string != "" and \ self.filter_string not in opname: self.remove_option_widget(opname) continue ow = self.find_option_widget(opname) if ow is None: ow = self.add_option_widget(opname, opdict) ow.update() # MMCC TODO set filterbox.set_info for w, _ in self.pile.contents[2:]: w.update() self.sort_option_widgets() def handle_filter_change(self, edit_button, userdata): self.filter_string = userdata self.update() def handle_edit(self, opname, value): self.placement_controller.set_option(self.service.service_name, opname, value) def find_option_widget(self, opname): return next((ow for ow in self.option_widgets if ow.name == opname), None) def add_option_widget(self, opname, opdict): cv = self.service.options.get(opname, None) ow = OptionWidget(opname, opdict['Type'], opdict['Description'], opdict['Default'], current_value=cv, value_changed_callback=self.handle_edit) self.option_widgets.append(ow) self.pile.contents.append((ow, self.pile.options())) return ow def remove_option_widget(self, opname): ow = self.find_option_widget(opname) if ow is None: return self.option_widgets.remove(ow) ow_idx = 0 for w, opts in self.pile.contents: if w == ow: break ow_idx += 1 c = self.pile.contents[:ow_idx] + \ self.pile.contents[ow_idx + 1:] self.pile.contents = c def focus_prev_or_top(self): # ? self.pile.focus_position = len(self.pile.contents) - 1 if len(self.pile.contents) <= 2: return pos = self.pile.focus_position if pos < 2: self.pile.focus_position = 2 def sort_option_widgets(self): def keyfunc(ow): return str(ow.name) self.option_widgets.sort(key=keyfunc) def wrappedkeyfunc(t): rw, options = t if isinstance(rw, OptionWidget): return keyfunc(rw) return 'A' self.pile.contents.sort(key=wrappedkeyfunc)
class SimpleMachineWidget(WidgetWrap): """A widget displaying a machine. When selected, shows action buttons for placement types. machine - the machine to display controller - a PlacementController instance display_controller - a PlacerView instance show_assignments - display info about which charms are assigned and what assignment type (LXC, KVM, etc) they have. """ def __init__(self, machine, controller, display_controller, show_assignments=True): self.machine = machine self.controller = controller self.display_controller = display_controller self.show_assignments = show_assignments self.is_selected = False w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): self.button = MenuSelectButton("I AM A MACHINE", self.do_select) self.action_button_cols = Columns([]) self.action_buttons = [] self.pile = Pile([self.button]) return self.pile def update_machine(self): """Refresh with potentially updated machine info from controller. Assumes that machine exists - machines going away is handled in machineslist.update(). """ self.machine = next((m for m in self.controller.machines() if m.instance_id == self.machine.instance_id), None) def update(self): self.update_machine() self.update_action_buttons() if self.is_selected: self.update_selected() else: self.update_unselected() def info_markup(self): m = self.machine return [self.machine.hostname + "\n", 'arch: {} '.format(m.arch), 'cores: {} '.format(m.cpu_cores), 'mem: {} '.format(m.mem), 'storage: {}'.format(m.storage)] def update_selected(self): cn = self.display_controller.selected_service.service_name msg = Text(" Add {} to {}:".format(cn, self.machine.hostname)) self.pile.contents = [(msg, self.pile.options()), (self.action_button_cols, self.pile.options()), (Divider(), self.pile.options())] def update_unselected(self): markup = self.info_markup() self.button.set_label(markup) self.pile.contents = [(AttrMap(self.button, 'text', 'button_secondary focus'), self.pile.options()), (Divider(), self.pile.options())] def update_action_buttons(self): all_actions = [(AssignmentType.BareMetal, 'Add as Bare Metal', self.select_baremetal), (AssignmentType.LXD, 'Add as LXD', self.select_lxd), (AssignmentType.KVM, 'Add as KVM', self.select_kvm)] sc = self.display_controller.selected_service if sc: allowed_set = set(sc.allowed_assignment_types) allowed_types = set([atype for atype, _, _ in all_actions]) allowed_types = allowed_types.intersection(allowed_set) else: allowed_types = set() # + 1 for the cancel button: if len(self.action_buttons) == len(allowed_types) + 1: return self.action_buttons = [AttrMap(PlainButton(label, on_press=func), 'button_secondary', 'button_secondary focus') for atype, label, func in all_actions if atype in allowed_types] self.action_buttons.append( AttrMap(PlainButton("Cancel", on_press=self.do_cancel), 'button_secondary', 'button_secondary focus')) opts = self.action_button_cols.options() self.action_button_cols.contents = [(b, opts) for b in self.action_buttons] def do_select(self, sender): if self.display_controller.selected_service is None: return self.is_selected = True self.update() self.pile.focus_position = 1 self.action_button_cols.focus_position = 0 def do_cancel(self, sender): self.is_selected = False self.update() self.pile.focus_position = 0 def _do_select_assignment(self, atype): {AssignmentType.BareMetal: self.display_controller.do_select_baremetal, AssignmentType.LXD: self.display_controller.do_select_lxd, AssignmentType.KVM: self.display_controller.do_select_kvm}[atype](self.machine) self.pile.focus_position = 0 def select_baremetal(self, sender): self._do_select_assignment(AssignmentType.BareMetal) def select_lxd(self, sender): self._do_select_assignment(AssignmentType.LXD) def select_kvm(self, sender): self._do_select_assignment(AssignmentType.KVM)
class MachinesColumn(WidgetWrap): """Shows machines or a link to MAAS to add more""" def __init__(self, display_controller, placement_controller, placement_view): self.display_controller = display_controller self.placement_controller = placement_controller self.placement_view = placement_view w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): def has_services_p(m): pc = self.placement_controller n = sum([len(al) for at, al in pc.assignments_for_machine(m).items()]) return n > 0 clear_machine_func = self.placement_view.do_clear_machine show_chooser_func = self.placement_view.do_show_service_chooser self.open_maas_button = AttrMap(Button("Open in Browser", on_press=self.browse_maas), 'button_secondary', 'button_secondary focus') bc = self.placement_view.config.juju_env['bootstrap-config'] maasname = "'{}' <{}>".format(bc['name'], bc['maas-server']) maastitle = "Connected to MAAS {}".format(maasname) tw = Columns([Text(maastitle), Padding(self.open_maas_button, align='right', width=BUTTON_SIZE, right=2)]) self.machines_list = MachinesList(self.placement_controller, [(has_services_p, 'Clear All Services', clear_machine_func), (has_services_p, 'Remove Some Services', show_chooser_func)], show_hardware=True, title_widgets=tw) self.machines_list.update() self.machines_list_pile = Pile([self.machines_list, Divider()]) # placeholders replaced in update() with absolute indexes, so # if you change this list, check update(). pl = [Text(('body', "Machines {}".format(MetaScroll().get_text()[0])), align='center'), Divider(), Pile([]), # machines_list Divider()] self.main_pile = Pile(pl) return self.main_pile def update(self): self.machines_list.update() bc = self.placement_view.config.juju_env['bootstrap-config'] empty_maas_msg = ("There are no available machines.\n" "Open {} to add machines to " "'{}':".format(bc['maas-server'], bc['name'])) self.empty_maas_widgets = Pile([Text([('error_icon', "\N{WARNING SIGN} "), empty_maas_msg], align='center'), Padding(self.open_maas_button, align='center', width=BUTTON_SIZE)]) # 1 machine is the subordinate placeholder: if len(self.placement_controller.machines()) == 1: self.main_pile.contents[2] = (self.empty_maas_widgets, self.main_pile.options()) else: self.main_pile.contents[2] = (self.machines_list_pile, self.main_pile.options()) def browse_maas(self, sender): bc = self.placement_view.config.juju_env['bootstrap-config'] try: p = Popen(["sensible-browser", bc['maas-server']], stdout=PIPE, stderr=PIPE) outs, errs = p.communicate(timeout=5) except TimeoutExpired: # went five seconds without an error, so we assume it's # OK. Don't kill it, just let it go: return e = errs.decode('utf-8') msg = "Error opening '{}' in a browser:\n{}".format(bc['name'], e) w = InfoDialogWidget(msg, self.placement_view.remove_overlay) self.placement_view.show_overlay(w)
class ServiceWalkthroughView(WidgetWrap): def __init__(self, service, idx, n_total, metadata_controller, callback): self.callback = callback self.service = service self.idx = idx self.n_total = n_total self.n_remaining = n_total - idx - 1 self.metadata_controller = metadata_controller self.info_handled = False w = self.build_widgets() super().__init__(w) self.get_async_info() if self.n_remaining == 0: self.pile.focus_position = len(self.pile.contents) - 1 else: self.pile.focus_position = len(self.pile.contents) - 2 def selectable(self): return True def build_widgets(self): self.description_w = Text("Description Loading…") self.readme_w = Text("README Loading…") self.scale_edit = IntegerEditor(default=self.service.num_units) connect_signal(self.scale_edit._edit, 'change', self.handle_scale_changed) self.skip_rest_button = PlainButton( "Deploy all {} Remaining Applications with Bundle Defaults".format( self.n_remaining), self.do_skip_rest ) col = Columns( [ (6, Text('Units:', align='right')), (15, Color.string_input(self.scale_edit, focus_map='string_input focus')) ], dividechars=1 ) if self.n_remaining == 0: buttons = [Padding.right_50(Color.button_primary( PlainButton("Deploy and Continue", self.do_deploy), focus_map='button_primary focus'))] else: buttons = [ Padding.right_50(Color.button_primary( PlainButton( "Deploy and Configure Next Application", self.do_deploy), focus_map='button_primary focus')), Padding.right_50( Color.button_secondary( self.skip_rest_button, focus_map='button_secondary focus'))] ws = [Text("{} of {}: {}".format(self.idx+1, self.n_total, self.service.service_name.upper())), Padding.center(HR()), Padding.center(self.description_w, left=2), Padding.line_break(""), Padding.center(self.readme_w, left=2), Padding.center(HR())] if not self.service.subordinate: ws.append(Padding.left(col, left=1)) ws.append(Padding.line_break("")) ws += buttons self.pile = Pile(ws) return Padding.center_90(Filler(self.pile, valign="top")) def get_async_info(self): info = self.metadata_controller.get_charm_info( self.service.csid.as_str_without_rev(), self.handle_info_updated) if info: self.handle_info_updated(info) self.metadata_controller.get_readme( self.service.csid.as_seriesname(), self.handle_readme_updated) def handle_info_updated(self, new_info): if self.info_handled: return self.info_handled = True EventLoop.loop.event_loop._loop.call_soon_threadsafe( self._update_info_on_main_thread, new_info) def _update_info_on_main_thread(self, new_info): self.description_w.set_text( new_info["Meta"]["charm-metadata"]["Summary"]) self._invalidate() self.add_options() def add_options(self): service_id = self.service.csid.as_str_without_rev() options = self.metadata_controller.get_options(service_id) metadata = app.config.get('metadata', None) if metadata is None: return options_whitelist = metadata.get('options-whitelist', None) if options_whitelist is None: return svc_opts_whitelist = options_whitelist.get(self.service.service_name, []) hidden = [n for n in options.keys() if n not in svc_opts_whitelist] log.info("Hiding options not in the whitelist: {}".format(hidden)) for opname in svc_opts_whitelist: opdict = options[opname] self.add_option_widget(opname, opdict) def add_option_widget(self, opname, opdict): cv = self.service.options.get(opname, None) ow = OptionWidget(opname, opdict['Type'], opdict['Description'], opdict['Default'], current_value=cv, value_changed_callback=self.handle_edit) self.pile.contents.insert(7, (ow, self.pile.options())) self._invalidate() return ow def handle_edit(self, opname, value): self.service.options[opname] = value def handle_readme_updated(self, readme_text_f): EventLoop.loop.event_loop._loop.call_soon_threadsafe( self._update_readme_on_main_thread, readme_text_f) def _update_readme_on_main_thread(self, readme_text_f): rls = readme_text_f.result().splitlines() rls = [l for l in rls if not l.startswith("#")] nrls = [] for i in range(len(rls)): if i+1 == len(rls): break if len(rls[i]) > 0: if rls[i][0] in ['-', '#', '=']: continue if len(rls[i+1]) > 0: if rls[i+1][0] in ['-', '=']: continue nrls.append(rls[i]) if len(nrls) == 0: return if nrls[0] == '': nrls = nrls[1:] # split after two paragraphs: if '' in nrls: firstparidx = nrls.index('') else: firstparidx = 1 try: splitidx = nrls.index('', firstparidx + 1) except: splitidx = firstparidx nrls = nrls[:splitidx] self.readme_w.set_text("\n".join(nrls)) self._invalidate() def handle_scale_changed(self, widget, newvalstr): if newvalstr == '': return self.service.num_units = int(newvalstr) def do_deploy(self, arg): self.callback(single_service=self.service) def do_skip_rest(self, arg): self.callback(single_service=None)
class SimpleServiceWidget(WidgetWrap): """A widget displaying a service as a button service - the service to display placement_controller - a PlacementController instance display_controller - a PlacerView instance callback - a function to be called when either of the buttons is pressed. The service will be passed to the function as userdata. show_placements - display the machine(s) currently assigned to host this service, both planned deployments (aka 'assignments', and already-deployed, called 'deployments'). """ def __init__(self, service, placement_controller, display_controller, show_placements=False): self.service = service self.placement_controller = placement_controller self.display_controller = display_controller self.show_placements = show_placements self.state = ServiceWidgetState.UNSELECTED w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): self.button = MenuSelectButton("I AM A SERVICE", self.do_select) self.action_button_cols = Columns([], dividechars=1) self.action_buttons = [] self.pile = Pile([self.button]) return self.pile def get_markup(self): if self.service.subordinate: return [self.service.service_name + " (subordinate)\n " + self.service.charm_source], [] nr = self.service.required_num_units() pl = "s" if nr > 1 else "" title_markup = [self.service.service_name + "\n {} unit{}: ".format(nr, pl) + self.service.charm_source] info_markup = [] if not self.display_controller.has_maas: return title_markup, info_markup pd = self.placement_controller.get_assignments(self.service) nplaced = sum([len(pd[k]) for k in pd]) if nr-nplaced > 0: pl = "" if nr-nplaced > 1: pl = "s" info_str = (" {} unit{} will be auto-placed " "by Juju\n".format(nr-nplaced, pl)) info_markup.append(info_str) def string_for_placement_dict(d): if self.display_controller.has_maas: defstring = "Bare Metal (Default)" else: defstring = "LXD (Default)" s = [] for atype, ml in sorted(d.items()): if atype == AssignmentType.DEFAULT: aname = defstring else: aname = atype.name hostnames = [m.hostname for m in ml] s.append(" {}: {}".format(aname, ", ".join(hostnames))) if len(s) == 0: return [] return "\n".join(s) ad = self.placement_controller.get_assignments(self.service) info_markup += string_for_placement_dict(ad) return title_markup, info_markup def update_choosing(self): title_markup, _ = self.get_markup() msg = Padding(Text(title_markup), left=2, right=2, align='center') self.pile.contents = [(msg, self.pile.options()), (self.action_button_cols, self.pile.options()), (Divider(), self.pile.options())] def update_default(self): title_markup, info_markup = self.get_markup() self.button.set_label(title_markup + ["\n"] + info_markup) if self.state == ServiceWidgetState.SELECTED: b = AttrMap(self.button, 'deploy_highlight_start', 'button_secondary focus') else: b = AttrMap(self.button, 'text', 'button_secondary focus') self.pile.contents = [(b, self.pile.options()), (Divider(), self.pile.options())] def update(self): self.service = next((s for s in self.placement_controller.bundle.services if s.service_name == self.service.service_name), self.service) self.update_action_buttons() if self.state == ServiceWidgetState.CHOOSING: self.update_choosing() else: self.update_default() def keypress(self, size, key): if key == 'backspace': self.display_controller.remove_service(self.service) elif key == '+': if not self.service.subordinate: self.display_controller.scale_service(self.service, 1) elif key == '-': if not self.service.subordinate: self.display_controller.scale_service(self.service, -1) return super().keypress(size, key) def update_action_buttons(self): all_actions = [] if self.display_controller.has_maas: all_actions = [('Choose Placement', self.handle_placement_button_pressed)] all_actions += [('Edit Relations', self.handle_relation_button_pressed), ('Edit Options', self.handle_options_button_pressed)] self.action_buttons = [AttrMap(PlainButton(label, on_press=func), 'button_secondary', 'button_secondary focus') for label, func in all_actions] self.action_buttons.append(AttrMap( PlainButton("Cancel", on_press=self.do_cancel), 'button_secondary', 'button_secondary focus')) opts = self.action_button_cols.options() self.action_button_cols.contents = [(b, opts) for b in self.action_buttons] def do_select(self, sender): self.display_controller.clear_selections() if self.state == ServiceWidgetState.SELECTED: self.state = ServiceWidgetState.UNSELECTED self.display_controller.set_selected_service(None) else: self.display_controller.set_selected_service(self.service) self.state = ServiceWidgetState.CHOOSING self.pile.focus_position = 1 self.action_button_cols.focus_position = 0 self.update() def do_cancel(self, sender): self.state = ServiceWidgetState.UNSELECTED self.update() self.display_controller.show_default_view() self.pile.focus_position = 0 def handle_placement_button_pressed(self, sender): self.state = ServiceWidgetState.SELECTED self.update() self.display_controller.edit_placement() self.pile.focus_position = 0 def handle_relation_button_pressed(self, sender): self.state = ServiceWidgetState.SELECTED self.update() self.display_controller.edit_relations() self.pile.focus_position = 0 def handle_options_button_pressed(self, sender): self.state = ServiceWidgetState.SELECTED self.update() self.display_controller.edit_options() self.pile.focus_position = 0
class OptionsColumn(WidgetWrap): """UI to edit options of a service """ def __init__(self, display_controller, placement_controller, placement_view, metadata_controller): self.placement_controller = placement_controller self.metadata_controller = metadata_controller self.service = None self.filter_string = "" self.placement_view = placement_view w = self.build_widgets() super().__init__(w) self.update() def build_widgets(self): self.title = Text('') self.option_widgets = [] self.pile = Pile([Divider(), self.title] + self.option_widgets) return self.pile def refresh(self): self.set_service(self.service) def set_service(self, service): self.service = service self.metadata_controller.add_charm(service.csid.as_str_without_rev()) self.pile.contents = self.pile.contents[:2] self.option_widgets = [] def update(self): if self.service is None: return self.title.set_text( ('body', "Edit Options for {}".format(self.service.service_name))) if len(self.option_widgets) == 0: if self.filter_string != "": self.title.set_text( ('body', "No options match '{}'".format(self.filter_string))) else: self.title.set_text(('body', "Loading Options...")) else: self.title.set_text(('body', "Edit Options: (Changes are " "saved immediately)")) mc = self.metadata_controller options = mc.get_options(self.service.csid.as_str_without_rev()) for opname, opdict in sorted(options.items()): if self.filter_string != "" and \ self.filter_string not in opname: self.remove_option_widget(opname) continue ow = self.find_option_widget(opname) if ow is None: ow = self.add_option_widget(opname, opdict) ow.update() # MMCC TODO set filterbox.set_info for w, _ in self.pile.contents[2:]: w.update() self.sort_option_widgets() def handle_filter_change(self, edit_button, userdata): self.filter_string = userdata self.update() def handle_edit(self, opname, value): self.placement_controller.set_option(self.service.service_name, opname, value) def find_option_widget(self, opname): return next((ow for ow in self.option_widgets if ow.name == opname), None) def add_option_widget(self, opname, opdict): cv = self.service.options.get(opname, None) ow = OptionWidget(opname, opdict['Type'], opdict['Description'], opdict['Default'], current_value=cv, value_changed_callback=self.handle_edit) self.option_widgets.append(ow) self.pile.contents.append((ow, self.pile.options())) return ow def remove_option_widget(self, opname): ow = self.find_option_widget(opname) if ow is None: return self.option_widgets.remove(ow) ow_idx = 0 for w, opts in self.pile.contents: if w == ow: break ow_idx += 1 c = self.pile.contents[:ow_idx] + \ self.pile.contents[ow_idx + 1:] self.pile.contents = c def focus_prev_or_top(self): # ? self.pile.focus_position = len(self.pile.contents) - 1 if len(self.pile.contents) <= 2: return pos = self.pile.focus_position if pos < 2: self.pile.focus_position = 2 def sort_option_widgets(self): def keyfunc(ow): return str(ow.name) self.option_widgets.sort(key=keyfunc) def wrappedkeyfunc(t): rw, options = t if isinstance(rw, OptionWidget): return keyfunc(rw) return 'A' self.pile.contents.sort(key=wrappedkeyfunc)