class HelpScreen(BaseScreen): '''Show localized help file pertaining to last traversed screen or help topics list from which to choose a desired help topic. ''' def __init__(self, main_win, help_header, help_index, intro): super(HelpScreen, self).__init__(main_win) try: self.locale = locale.setlocale(locale.LC_MESSAGES, "") except locale.Error: terminalui.LOGGER.warning("System configured with invalid " "locale(5), falling back to C.") self.locale = locale.setlocale(locale.LC_MESSAGES, "C") terminalui.LOGGER.debug("locale=%s", self.locale) self.help_header = help_header self.help_index = help_index self.intro = intro self.screen = None self.screen_last = None self.help_info = [] self.help_dict = None self.topics = False self.scroll_region = None self.cur_help_idx = 0 self.is_x86 = (platform.processor() == "i386") def setup_help_data(self, screens): '''Setup the help_dict and help_info structures help_dict contains: key: screen name tuple: (<helpfile_name>, <header for help screen>) tuple: (<helpfile_name>, <header for help screen and help topics menu entry>) help_info contains tuples: (tuple of screen names, format of text) ''' self.help_dict = {} self.help_info = [] for screen in screens: if screen.help_data[0]: key = screen.__class__.__name__ + screen.instance self.help_dict[key] = screen.help_data self.help_info.append((key, " " + screen.help_format)) terminalui.LOGGER.debug("self.help_dict=%s", self.help_dict) terminalui.LOGGER.debug("self.help_info=%s", self.help_info) def set_actions(self): '''Remove the continue key for help screen and Help key for help topics screen. Redirect F2_Continue to display the selected topic, when at the topics list ''' terminalui.LOGGER.debug("in set_actions self.class_name=%s", self.__class__.__name__) # change F6 description self.main_win.help_action.text = self.help_index # change continue to call continue_action, rather than # normal continue. Though we stay on the same screen, # we simulate the continue here by changing the screen text. # help_continue = Action(curses.KEY_F2, self.main_win.continue_action.text, self.continue_action) self.main_win.actions[help_continue.key] = help_continue if (self.screen == self.__class__.__name__): # help topics screen self.main_win.actions.pop(self.main_win.help_action.key, None) else: # help screen self.main_win.actions.pop(self.main_win.continue_action.key, None) def display_help_topics(self): '''Display the help topics screen.''' self.main_win.set_header_text(self.help_header) y_loc = 1 y_loc += self.center_win.add_paragraph(self.intro, y_loc, 1, max_x=(self.win_size_x - 1)) y_loc += 1 area = WindowArea(scrollable_lines=(len(self.help_info) + 1), y_loc=y_loc, x_loc=0) terminalui.LOGGER.debug("lines=%s", len(self.help_dict)) area.lines = self.win_size_y - (y_loc + 1) area.columns = self.win_size_x self.scroll_region = ScrollWindow(area, window=self.center_win) # add the entries to the screen terminalui.LOGGER.debug("range=%s", len(self.help_info)) for idx, info in enumerate(self.help_info): # create ListItem for each help topic topic_format = info[1] topic_title = self.help_dict[info[0]][1] help_topic = topic_format % topic_title hilite = min(self.win_size_x, textwidth(help_topic) + 1) list_item = ListItem(WindowArea(1, hilite, idx, 0), window=self.scroll_region, text=help_topic) terminalui.LOGGER.debug("self.screen_last=%s", self.screen_last) if self.screen_last == info[0]: terminalui.LOGGER.debug("Set cur_help_idx = %s", idx) self.cur_help_idx = idx terminalui.LOGGER.debug("beg_y=%d, beg_x=%d", *list_item.window.getbegyx()) self.center_win.activate_object(self.scroll_region) self.scroll_region.activate_object(self.cur_help_idx) def continue_action(self, dummy=None): '''Called when user presses F2 on help topics screen. Results in show being called again to display single file help of chosen topic. ''' terminalui.LOGGER.debug("continue_action:%s", self.scroll_region.active_object) cur_topic = self.scroll_region.active_object self.screen = self.help_info[cur_topic][0] terminalui.LOGGER.debug("continue_action self.screen=%s", self.screen) self.topics = False return self def display_help(self): '''Display the single file help screen''' # customize header help_header = "%s: %s" terminalui.LOGGER.debug("self.screen is =%s", self.screen) if self.screen in self.help_dict: help_header = help_header % (_("Help"), self.help_dict[self.screen][1]) help_text = self.get_help_text(self.help_dict[self.screen][0]) else: help_header = help_header % (_("Help"), _("Not Available")) help_text = _("Help for this screen is not available") self.main_win.set_header_text(help_header) help_text = convert_paragraph(help_text, self.win_size_x - 5) terminalui.LOGGER.debug("help_text #lines=%d, text is \n%s", len(help_text), help_text) area = WindowArea(x_loc=0, y_loc=1, scrollable_lines=(len(help_text) + 1)) area.lines = self.win_size_y - 1 area.columns = self.win_size_x self.scroll_region = ScrollWindow(area, window=self.center_win) self.scroll_region.add_paragraph(help_text, start_x=(area.x_loc + 3)) self.center_win.activate_object(self.scroll_region) def _show(self): '''Display the screen, either the single file help or help topics.''' terminalui.LOGGER.debug("in show self.screen=%s", self.screen) if (self.screen == self.__class__.__name__): terminalui.LOGGER.debug("setting self topics to true:") self.topics = True else: self.topics = False self.screen_last = self.screen terminalui.LOGGER.debug("setting self.screen_last to %s", self.screen_last) if self.topics: self.display_help_topics() else: self.display_help() def get_help_text(self, filename=None): ''' Get the localized help text for the filename passed in. First check locid directory. If not there, strip off dot extension (fr_FR.UTF-8 becomes fr_FR). If not there, truncate to 2 chars (fr). If not there, use C. ''' if not filename: return "" help_file = None try: for locid in self._get_locids(): full_path = filename % locid terminalui.LOGGER.debug("Accessing help file %s", full_path) if os.access(full_path, os.R_OK): break terminalui.LOGGER.debug("Opening help file %s", full_path) with open(full_path) as help_file: help_text = help_file.read() terminalui.LOGGER.debug("Done reading help file %s", full_path) except IOError: terminalui.LOGGER.debug("Unable to open help file %s", full_path) help_text = _("Help for this screen is not available") return help_text def _get_locids(self): '''Generate a list of possible locales - folders to check for help screen text in. Used by get_help_text() in conjunction with a screen's indicated help text file path. The list will include one or more of: * The current locale (e.g., "en_US.UTF-8") * The current locale, stripped of encoding (e.g., "en_US") * The current language, stripped of locale (e.g., "en") * The default locale ("C") ''' locale = self.locale locids = [locale] if "." in locale: locale = locale.split(".")[0] locids.append(locale) if len(locale) > 2: locids.append(locale[:2]) locids.append("C") return locids
class DiskWindow(InnerWindow): '''Display and edit disk information, including partitions and slices''' STATIC_PARTITION_HEADERS = [(12, _("Primary"), _("Logical")), (10, _(" Size(GB)"), _(" Size(GB)"))] EDIT_PARTITION_HEADERS = [(13, _("Primary"), _("Logical")), (10, _(" Size(GB)"), _(" Size(GB)")), (7, _(" Avail"), _(" Avail"))] STATIC_SLICE_HEADERS = [(13, _("Slice"), _("Slice")), (2, "#", "#"), (10, _(" Size(GB)"), _(" Size(GB)"))] EDIT_SLICE_HEADERS = [(13, _("Slice"), _("Slice")), (2, "#", "#"), (10, _(" Size(GB)"), _(" Size(GB)")), (7, _(" Avail"), _(" Avail"))] ADD_KEYS = {curses.KEY_LEFT: no_action, curses.KEY_RIGHT: no_action} DEAD_ZONE = 3 SCROLL_PAD = 2 MIN_SIZE = None REC_SIZE = None SIZE_PRECISION = Size(UI_PRECISION).get(Size.gb_units) DESTROYED_MARK = EditField.ASTERISK_CHAR def __init__(self, area, disk_info, editable=False, error_win=None, target_controller=None, **kwargs): '''See also InnerWindow.__init__ disk_info (required) - Either a Disk or Partition object containing the data to be represented. If a Partition objects is provided, it will be used for displaying slice data within that partition. If Disk has partition(s), those are displayed. If not, but it has slices, then those are displayed. If neither partition data nor slice data are available, a ValueError is raised. headers (required) - List of tuples to populate the header of this window with. The first item in each tuple should be the width of the header, the second item should be the left side header. editable (optional) - If True, the window will be created such that data is editable. target_controller(optional) - Target controller ''' global LOGGER LOGGER = logging.getLogger(INSTALL_LOGGER_NAME) self.headers = None self.orig_ext_part_field = None self.orig_logicals_active = False self.ext_part_field = None self.error_win = error_win self.editable = editable self.win_width = None self.left_win = None self.right_win = None self.list_area = None self.edit_area = None super(DiskWindow, self).__init__(area, add_obj=editable, **kwargs) self.left_header_string = None self.right_header_string = None self._orig_data = None self.disk_info = None self.has_partition_data = False self.key_dict[curses.KEY_LEFT] = self.on_arrow_key self.key_dict[curses.KEY_RIGHT] = self.on_arrow_key if self.editable: self.key_dict[curses.KEY_F5] = self.change_type self.tc = target_controller self._ui_obj = None self.ui_obj = disk_info self.set_disk_info(ui_obj=self.ui_obj) LOGGER.debug(self.ui_obj) if platform.processor() == "sparc": self.is_x86 = False else: self.is_x86 = True @property def ui_obj(self): return self._ui_obj @ui_obj.setter def ui_obj(self, part): ''' create and set the value for ui_obj depending on type ''' if isinstance(part, Disk): self._ui_obj = UIDisk(self.tc, parent=None, doc_obj=part) elif isinstance(part, Partition): self._ui_obj = UIPartition(self.tc, parent=None, doc_obj=part) else: # Must be a either a Disk or Partition. It's an error to be here raise RuntimeError("disk_info object is invalid") def _init_win(self, window): '''Require at least 70 columns and 6 lines to fit current needs for display of partitions and slices. Builds two inner ScrollWindows for displaying/editing the data. ''' if self.area.columns < 70: raise ValueError("Insufficient space - area.columns < 70") if self.area.lines < 6: raise ValueError("Insufficient space - area.lines < 6") self.win_width = (self.area.columns - DiskWindow.DEAD_ZONE + DiskWindow.SCROLL_PAD) / 2 super(DiskWindow, self)._init_win(window) win_area = WindowArea(self.area.lines - 1, self.win_width, 2, 0) win_area.scrollable_lines = self.area.lines - 2 self.left_win = ScrollWindow(win_area, window=self, add_obj=False) self.left_win.color = None self.left_win.highlight_color = None win_area.x_loc = self.win_width + DiskWindow.DEAD_ZONE win_area.scrollable_lines = 2 * MAX_EXT_PARTS self.right_win = ScrollWindow(win_area, window=self, add_obj=False) self.right_win.color = None self.right_win.highlight_color = None def set_disk_info(self, ui_obj=None, disk_info=None, no_part_ok=False): '''Set up this DiskWindow to represent disk_info''' if ui_obj is not None: disk_info = ui_obj.doc_obj elif disk_info is not None: self.ui_obj = disk_info else: # Should never be this case raise RuntimeError("Unable to find ui_obj or disk_info") part_list = disk_info.get_children(class_type=Partition) if part_list: self.has_partition_data = True else: slice_list = disk_info.get_children(class_type=Slice) if slice_list: self.has_partition_data = False else: # No partitions and no slices if no_part_ok: if self.is_x86: self.has_partition_data = True else: self.has_partition_data = False else: return if self.has_partition_data: if self.editable: self.headers = DiskWindow.EDIT_PARTITION_HEADERS self.list_area = WindowArea(1, self.headers[0][0] + self.headers[1][0], 0, DiskWindow.SCROLL_PAD) self.edit_area = WindowArea(1, self.headers[1][0], 0, self.headers[0][0]) else: self.headers = DiskWindow.STATIC_PARTITION_HEADERS else: if self.editable: self.headers = DiskWindow.EDIT_SLICE_HEADERS self.list_area = WindowArea(1, self.headers[0][0] + self.headers[1][0] + self.headers[2][0], 0, DiskWindow.SCROLL_PAD) self.edit_area = WindowArea(1, self.headers[2][0], 0, self.headers[0][0] + self.headers[1][0]) else: self.headers = DiskWindow.STATIC_SLICE_HEADERS LOGGER.debug("have_partition: %s", self.has_partition_data) LOGGER.debug(self.ui_obj) self.ui_obj.add_unused_parts(no_part_ok=no_part_ok) self.left_win.clear() self.right_win.clear() self.window.erase() self.print_headers() if self.editable: self.active_object = None self.build_edit_fields() self.right_win.bottom = max(0, len(self.right_win.all_objects) - self.right_win.area.lines) if self.has_partition_data: self.orig_ext_part_field = None for obj in self.left_win.objects: if (obj.data_obj.is_extended()): self.orig_ext_part_field = obj self.orig_logicals_active = True break else: self.print_data() def print_headers(self): '''Print the headers for the displayed data. header[0] - The width of this column. header[1] and header[2] are trimmed to this size header[1] - The internationalized text for the left window header[2] - The internationalized text for the right window ''' self.left_header_string = [] self.right_header_string = [] for header in self.headers: left_header_str = header[1] right_header_str = header[2] # Trim the header to fit in the column width, # splitting columns with at least 1 space # Pad with extra space(s) to align the columns left_header_str = fit_text_truncate(left_header_str, header[0] - 1, just="left") self.left_header_string.append(left_header_str) right_header_str = fit_text_truncate(right_header_str, header[0] - 1, just="left") self.right_header_string.append(right_header_str) self.left_header_string = " ".join(self.left_header_string) self.right_header_string = " ".join(self.right_header_string) LOGGER.debug(self.left_header_string) self.add_text(self.left_header_string, 0, DiskWindow.SCROLL_PAD) right_win_offset = (self.win_width + DiskWindow.DEAD_ZONE + DiskWindow.SCROLL_PAD) self.add_text(self.right_header_string, 0, right_win_offset) self.window.hline(1, DiskWindow.SCROLL_PAD, curses.ACS_HLINE, textwidth(self.left_header_string)) self.window.hline(1, right_win_offset, curses.ACS_HLINE, textwidth(self.right_header_string)) self.no_ut_refresh() def print_data(self): '''Print static (non-editable) data. Slices - fill the left side, then remaining slices on the right side. If for some reason not all slices fit, indicate how many more slices there area Partitions - Put standard partitions on the left, logical partitions on the right ''' part_index = 0 data = self.ui_obj.get_parts_in_use() if len(data) == 0: return # should never be this case if self.has_partition_data: max_parts = MAX_PRIMARY_PARTS else: max_parts = min(len(data), self.left_win.area.lines) win = self.left_win y_loc = 0 for next_data in data: LOGGER.debug("next_data: %s", next_data) if y_loc >= max_parts: if win is self.left_win: win = self.right_win y_loc = 0 max_parts = win.area.lines else: num_extra = len(data) - part_index if self.has_partition_data: more_parts_txt = _("%d more partitions") % num_extra else: more_parts_txt = _("%d more slices") % num_extra win.add_text(more_parts_txt, win.area.lines, 3) break x_loc = DiskWindow.SCROLL_PAD field = 0 win.add_text(next_data.get_description(), y_loc, x_loc, self.headers[field][0] - 1) x_loc += self.headers[field][0] field += 1 if not self.has_partition_data: win.add_text(str(next_data.name), y_loc, x_loc, self.headers[field][0] - 1) x_loc += self.headers[field][0] field += 1 win.add_text(locale.format("%*.1f", (self.headers[field][0] - 1, next_data.size.get(Size.gb_units))), y_loc, x_loc, self.headers[field][0] - 1) x_loc += self.headers[field][0] y_loc += 1 field += 1 part_index += 1 self.right_win.use_vert_scroll_bar = False self.no_ut_refresh() def build_edit_fields(self): '''Build subwindows for editing partition sizes For slices, fill the left side, then the right (right side scrolling as needed, though this shouldn't happen unless the number of slices on disk exceeds 8 for some reason) For partitions, fill the left side up to MAX_PRIMARY_PARTS, and place all logical partitions on the right. ''' data = self.ui_obj.get_parts_in_use() if self.has_partition_data: max_left_parts = MAX_PRIMARY_PARTS else: if len(data) == 0: return # should never be this case max_left_parts = min(len(data), self.left_win.area.lines) part_iter = iter(data) try: next_part = part_iter.next() self.objects.append(self.left_win) for y_loc in range(max_left_parts): self.list_area.y_loc = y_loc self.create_list_item(next_part, self.left_win, self.list_area) next_part = part_iter.next() self.objects.append(self.right_win) for y_loc in range(self.right_win.area.scrollable_lines): self.list_area.y_loc = y_loc self.create_list_item(next_part, self.right_win, self.list_area) next_part = part_iter.next() if len(data) > max_left_parts: self.right_win.use_vert_scroll_bar = True except StopIteration: if len(self.right_win.all_objects) <= self.right_win.area.lines: self.right_win.use_vert_scroll_bar = False self.right_win.no_ut_refresh() else: raise ValueError("Could not fit all partitions in DiskWindow") self.no_ut_refresh() def create_list_item(self, next_part, win, list_area): '''Add an entry for next_part (a Partition or Slice) to the DiskWindow ''' list_item = ListItem(list_area, window=win, data_obj=next_part) list_item.key_dict.update(DiskWindow.ADD_KEYS) edit_field = EditField(self.edit_area, window=list_item, numeric_pad=" ", validate=decimal_valid, on_exit=on_exit_edit, error_win=self.error_win, add_obj=False, data_obj=next_part) edit_field.right_justify = True edit_field.validate_kwargs["disk_win"] = self edit_field.on_exit_kwargs["disk_win"] = self edit_field.key_dict.update(DiskWindow.ADD_KEYS) self.update_part(part_field=list_item) return list_item def update_part(self, part_info=None, part_field=None): '''Sync changed partition data to the screen.''' if part_field is None: if part_info is None: raise ValueError("Must supply either part_info or part_field") part_field = self.find_part_field(part_info)[1] elif part_info is None: part_info = part_field.data_obj elif part_field.data_obj is not part_info: raise ValueError("part_field must be a ListItem associated with " "part_info") if not isinstance(part_field, ListItem): raise TypeError("part_field must be a ListItem associated with " "part_info") if self.has_partition_data: desc_text = part_info.get_description() else: desc_length = self.headers[0][0] - 1 desc_text = "%-*.*s %s" % (desc_length, desc_length, part_info.get_description(), part_info.name) part_field.set_text(desc_text) edit_field = part_field.all_objects[0] edit_field.set_text(locale.format("%.1f", part_info.size.get(Size.gb_units))) self.mark_if_destroyed(part_field) self._update_edit_field(part_info, part_field, edit_field) self.update_avail_space(part_info=part_info) if self.has_partition_data: if part_info.is_extended(): self.ext_part_field = part_field def _update_edit_field(self, part_info, part_field, edit_field): '''If the partition/slice is editable, add it to the .objects list. If it's also the part_field that's currently selected, then activate the edit field. ''' if part_info.editable(): part_field.objects = [edit_field] active_win = self.get_active_object() if active_win is not None: if active_win.get_active_object() is part_field: part_field.activate_object(edit_field) else: edit_field.make_inactive() part_field.objects = [] part_field.active_object = None def mark_if_destroyed(self, part_field): '''Determine if the partition/slice represented by part_field has changed such that its contents will be destroyed. ''' part_info = part_field.data_obj destroyed = part_info.modified() self.mark_destroyed(part_field, destroyed) def mark_destroyed(self, part_field, destroyed): '''If destroyed is True, add an asterisk indicating that the partition or slice's content will be destroyed during installation. Otherwise, clear the asterisk ''' y_loc = part_field.area.y_loc x_loc = part_field.area.x_loc - 1 if part_field in self.right_win.objects: win = self.right_win else: win = self.left_win if destroyed: win.window.addch(y_loc, x_loc, DiskWindow.DESTROYED_MARK, win.color_theme.inactive) else: win.window.addch(y_loc, x_loc, InnerWindow.BKGD_CHAR) def update_avail_space(self, part_number=None, part_info=None): '''Update the 'Avail' column for the specified slice or partition. If no number is given, all avail columns are updated ''' if part_number is None and part_info is None: self._update_all_avail_space() else: self._update_avail_space(part_number, part_info) def _update_all_avail_space(self): '''Update the 'Avail' column for all slices or partitions.''' idx = 0 for item in self.left_win.objects: self.update_avail_space(idx) idx += 1 for item in self.right_win.objects: self.update_avail_space(idx) idx += 1 y_loc = idx - len(self.left_win.objects) if self.has_partition_data: x_loc = self.headers[0][0] + self.headers[1][0] + 1 field = 2 else: x_loc = (self.headers[0][0] + self.headers[1][0] + self.headers[2][0] + 1) field = 3 if y_loc > 0: self.right_win.add_text(" " * self.headers[field][0], y_loc, x_loc) def _update_avail_space(self, part_number=None, part_info=None): '''Update the 'Avail' column for the specified slice or partition.''' if part_number is None: win, item = self.find_part_field(part_info) elif part_number < len(self.left_win.objects): win = self.left_win item = win.objects[part_number] else: win = self.right_win item = win.objects[part_number - len(self.left_win.objects)] if self.has_partition_data: x_loc = self.headers[0][0] + self.headers[1][0] + 1 field = 2 else: x_loc = (self.headers[0][0] + self.headers[1][0] + self.headers[2][0] + 1) field = 3 y_loc = item.area.y_loc part = item.data_obj max_space = part.get_max_size() max_space = locale.format("%*.1f", (self.headers[field][0], max_space.get(Size.gb_units))) win.add_text(max_space, y_loc, x_loc) def find_part_field(self, part_info): '''Given a PartitionInfo or SliceInfo object, find the associated ListItem. This search compares by reference, and will only succeed if you have a handle to the exact object referenced by the ListItem ''' for win in [self.left_win, self.right_win]: for item in win.objects: if item.data_obj is part_info: return win, item raise ValueError("Part field not found") def reset(self, dummy=None): '''Reset ui_obj to value found from Target Discovery. Meaningful only for editable DiskWindows ''' if not self.editable: return doc = InstallEngine.get_instance().doc # "reset" the desired target reset_obj = None if isinstance(self.ui_obj, UIDisk): reset_obj = (self.tc.reset_layout(disk=self.ui_obj.doc_obj))[0] else: # reset the partition by removing the modified Partition, and # resetting it with the partition found during target discovery. discovered_obj = self.ui_obj.discovered_doc_obj desired_disk = get_desired_target_disk(doc) desired_part = get_solaris_partition(doc) desired_disk.delete_partition(desired_part) part_copy = deepcopy(discovered_obj) desired_disk.insert_children(part_copy) # get the updated reference reset_obj = get_solaris_partition(doc) dump_doc("After doing reset") self.set_disk_info(disk_info=reset_obj) self.activate_solaris_data() def activate_solaris_data(self): '''Find the Solaris Partition / ZFS Root Pool Slice and activate it. ''' if self.editable: solaris_part = self.ui_obj.get_solaris_data() if solaris_part is None: LOGGER.debug("No Solaris data, activating default") self.activate_object() self.right_win.scroll(scroll_to_line=0) return disk_order = self.ui_obj.get_parts_in_use().index(solaris_part) LOGGER.debug("solaris disk at disk_order = %s", disk_order) self.activate_index(disk_order) def make_active(self): '''On activate, select the solaris partition or ZFS root pool, instead of defaulting to 0 ''' self.set_color(self.highlight_color) self.activate_solaris_data() def on_arrow_key(self, input_key): ''' On curses.KEY_LEFT: Move from the right win to the left win On curses.KEY_RIGHT: Move from the left to the right ''' if (input_key == curses.KEY_LEFT and self.get_active_object() is self.right_win and len(self.left_win.objects) > 0): active_object = self.right_win.get_active_object().area.y_loc if (active_object >= len(self.left_win.objects)): active_object = len(self.left_win.objects) - 1 self.activate_object(self.left_win) self.left_win.activate_object(active_object) return None elif (input_key == curses.KEY_RIGHT and self.get_active_object() is self.left_win and len(self.right_win.objects) > 0): active_line = (self.left_win.active_object + self.right_win.current_line[0]) active_object = None force_to_top = False for obj in self.right_win.objects: if obj.area.y_loc >= active_line: active_object = obj off_screen = (self.right_win.current_line[0] + self.right_win.area.lines) if active_object.area.y_loc > off_screen: force_to_top = True break if active_object is None: active_object = 0 self.left_win.activate_object(-1, loop=True) self.activate_object(self.right_win) self.right_win.activate_object_force(active_object, force_to_top=force_to_top) return None return input_key def no_ut_refresh(self, abs_y=None, abs_x=None): '''Refresh self, left win and right win explicitly''' super(DiskWindow, self).no_ut_refresh() self.left_win.no_ut_refresh(abs_y, abs_x) self.right_win.no_ut_refresh(abs_y, abs_x) def change_type(self, dummy): '''Cycle the type for the currently active object, and update its field ''' LOGGER.debug("changing type") part_field = self.get_active_object().get_active_object() part_info = part_field.data_obj part_order = self.ui_obj.get_parts_in_use().index(part_info) old_obj = part_info.discovered_doc_obj old_type = list() if old_obj is not None: if self.has_partition_data: old_type.append(old_obj.part_type) else: if old_obj.in_zpool is not None: old_type.append(old_obj.in_zpool) else: in_use = part_info.doc_obj.in_use if in_use is not None: if in_use['used_name']: old_type.append((in_use['used_name'])[0]) LOGGER.debug("extra type to cycle: %s", old_type) part_info.cycle_type(extra_type=old_type) self.set_disk_info(ui_obj=self.ui_obj, no_part_ok=True) self.activate_index(part_order) return None def create_extended(self, ext_part_field): '''If this is the original extended partition, restore the original logical partitions. Otherwise, create a single unused logical partition. ''' if not ext_part_field.data_obj.modified(): self.right_win.clear() self.orig_logicals_active = True logicals = deepcopy(self._orig_data.get_logicals()) self.disk_info.partitions.extend(logicals) for idx, logical in enumerate(logicals): self.list_area.y_loc = idx self.create_list_item(logical, self.right_win, self.list_area) if self.right_win not in self.objects: self.objects.append(self.right_win) self.right_win.activate_object_force(0, force_to_top=True) self.right_win.make_inactive() self.right_win.no_ut_refresh() else: # Leave old data be, create new Unused logical partition if self.right_win not in self.objects: self.objects.append(self.right_win) self.append_unused_logical() def activate_index(self, obj_index): '''Activate the object at the specified index ''' if obj_index < len(self.left_win.objects): LOGGER.debug("activating in left_win") self.left_win.activate_object(obj_index) self.activate_object(self.left_win) self.right_win.scroll(scroll_to_line=0) else: activate = obj_index - len(self.left_win.objects) LOGGER.debug('activating in right win') self.right_win.activate_object_force(activate, force_to_top=True) self.activate_object(self.right_win) left_active = self.left_win.get_active_object() if left_active is not None: left_active.make_inactive() def append_unused_logical(self): '''Adds a single Unused logical partition to the right window''' new_part = self.disk_info.append_unused_logical() self.list_area.y_loc = len(self.right_win.all_objects) bottom = self.list_area.y_loc - self.right_win.area.lines + 1 self.right_win.bottom = max(0, bottom) self.create_list_item(new_part, self.right_win, self.list_area) scroll = len(self.right_win.all_objects) > self.right_win.area.lines self.right_win.use_vert_scroll_bar = scroll self.right_win.no_ut_refresh()
class DiskScreen(BaseScreen): ''' Allow the user to select a (valid) disk target for installation Display the partition/slice table for the highlighted disk ''' HEADER_TEXT = _("Disks") PARAGRAPH = _("Where should %(release)s be installed?") % RELEASE REC_SIZE_TEXT = _("Recommended size: ") MIN_SIZE_TEXT = _(" Minimum size: ") DISK_SEEK_TEXT = _("Seeking disks on system") FOUND_x86 = _("The following partitions were found on the disk.") FOUND_SPARC = _("The following slices were found on the disk.") PROPOSED_x86 = _("A partition table was not found. The following is " "proposed.") PROPOSED_SPARC = _("A VTOC label was not found. The following " "is proposed.") PROPOSED_GPT = _("A GPT labeled disk was found. The following is " "proposed.") TOO_SMALL = _("Too small") TOO_BIG_WARN = _("Limited to %.1f TB") GPT_LABELED = _("GPT labeled disk") NO_DISKS = _("No disks found. Additional device drivers may " "be needed.") NO_TARGETS = _("%(release)s cannot be installed on any disk") % RELEASE TGT_ERROR = _("An error occurred while searching for installation" " targets. Please check the install log and file a bug" " at defect.opensolaris.org.") DISK_HEADERS = [(8, _("Type")), (10, _(" Size(GB)")), (6, _("Boot")), (26, _("Device")), (10, _("Vendor")), (3, ""), (10, _("Notes"))] #blank header for the notes column VENDOR_LEN = 15 SPINNER = ["\\", "|", "/", "-"] DISK_WARNING_HEADER = _("Warning") DISK_WARNING_TOOBIG = _("Only the first %.1fTB can be used.") DISK_WARNING_GPT = _("You have chosen a GPT labeled disk. Installing " "onto a GPT labeled disk will cause the loss " "of all existing data and the disk will be " "relabeled as SMI.") CANCEL_BUTTON = _("Cancel") CONTINUE_BUTTON = _("Continue") HELP_DATA = (TUI_HELP + "/%s/disks.txt", _("Disks")) def __init__(self, main_win, target_controller): global LOGGER LOGGER = logging.getLogger(INSTALL_LOGGER_NAME) super(DiskScreen, self).__init__(main_win) if platform.processor() == "i386": self.found_text = DiskScreen.FOUND_x86 self.proposed_text = DiskScreen.PROPOSED_x86 else: self.found_text = DiskScreen.FOUND_SPARC self.proposed_text = DiskScreen.PROPOSED_SPARC disk_header_text = [] for header in DiskScreen.DISK_HEADERS: header_str = fit_text_truncate(header[1], header[0] - 1, just="left") disk_header_text.append(header_str) self.disk_header_text = " ".join(disk_header_text) max_note_size = DiskScreen.DISK_HEADERS[5][0] self.too_small_text = DiskScreen.TOO_SMALL[:max_note_size] max_disk_size = (Size(MAX_VTOC)).get(Size.tb_units) too_big_warn = DiskScreen.TOO_BIG_WARN % max_disk_size self.too_big_warn = too_big_warn[:max_note_size] self.disk_warning_too_big = \ DiskScreen.DISK_WARNING_TOOBIG % max_disk_size self.disks = [] self.existing_pools = [] self.disk_win = None self.disk_detail = None self.num_targets = 0 self.td_handle = None self._size_line = None self.selected_disk_index = 0 self._minimum_size = None self._recommended_size = None self.engine = InstallEngine.get_instance() self.doc = self.engine.data_object_cache self.tc = target_controller self._target_discovery_completed = False self._target_discovery_status = InstallEngine.EXEC_SUCCESS self._image_size = None def determine_minimum(self): '''Returns minimum install size, fetching first if needed''' self.determine_size_data() return self._minimum_size minimum_size = property(determine_minimum) def determine_recommended(self): '''Returns recommended install size, fetching first if needed''' self.determine_size_data() return self._recommended_size recommended_size = property(determine_recommended) def determine_size_data(self): '''Retrieve the minimum and recommended sizes and generate the string to present that information. ''' if self._minimum_size is None or self._recommended_size is None: self._recommended_size = get_recommended_size(self.tc) self._minimum_size = get_minimum_size(self.tc) def get_size_line(self): '''Returns the line of text displaying the min/recommended sizes''' if self._size_line is None: rec_size_str = locale.format( "%.1f", self.recommended_size.get( Size.gb_units)) + LOCALIZED_GB min_size_str = locale.format( "%.1f", self.minimum_size.get(Size.gb_units)) + LOCALIZED_GB self._size_line = DiskScreen.REC_SIZE_TEXT + rec_size_str + \ DiskScreen.MIN_SIZE_TEXT + min_size_str return self._size_line size_line = property(get_size_line) def wait_for_disks(self): '''Block while waiting for libtd to finish. Catch F9 and quit if needed ''' self.main_win.actions.pop(curses.KEY_F2, None) self.main_win.actions.pop(curses.KEY_F6, None) self.main_win.actions.pop(curses.KEY_F3, None) self.main_win.show_actions() self.center_win.add_text(DiskScreen.DISK_SEEK_TEXT, 5, 1, self.win_size_x - 3) self.main_win.do_update() offset = textwidth(DiskScreen.DISK_SEEK_TEXT) + 2 spin_index = 0 self.center_win.window.timeout(250) while not self._target_discovery_completed: input_key = self.main_win.getch() if input_key == curses.KEY_F9: if self.confirm_quit(): raise QuitException self.center_win.add_text(DiskScreen.SPINNER[spin_index], 5, offset) self.center_win.no_ut_refresh() self.main_win.do_update() spin_index = (spin_index + 1) % len(DiskScreen.SPINNER) self.center_win.window.timeout(-1) self.center_win.clear() # check the result of target discovery if self._target_discovery_status is not InstallEngine.EXEC_SUCCESS: err_data = (errsvc.get_errors_by_mod_id(TARGET_DISCOVERY))[0] LOGGER.error("Target discovery failed") err = err_data.error_data[liberrsvc.ES_DATA_EXCEPTION] LOGGER.error(err) raise TargetDiscoveryError( ("Unexpected error (%s) during target " "discovery. See log for details.") % err) def _td_callback(self, status, errsvc): '''Callback function for Target Discovery checkpoint execution. The status value is saved to be interpreted later. This function sets the self._target_discovery_completed value to true so the wait_for_disks() function will know to stop displaying the spinner. ''' self._target_discovery_status = status self._target_discovery_completed = True def _show(self): '''Create a list of disks to choose from and create the window for displaying the partition/slice information from the selected disk ''' self.wait_for_disks() discovered_target = self.doc.persistent.get_first_child( \ name=Target.DISCOVERED) LOGGER.debug(discovered_target) if discovered_target is None: self.center_win.add_paragraph(DiskScreen.NO_DISKS, 1, 1, max_x=(self.win_size_x - 1)) return self.disks = discovered_target.get_children(class_type=Disk) if not self.disks: self.center_win.add_paragraph(DiskScreen.NO_TARGETS, 1, 1, max_x=(self.win_size_x - 1)) return if self._image_size is None: try: self._image_size = Size(str(get_image_size(LOGGER)) + \ Size.mb_units) LOGGER.debug("Image_size: %s", self._image_size) except: # Unable to get the image size for some reason, allow # the target controller to use it's default size. LOGGER.debug("Unable to get image size") self._image_size = FALLBACK_IMAGE_SIZE # initialize the target controller so the min/max size for # the installation can be calculated. Explicitly do not # want to select an initial disk at this time in case # none of the disks discovered is usable. The target controller # initialization needs to be done everytime we show the disk selection # screen so the desired target node in the DOC can be re-populated # with information from target discovery. self.tc.initialize(image_size=self._image_size, no_initial_disk=True) # Go through all the disks found and find ones that have # enough space for installation. At the same time, see if any # existing disk is the boot disk. If a boot disk is found, move # it to the front of the list num_usable_disks = 0 boot_disk = None for disk in self.disks: LOGGER.debug("size: %s, min: %s" % \ (disk.disk_prop.dev_size, self.minimum_size)) if disk.disk_prop.dev_size >= self.minimum_size: if disk.is_boot_disk(): boot_disk = disk num_usable_disks += 1 if boot_disk is not None: self.disks.remove(boot_disk) self.disks.insert(0, boot_disk) if num_usable_disks == 0: self.center_win.add_paragraph(DiskScreen.NO_DISKS, 1, 1, max_x=(self.win_size_x - 1)) return self.main_win.reset_actions() self.main_win.show_actions() y_loc = 1 self.center_win.add_text(DiskScreen.PARAGRAPH, y_loc, 1) y_loc += 1 self.center_win.add_text(self.size_line, y_loc, 1) y_loc += 2 self.center_win.add_text(self.disk_header_text, y_loc, 1) y_loc += 1 self.center_win.window.hline(y_loc, self.center_win.border_size[1] + 1, curses.ACS_HLINE, textwidth(self.disk_header_text)) y_loc += 1 disk_win_area = WindowArea(4, textwidth(self.disk_header_text) + 2, y_loc, 0) disk_win_area.scrollable_lines = len(self.disks) + 1 self.disk_win = ScrollWindow(disk_win_area, window=self.center_win) disk_item_area = WindowArea(1, disk_win_area.columns - 2, 0, 1) disk_index = 0 len_type = DiskScreen.DISK_HEADERS[0][0] - 1 len_size = DiskScreen.DISK_HEADERS[1][0] - 1 len_boot = DiskScreen.DISK_HEADERS[2][0] - 1 len_dev = DiskScreen.DISK_HEADERS[3][0] - 1 len_mftr = DiskScreen.DISK_HEADERS[4][0] - 1 for disk in self.disks: disk_text_fields = [] if disk.disk_prop is None or disk.disk_prop.dev_type is None: continue type_field = disk.disk_prop.dev_type[:len_type] type_field = ljust_columns(type_field, len_type) disk_text_fields.append(type_field) disk_size = disk.disk_prop.dev_size.get(Size.gb_units) size_field = locale.format("%*.1f", (len_size, disk_size)) disk_text_fields.append(size_field) if disk.is_boot_disk(): bootable_field = "+".center(len_boot) else: bootable_field = " " * (len_boot) disk_text_fields.append(bootable_field) device_field = disk.ctd[:len_dev] device_field = ljust_columns(device_field, len_dev) disk_text_fields.append(device_field) vendor = disk.disk_prop.dev_vendor if vendor is not None: mftr_field = vendor[:len_mftr] mftr_field = ljust_columns(mftr_field, len_mftr) else: mftr_field = " " * len_mftr disk_text_fields.append(mftr_field) selectable = True if disk.disk_prop.dev_size < self.minimum_size: note_field = self.too_small_text selectable = False elif disk_size > Size(MAX_VTOC).get(Size.gb_units): note_field = self.too_big_warn else: note_field = "" disk_text_fields.append(note_field) disk_text = " ".join(disk_text_fields) disk_item_area.y_loc = disk_index disk_list_item = ListItem(disk_item_area, window=self.disk_win, text=disk_text, add_obj=selectable) disk_list_item.on_make_active = on_activate disk_list_item.on_make_active_kwargs["disk"] = disk disk_list_item.on_make_active_kwargs["disk_select"] = self disk_index += 1 self.disk_win.no_ut_refresh() y_loc += 7 disk_detail_area = WindowArea(6, 70, y_loc, 1) self.disk_detail = DiskWindow(disk_detail_area, self.disks[0], target_controller=self.tc, window=self.center_win) self.main_win.do_update() self.center_win.activate_object(self.disk_win) self.disk_win.activate_object(self.selected_disk_index) def on_change_screen(self): ''' Save the index of the current selected object in case the user returns to this screen later ''' # Save the index of the selected object self.selected_disk_index = self.disk_win.active_object LOGGER.debug("disk_selection.on_change_screen, saved_index: %s", self.selected_disk_index) LOGGER.debug(self.doc.persistent) def start_discovery(self): # start target discovery if not self._target_discovery_completed: errsvc.clear_error_list() self.engine.execute_checkpoints(pause_before=TRANSFER_PREP, callback=self._td_callback) def validate(self): '''Validate the size of the disk.''' warning_txt = [] disk = self.disk_detail.ui_obj.doc_obj disk_size_gb = disk.disk_prop.dev_size.get(Size.gb_units) max_size_gb = Size(MAX_VTOC).get(Size.gb_units) if disk_size_gb > max_size_gb: warning_txt.append(self.disk_warning_too_big) warning_txt = " ".join(warning_txt) if warning_txt: # warn the user and give user a chance to change result = self.main_win.pop_up(DiskScreen.DISK_WARNING_HEADER, warning_txt, DiskScreen.CANCEL_BUTTON, DiskScreen.CONTINUE_BUTTON) if not result: raise UIMessage() # let user select different disk
class MainWindow(object): '''Represent initscr (the whole screen), and break it into a border, header, and central region. Map F# keystrokes to Actions ''' def __init__(self, initscr, screen_list, default_actions, theme=None, force_bw=False): '''Set the theme, and call reset to initialize the terminal to prepare for the first screen. ''' if theme is not None: self.theme = theme else: self.theme = ColorTheme(force_bw=force_bw) self.screen_list = screen_list self.initscr = initscr self.default_cursor_pos = (initscr.getmaxyx()[0] - 1, 0) self.cursor_pos = self.default_cursor_pos self.footer = None self.header = None self._cur_header_text = None self.central_area = None self.popup_win = None self.error_line = None self._active_win = None self.actions = None # _default_actions keeps a "pristine" copy of the actions self._default_actions = default_actions # default_actions is copied from _default_actions and may # get modified during the course of display of a screen. # reset_actions() is responsible for copying the pristine copy # into this variable. self.default_actions = None self.reset() def redrawwin(self): '''Completely repaint the screen''' self.header.redrawwin() self.footer.redrawwin() self.error_line.redrawwin() self.central_area.redrawwin() if self._active_win is self.popup_win: self.popup_win.redrawwin() def do_update(self): '''Wrapper to curses.doupdate()''' curses.setsyx(*self.get_cursor_loc()) curses.doupdate() def get_cursor_loc(self): '''Retrieve the current cursor position from the active UI element. ''' cursor = self.central_area.get_cursor_loc() if cursor is None: cursor = self.cursor_pos return cursor def reset(self): '''Create the InnerWindows representing the header, footer/border, error line, and main central_area ''' window_size = self.initscr.getmaxyx() win_size_y = window_size[0] win_size_x = window_size[1] footer_area = WindowArea(1, win_size_x, win_size_y - 1, 0) self.footer = InnerWindow(footer_area, color_theme=self.theme, color=self.theme.border) top = self.initscr.derwin(1, win_size_x, 0, 0) left = self.initscr.derwin(win_size_y - 2, 1, 1, 0) right = self.initscr.derwin(win_size_y - 2, 1, 1, win_size_x - 1) self.footer.more_windows = [top, left, right] self.footer.set_color(self.theme.border) header_area = WindowArea(1, win_size_x - 2, 1, 1) self.header = InnerWindow(header_area, color_theme=self.theme, color=self.theme.header) central_win_area = WindowArea(win_size_y - 4, win_size_x - 2, 2, 1) self.central_area = InnerWindow(central_win_area, border_size=(0, 2), color_theme=self.theme) self._active_win = self.central_area popup_win_area = WindowArea(central_win_area.lines - 10, central_win_area.columns - 20, 5, 10, central_win_area.lines - 10) self.popup_win = ScrollWindow(popup_win_area, window=self.central_area, color=self.theme.error_msg, highlight_color=self.theme.error_msg) error_area = WindowArea(1, win_size_x - 2, win_size_y - 2, 1) self.error_line = ErrorWindow(error_area, color_theme=self.theme) self.reset_actions() def reset_actions(self): '''Reset the actions to the defaults, clearing any custom actions registered by individual screens ''' # A shallow copy of each Action is desired to properly preserve # the Action's reference to a given bound method. actions = [copy.copy(action) for action in self._default_actions] self.default_actions = actions self.set_default_actions() @property def continue_action(self): return self.actions[curses.KEY_F2] @property def back_action(self): return self.actions[curses.KEY_F3] @property def help_action(self): return self.actions[curses.KEY_F6] @property def quit_action(self): return self.actions[curses.KEY_F9] def clear(self): '''Clear all InnerWindows and reset_actions()''' self.header.clear() self.footer.clear() self.central_area.clear() self.error_line.clear_err() self.reset_actions() def set_header_text(self, header_text): '''Set the header_text''' text = center_columns(header_text, self.header.area.columns - 1) self.header.add_text(text) self._cur_header_text = text def set_default_actions(self): '''Clear the actions dictionary and add the default actions back into it ''' self.actions = {} for action in self.default_actions: self.actions[action.key] = action def show_actions(self): '''Read through the actions dictionary, displaying all the actions descriptive text along the footer (along with a prefix linked to its associated keystroke) ''' self.footer.window.clear() if InnerWindow.USE_ESC: prefix = " Esc-" else: prefix = " F" strings = [] length = 0 action_format = "%s%i_%s" for key in sorted(self.actions.keys()): key_num = key - curses.KEY_F0 action_text = self.actions[key].text action_str = action_format % (prefix, key_num, action_text) strings.append(action_str) display_str = "".join(strings) max_len = self.footer.window.getmaxyx()[1] length = textwidth(display_str) if not InnerWindow.USE_ESC: length += (len(" Esc-") - len(" F")) * len(self.actions) if length > max_len: raise ValueError("Can't display footer actions - string too long") self.footer.window.addstr(display_str.encode(get_encoding())) self.footer.window.noutrefresh() def getch(self, redraw_keys=[InnerWindow.REPAINT_KEY]): '''Call down into central_area to get a keystroke, and, if necessary, update the footer to switch to using the Esc- prefixes. Redraw the screen if any of redraw keys is pressed. ''' input_key = self._active_win.getch() # Redraw whole screen if one of 'redraw' keys has been pressed. if input_key in redraw_keys: self.redrawwin() input_key = None if InnerWindow.UPDATE_FOOTER: InnerWindow.UPDATE_FOOTER = False self.show_actions() return input_key def process_input(self, current_screen): '''Read input until a keystroke that fires a screen change is caught ''' input_key = None while input_key not in self.actions: input_key = self.getch(current_screen.redraw_keys) input_key = self.central_area.process(input_key) self.do_update() return self.actions[input_key].do_action(current_screen) def pop_up(self, header, question, left_btn_txt, right_btn_txt, color=None): '''Suspend the current screen, setting the header to 'header', presenting the 'question,' and providing two 'buttons'. Returns True if the RIGHT button is selected, False if the LEFT is selected. The LEFT button is initially selected. ''' # Hide the cursor, storing its previous state (visibility) so # it can be restored when finished. Then, move the cursor # to the default position (in case this terminal type does not support # hiding the cursor entirely) try: old_cursor_state = curses.curs_set(0) except curses.error: old_cursor_state = 2 cursor_loc = curses.getsyx() curses.setsyx(self.cursor_pos[0], self.cursor_pos[1]) # Add the header, a border, and the question to the window self.popup_win.window.border() header_x = (self.popup_win.area.columns - textwidth(header)) / 2 self.popup_win.add_text(header, 0, header_x) y_loc = 2 y_loc += self.popup_win.add_paragraph(question, y_loc, 2) y_loc += 2 # Set the background color based on the parameter given, or choose # a default based on the theme. Set the highlight_color by flipping # the A_REVERSE bit of the color if color is None: color = self.popup_win.color self.popup_win.set_color(color) highlight_color = color ^ curses.A_REVERSE # Create two "buttons" of equal size by finding the larger of the # two, and centering them max_len = max(textwidth(left_btn_txt), textwidth(right_btn_txt)) left_btn_txt = " [ %s ]" % left_btn_txt.center(max_len) right_btn_txt = " [ %s ]" % right_btn_txt.center(max_len) button_len = textwidth(left_btn_txt) + 1 win_size = self.popup_win.window.getmaxyx() left_area = WindowArea(1, button_len, y_loc, (win_size[1] / 2) - (button_len + 2)) left_button = ListItem(left_area, window=self.popup_win, text=left_btn_txt, color=color, highlight_color=highlight_color) right_area = WindowArea(1, button_len, y_loc, win_size[1] / 2 + 2) right_button = ListItem(right_area, window=self.popup_win, text=right_btn_txt, color=color, highlight_color=highlight_color) # Highlight the left button, clear any errors on the screen, # and display the pop up self.popup_win.activate_object(left_button) self.popup_win.no_ut_refresh() self.error_line.clear_err() self.do_update() self._active_win = self.popup_win # Loop until the user selects an option. input_key = None while input_key != curses.KEY_ENTER: input_key = self.getch() input_key = self.popup_win.process(input_key) if input_key == curses.KEY_LEFT: self.popup_win.activate_object(left_button) elif input_key == curses.KEY_RIGHT: self.popup_win.activate_object(right_button) self.do_update() self._active_win = self.central_area user_selected = (self.popup_win.get_active_object() is right_button) # Clear the pop up and restore the previous screen, including the # cursor position and visibility self.popup_win.clear() self.central_area.redrawwin() curses.setsyx(cursor_loc[0], cursor_loc[1]) try: curses.curs_set(old_cursor_state) except curses.error: pass self.do_update() return user_selected
class DiskScreen(BaseScreen): ''' Allow the user to select a (valid) disk target for installation Display the partition/slice table for the highlighted disk ''' HEADER_TEXT = _("Disks") PARAGRAPH = _("Where should %(release)s be installed?") % RELEASE REC_SIZE_TEXT = _("Recommended size: ") MIN_SIZE_TEXT = _(" Minimum size: ") DISK_SEEK_TEXT = _("Seeking disks on system") FOUND_x86 = _("The following partitions were found on the disk.") FOUND_SPARC = _("The following slices were found on the disk.") FOUND_GPT = _("The following GPT partitions were found on the disk.") PROPOSED_x86 = _("A partition table was not found. The following is " "proposed.") PROPOSED_SPARC = _("A VTOC label was not found. The following " "is proposed.") PROPOSED_GPT = _("A GPT labeled disk was not found. The following is " "proposed.") TOO_SMALL = "<" TOO_BIG = ">" INVALID_DISK = "!" GPT_LABELED = _("GPT labeled disk") NO_DISKS = _("No disks found. Additional device drivers may " "be needed.") NO_TARGETS = _("%(release)s cannot be installed on any disk") % RELEASE DISK_HEADERS = [(8, _("Type")), (10, _(" Size(GB)")), (6, _("Boot")), (44, _("Device")), (3, "")] # blank header for the notes column VENDOR_LEN = 15 SPINNER = ["\\", "|", "/", "-"] DISK_WARNING_HEADER = _("Warning") DISK_WARNING_TOOBIG = _("Only the first %.1fTB can be used.") DISK_WARNING_RELABEL = _("You have chosen a GPT labeled disk. Installing " "onto this disk requires it to be relabeled as " "SMI. This causes IMMEDIATE LOSS of all data " "on the disk. Select Continue only if you are " "prepared to erase all data on this disk now.") CANCEL_BUTTON = _("Cancel") CONTINUE_BUTTON = _("Continue") HELP_DATA = (TUI_HELP + "/%s/disks.txt", _("Disks")) def __init__(self, main_win, target_controller): """ screen object containing the disk selection choice for the user """ global LOGGER LOGGER = logging.getLogger(INSTALL_LOGGER_NAME) super(DiskScreen, self).__init__(main_win) if platform.processor() == "i386": self.found_text = DiskScreen.FOUND_x86 self.proposed_text = DiskScreen.PROPOSED_x86 else: self.found_text = DiskScreen.FOUND_SPARC self.proposed_text = DiskScreen.PROPOSED_SPARC self.gpt_found_text = DiskScreen.FOUND_GPT self.gpt_proposed_text = DiskScreen.PROPOSED_GPT disk_header_text = [] for header in DiskScreen.DISK_HEADERS: header_str = fit_text_truncate(header[1], header[0] - 1, just="left") disk_header_text.append(header_str) self.disk_header_text = " ".join(disk_header_text) self.max_vtoc_disk_size = (Size(MAX_VTOC)).get(Size.tb_units) self.disk_warning_too_big = \ DiskScreen.DISK_WARNING_TOOBIG % self.max_vtoc_disk_size self.disks = [] self.existing_pools = [] self.disk_win = None self.disk_detail = None self.num_targets = 0 self.td_handle = None self._size_line = None self.selected_disk_index = 0 self._minimum_size = None self._recommended_size = None self.engine = InstallEngine.get_instance() self.doc = self.engine.data_object_cache self.tc = target_controller self._target_discovery_completed = False self._target_discovery_status = InstallEngine.EXEC_SUCCESS self._image_size = None self.iscsi = None self._iscsi_target_discovery_completed = False self._iscsi_target_discovery_status = InstallEngine.EXEC_SUCCESS def determine_minimum(self): '''Returns minimum install size, fetching first if needed''' self.determine_size_data() return self._minimum_size minimum_size = property(determine_minimum) def determine_recommended(self): '''Returns recommended install size, fetching first if needed''' self.determine_size_data() return self._recommended_size recommended_size = property(determine_recommended) def determine_size_data(self): '''Retrieve the minimum and recommended sizes and generate the string to present that information. ''' if self._minimum_size is None or self._recommended_size is None: self._recommended_size = get_recommended_size(self.tc) self._minimum_size = get_minimum_size(self.tc) def get_size_line(self): '''Returns the line of text displaying the min/recommended sizes''' if self._size_line is None: rec_size_str = locale.format( "%.1f", self.recommended_size.get( Size.gb_units)) + LOCALIZED_GB min_size_str = locale.format( "%.1f", self.minimum_size.get(Size.gb_units)) + LOCALIZED_GB self._size_line = DiskScreen.REC_SIZE_TEXT + rec_size_str + \ DiskScreen.MIN_SIZE_TEXT + min_size_str return self._size_line size_line = property(get_size_line) def wait_for_disks(self): '''Block while waiting for target discovery to finish. Catch F9 and quit if needed ''' self.main_win.actions.pop(curses.KEY_F2, None) self.main_win.actions.pop(curses.KEY_F6, None) self.main_win.actions.pop(curses.KEY_F3, None) self.main_win.show_actions() self.center_win.add_text(DiskScreen.DISK_SEEK_TEXT, 5, 1, self.win_size_x - 3) self.main_win.do_update() offset = textwidth(DiskScreen.DISK_SEEK_TEXT) + 2 spin_index = 0 self.center_win.window.timeout(250) while not self._target_discovery_completed: input_key = self.main_win.getch() if input_key == curses.KEY_F9: if self.confirm_quit(): raise QuitException self.center_win.add_text(DiskScreen.SPINNER[spin_index], 5, offset) self.center_win.no_ut_refresh() self.main_win.do_update() spin_index = (spin_index + 1) % len(DiskScreen.SPINNER) self.center_win.window.timeout(-1) self.center_win.clear() # check the result of target discovery if self._target_discovery_status is not InstallEngine.EXEC_SUCCESS: err_data = errsvc.get_errors_by_mod_id(TARGET_DISCOVERY)[0] LOGGER.error("Target discovery failed") err = err_data.error_data[liberrsvc.ES_DATA_EXCEPTION] LOGGER.error(err) raise TargetDiscoveryError( ("Unexpected error (%s) during target " "discovery. See log for details.") % err) def wait_for_iscsi_disk(self): ''' Block while waiting for iSCSI discovery to finish ''' # check for the existence of an Iscsi object in the DOC. That object # is only added when the user selects 'iSCSI' as the discovery criteria self.iscsi = self.doc.volatile.get_first_child(name=ISCSI_LABEL, class_type=Iscsi) if not self.iscsi: return self.main_win.actions.pop(curses.KEY_F2, None) self.main_win.actions.pop(curses.KEY_F6, None) self.main_win.actions.pop(curses.KEY_F3, None) self.main_win.show_actions() self.center_win.add_text(DiskScreen.DISK_SEEK_TEXT, 5, 1, self.win_size_x - 3) self.main_win.do_update() offset = textwidth(DiskScreen.DISK_SEEK_TEXT) + 2 spin_index = 0 self.center_win.window.timeout(250) # there's an iscsi object in the DOC. Check for an existing iSCSI # target discovery checkpoint. If found, update the kwargs for # discovery to find all the information about the newest mapped LUN. # If not, register a new checkpoint. kwargs = {"search_type": "disk", "search_name": self.iscsi.ctd_list} for checkpoint in self.engine._checkpoints: if checkpoint.name == ISCSI_TARGET_DISCOVERY: checkpoint.kwargs = kwargs break else: # register a new iSCSI target discovery checkpoint self.engine.register_checkpoint(ISCSI_TARGET_DISCOVERY, "solaris_install/target/discovery", "TargetDiscovery", kwargs=kwargs, insert_before=TRANSFER_PREP) # run target discovery again against the iSCSI LUN self._iscsi_target_discovery_completed = False errsvc.clear_error_list() self.engine.execute_checkpoints(start_from=ISCSI_TARGET_DISCOVERY, pause_before=TRANSFER_PREP, callback=self._iscsi_td_callback) while not self._iscsi_target_discovery_completed: input_key = self.main_win.getch() if input_key == curses.KEY_F9: if self.confirm_quit(): raise QuitException self.center_win.add_text(DiskScreen.SPINNER[spin_index], 5, offset) self.center_win.no_ut_refresh() self.main_win.do_update() spin_index = (spin_index + 1) % len(DiskScreen.SPINNER) self.center_win.window.timeout(-1) self.center_win.clear() # check the result of target discovery if self._iscsi_target_discovery_status is not \ InstallEngine.EXEC_SUCCESS: err_data = errsvc.get_errors_by_mod_id(ISCSI_TARGET_DISCOVERY)[0] LOGGER.error("iSCSI target discovery failed") err = err_data.error_data[liberrsvc.ES_DATA_EXCEPTION] LOGGER.error(err) raise TargetDiscoveryError( "Unexpected error (%s) during iSCSI " "target discovery. See log for details." % err) def _td_callback(self, status, errsvc): '''Callback function for Target Discovery checkpoint execution. The status value is saved to be interpreted later. This function sets the self._target_discovery_completed value to true so the wait_for_disks() function will know to stop displaying the spinner. ''' self._target_discovery_status = status self._target_discovery_completed = True def _iscsi_td_callback(self, status, errsvc): '''Callback function for iSCSI Target Discovery checkpoint execution. The status value is saved to be interpreted later. This function sets the self._iscsi_target_discovery_completed value to true so the wait_for_iscsi_disk() function will know to stop displaying the spinner. ''' self._iscsi_target_discovery_status = status self._iscsi_target_discovery_completed = True def _show(self): '''Create a list of disks to choose from and create the window for displaying the partition/slice information from the selected disk ''' self.wait_for_disks() self.wait_for_iscsi_disk() discovered_target = self.doc.persistent.get_first_child( \ name=Target.DISCOVERED) LOGGER.debug(discovered_target) if discovered_target is None: self.center_win.add_paragraph(DiskScreen.NO_DISKS, 1, 1, max_x=(self.win_size_x - 1)) return self.disks = discovered_target.get_children(class_type=Disk) if not self.disks: self.center_win.add_paragraph(DiskScreen.NO_TARGETS, 1, 1, max_x=(self.win_size_x - 1)) return if self._image_size is None: try: self._image_size = Size(str(get_image_size(LOGGER)) + \ Size.mb_units) LOGGER.debug("Image_size: %s", self._image_size) except: # Unable to get the image size for some reason, allow # the target controller to use it's default size. LOGGER.debug("Unable to get image size") self._image_size = FALLBACK_IMAGE_SIZE # initialize the target controller so the min/max size for the # installation can be calculated. Explicitly do not want to select an # initial disk at this time in case none of the disks discovered is # usable. The target controller initialization needs to be done # everytime we show the disk selection screen so the desired target # node in the DOC can be re-populated with information from target # discovery. self.tc.initialize(image_size=self._image_size, no_initial_disk=True) # Go through all the disks found and find ones that have enough space # for installation. At the same time, see if any existing disk is the # boot disk. If a boot disk is found, move it to the front of the list num_usable_disks = 0 boot_disk = None for disk in self.disks: LOGGER.debug("size: %s, min: %s" % \ (disk.disk_prop.dev_size, self.minimum_size)) if disk.disk_prop.dev_size >= self.minimum_size: if disk.is_boot_disk(): boot_disk = disk num_usable_disks += 1 if boot_disk is not None: self.disks.remove(boot_disk) self.disks.insert(0, boot_disk) if num_usable_disks == 0: self.center_win.add_paragraph(DiskScreen.NO_DISKS, 1, 1, max_x=(self.win_size_x - 1)) return self.main_win.reset_actions() self.main_win.show_actions() y_loc = 1 self.center_win.add_text(DiskScreen.PARAGRAPH, y_loc, 1) y_loc += 1 self.center_win.add_text(self.size_line, y_loc, 1) y_loc += 2 self.center_win.add_text(self.disk_header_text, y_loc, 1) y_loc += 1 self.center_win.window.hline(y_loc, self.center_win.border_size[1] + 1, curses.ACS_HLINE, textwidth(self.disk_header_text)) y_loc += 1 disk_win_area = WindowArea(4, textwidth(self.disk_header_text) + 2, y_loc, 0) disk_win_area.scrollable_lines = len(self.disks) + 1 self.disk_win = ScrollWindow(disk_win_area, window=self.center_win) disk_item_area = WindowArea(1, disk_win_area.columns - 2, 0, 1) disk_index = 0 len_type = DiskScreen.DISK_HEADERS[0][0] - 1 len_size = DiskScreen.DISK_HEADERS[1][0] - 1 len_boot = DiskScreen.DISK_HEADERS[2][0] - 1 len_dev = DiskScreen.DISK_HEADERS[3][0] - 1 len_notes = DiskScreen.DISK_HEADERS[4][0] - 1 for disk in self.disks: disk_text_fields = [] dev_type = disk.disk_prop.dev_type if dev_type is not None: type_field = dev_type[:len_type] type_field = ljust_columns(type_field, len_type) else: type_field = " " * len_type disk_text_fields.append(type_field) disk_size = disk.disk_prop.dev_size.get(Size.gb_units) size_field = locale.format("%*.1f", (len_size, disk_size)) disk_text_fields.append(size_field) if disk.is_boot_disk(): bootable_field = "+".center(len_boot) else: bootable_field = " " * (len_boot) disk_text_fields.append(bootable_field) # # Information will be displayed in the device column with # the following priority: # # First priority is to display receptacle information, # if available. If receptacle information is displayed, # ctd name will not be displayed. # # If receptacle information is not available, the ctd name # will be displayed. # # Both items above can take as much as the 44 character wide # column as needed. # # If the receptacle/ctd name is less than 30 characters, # manufacturer information will be displayed in the left # over space. There won't be a column heading for the # manufacturer information. # device = disk.receptacle or disk.ctd added_device_field = False # is there enough room to display the manufacturer? if (len_dev - len(device)) >= DiskScreen.VENDOR_LEN: vendor = disk.disk_prop.dev_vendor if vendor is not None: dev_display_len = len_dev - DiskScreen.VENDOR_LEN device_field = ljust_columns(device, dev_display_len) disk_text_fields.append(device_field) vendor_field = vendor[:DiskScreen.VENDOR_LEN - 1] vendor_field = ljust_columns(vendor_field, DiskScreen.VENDOR_LEN - 1) disk_text_fields.append(vendor_field) added_device_field = True if not added_device_field: device_field = device[:len_dev] device_field = ljust_columns(device_field, len_dev) disk_text_fields.append(device_field) # display "<" or ">" if the disk is too big or too small selectable = True if disk.disk_prop.dev_size < self.minimum_size: selectable = False notes_field = DiskScreen.TOO_SMALL.center(len_notes) disk_text_fields.append(notes_field) elif disk.disk_prop.dev_size > Size(MAX_VTOC): notes_field = DiskScreen.TOO_BIG.center(len_notes) disk_text_fields.append(notes_field) # check the blocksize of the disk. If it's not 512 bytes and we # have an EFI firmware on x86, make the disk unselectable by the # user. See PSARC 2008/769 elif platform.processor() == "i386" and \ disk.geometry.blocksize != 512: firmware = SystemFirmware.get() if firmware.fw_name == "uefi64": selectable = False notes_field = DiskScreen.INVALID_DISK.center(len_notes) disk_text_fields.append(notes_field) LOGGER.debug( "marking disk %s unselectable as its " "blocksize is not 512 bytes on an UEFI " "firmware x86 system.", disk.ctd) disk_text = " ".join(disk_text_fields) disk_item_area.y_loc = disk_index disk_list_item = ListItem(disk_item_area, window=self.disk_win, text=disk_text, add_obj=selectable) disk_list_item.on_make_active = on_activate disk_list_item.on_make_active_kwargs["disk"] = disk disk_list_item.on_make_active_kwargs["disk_select"] = self disk_index += 1 self.disk_win.no_ut_refresh() y_loc += 7 disk_detail_area = WindowArea(6, 70, y_loc, 1) self.disk_detail = DiskWindow(disk_detail_area, self.disks[0], target_controller=self.tc, window=self.center_win) self.main_win.do_update() self.center_win.activate_object(self.disk_win) self.disk_win.activate_object(self.selected_disk_index, jump=True) def on_prev(self): ''' If the user goes back a screen, teardown any existing Iscsi objects and reset iSCSI target discovery ''' if self.iscsi is not None: # remove the iscsi Disk objects from the doc and from self.disks if self.iscsi.ctd_list: discovered_target = self.doc.persistent.get_first_child( name=Target.DISCOVERED) for disk in discovered_target.get_descendants(class_type=Disk): if disk.ctd in self.iscsi.ctd_list: disk.delete() self.disks = discovered_target.get_children(class_type=Disk) # tear down the Iscsi object self.iscsi.teardown() # reset the selected disk index to 0 self.selected_disk_index = 0 # reset the iscsi_discovery status self._iscsi_target_discovery_completed = False # reset the target controller's discovered_disk self.tc._discovered_disks = None def on_change_screen(self): ''' Save the index of the current selected object in case the user returns to this screen later ''' # Save the index of the selected object self.selected_disk_index = self.disk_win.active_object LOGGER.debug("disk_selection.on_change_screen, saved_index: %s", self.selected_disk_index) LOGGER.debug(self.doc.persistent) def start_discovery(self): # start target discovery if not self._target_discovery_completed: errsvc.clear_error_list() self.engine.execute_checkpoints(pause_before=TRANSFER_PREP, callback=self._td_callback) def validate(self): '''Validate the size of the disk.''' warning_txt = list() disk = self.disk_detail.ui_obj.doc_obj disk_size_gb = disk.disk_prop.dev_size.get(Size.gb_units) max_vtoc_size_gb = Size(MAX_VTOC).get(Size.gb_units) # Disk size warning should only be displayed if we are restricted to # VTOC boot disks. if not can_use_gpt and disk_size_gb > max_vtoc_size_gb: warning_txt.append(self.disk_warning_too_big) warning_txt = " ".join(warning_txt) if warning_txt: # warn the user and give user a chance to change result = self.main_win.pop_up(DiskScreen.DISK_WARNING_HEADER, warning_txt, DiskScreen.CANCEL_BUTTON, DiskScreen.CONTINUE_BUTTON) if not result: raise UIMessage() # let user select different disk # if user didn't quit it is always OK to ignore disk size, # that will be forced less than the maximum in partitioning. warning_txt = list() # We also need to warn the user if we need to relabel the disk from # GPT to SMI-VTOC if disk.label == "GPT" and not can_use_gpt and \ disk.disk_prop.dev_type != "iSCSI": warning_txt.append(DiskScreen.DISK_WARNING_RELABEL) warning_txt = " ".join(warning_txt) if warning_txt: # warn the user and give user a chance to change result = self.main_win.pop_up(DiskScreen.DISK_WARNING_HEADER, warning_txt, DiskScreen.CANCEL_BUTTON, DiskScreen.CONTINUE_BUTTON) if not result: raise UIMessage() # let user select different disk # if user didn't Cancel it is OK to relabel the disk. # This is one of the lesser known (and potentially dangerous) # features of target controller: select_disk() with # use_whole_disk=True can force a relabeling of the disk from GPT # to VTOC is necessary for booting from the disk disk = self.tc.select_disk(disk, use_whole_disk=True)[0] # The DiskWindow object needs its disk reference updated too self.disk_detail.set_disk_info(disk_info=disk)
class DiskScreen(BaseScreen): ''' Allow the user to select a (valid) disk target for installation Display the partition/slice table for the highlighted disk ''' HEADER_TEXT = _("Disks") PARAGRAPH = _("Where should %(release)s be installed?") % RELEASE REC_SIZE_TEXT = _("Recommended size: ") MIN_SIZE_TEXT = _(" Minimum size: ") DISK_SEEK_TEXT = _("Seeking disks on system") FOUND_x86 = _("The following partitions were found on the disk.") FOUND_SPARC = _("The following slices were found on the disk.") PROPOSED_x86 = _("A partition table was not found. The following is " "proposed.") PROPOSED_SPARC = _("A VTOC label was not found. The following " "is proposed.") PROPOSED_GPT = _("A GPT labeled disk was found. The following is " "proposed.") TOO_SMALL = _("Too small") TOO_BIG_WARN = _("Limited to %.1f TB") GPT_LABELED = _("GPT labeled disk") NO_DISKS = _("No disks found. Additional device drivers may " "be needed.") NO_TARGETS = _("%(release)s cannot be installed on any disk") % RELEASE TGT_ERROR = _("An error occurred while searching for installation" " targets. Please check the install log and file a bug" " at defect.opensolaris.org.") DISK_HEADERS = [(8, _("Type")), (10, _(" Size(GB)")), (6, _("Boot")), (26, _("Device")), (10, _("Vendor")), (3, ""), (10, _("Notes"))] #blank header for the notes column VENDOR_LEN = 15 SPINNER = ["\\", "|", "/", "-"] DISK_WARNING_HEADER = _("Warning") DISK_WARNING_TOOBIG = _("Only the first %.1fTB can be used.") DISK_WARNING_GPT = _("You have chosen a GPT labeled disk. Installing " "onto a GPT labeled disk will cause the loss " "of all existing data and the disk will be " "relabeled as SMI.") CANCEL_BUTTON = _("Cancel") CONTINUE_BUTTON = _("Continue") HELP_DATA = (TUI_HELP + "/%s/disks.txt", _("Disks")) def __init__(self, main_win, target_controller): global LOGGER LOGGER = logging.getLogger(INSTALL_LOGGER_NAME) super(DiskScreen, self).__init__(main_win) if platform.processor() == "i386": self.found_text = DiskScreen.FOUND_x86 self.proposed_text = DiskScreen.PROPOSED_x86 else: self.found_text = DiskScreen.FOUND_SPARC self.proposed_text = DiskScreen.PROPOSED_SPARC disk_header_text = [] for header in DiskScreen.DISK_HEADERS: header_str = fit_text_truncate(header[1], header[0] - 1, just="left") disk_header_text.append(header_str) self.disk_header_text = " ".join(disk_header_text) max_note_size = DiskScreen.DISK_HEADERS[5][0] self.too_small_text = DiskScreen.TOO_SMALL[:max_note_size] max_disk_size = (Size(MAX_VTOC)).get(Size.tb_units) too_big_warn = DiskScreen.TOO_BIG_WARN % max_disk_size self.too_big_warn = too_big_warn[:max_note_size] self.disk_warning_too_big = \ DiskScreen.DISK_WARNING_TOOBIG % max_disk_size self.disks = [] self.existing_pools = [] self.disk_win = None self.disk_detail = None self.num_targets = 0 self.td_handle = None self._size_line = None self.selected_disk_index = 0 self._minimum_size = None self._recommended_size = None self.engine = InstallEngine.get_instance() self.doc = self.engine.data_object_cache self.tc = target_controller self._target_discovery_completed = False self._target_discovery_status = InstallEngine.EXEC_SUCCESS self._image_size = None def determine_minimum(self): '''Returns minimum install size, fetching first if needed''' self.determine_size_data() return self._minimum_size minimum_size = property(determine_minimum) def determine_recommended(self): '''Returns recommended install size, fetching first if needed''' self.determine_size_data() return self._recommended_size recommended_size = property(determine_recommended) def determine_size_data(self): '''Retrieve the minimum and recommended sizes and generate the string to present that information. ''' if self._minimum_size is None or self._recommended_size is None: self._recommended_size = get_recommended_size(self.tc) self._minimum_size = get_minimum_size(self.tc) def get_size_line(self): '''Returns the line of text displaying the min/recommended sizes''' if self._size_line is None: rec_size_str = locale.format("%.1f", self.recommended_size.get(Size.gb_units)) + LOCALIZED_GB min_size_str = locale.format("%.1f", self.minimum_size.get(Size.gb_units)) + LOCALIZED_GB self._size_line = DiskScreen.REC_SIZE_TEXT + rec_size_str + \ DiskScreen.MIN_SIZE_TEXT + min_size_str return self._size_line size_line = property(get_size_line) def wait_for_disks(self): '''Block while waiting for libtd to finish. Catch F9 and quit if needed ''' self.main_win.actions.pop(curses.KEY_F2, None) self.main_win.actions.pop(curses.KEY_F6, None) self.main_win.actions.pop(curses.KEY_F3, None) self.main_win.show_actions() self.center_win.add_text(DiskScreen.DISK_SEEK_TEXT, 5, 1, self.win_size_x - 3) self.main_win.do_update() offset = textwidth(DiskScreen.DISK_SEEK_TEXT) + 2 spin_index = 0 self.center_win.window.timeout(250) while not self._target_discovery_completed: input_key = self.main_win.getch() if input_key == curses.KEY_F9: if self.confirm_quit(): raise QuitException self.center_win.add_text(DiskScreen.SPINNER[spin_index], 5, offset) self.center_win.no_ut_refresh() self.main_win.do_update() spin_index = (spin_index + 1) % len(DiskScreen.SPINNER) self.center_win.window.timeout(-1) self.center_win.clear() # check the result of target discovery if self._target_discovery_status is not InstallEngine.EXEC_SUCCESS: err_data = (errsvc.get_errors_by_mod_id(TARGET_DISCOVERY))[0] LOGGER.error("Target discovery failed") err = err_data.error_data[liberrsvc.ES_DATA_EXCEPTION] LOGGER.error(err) raise TargetDiscoveryError(("Unexpected error (%s) during target " "discovery. See log for details.") % err) def _td_callback(self, status, errsvc): '''Callback function for Target Discovery checkpoint execution. The status value is saved to be interpreted later. This function sets the self._target_discovery_completed value to true so the wait_for_disks() function will know to stop displaying the spinner. ''' self._target_discovery_status = status self._target_discovery_completed = True def _show(self): '''Create a list of disks to choose from and create the window for displaying the partition/slice information from the selected disk ''' self.wait_for_disks() discovered_target = self.doc.persistent.get_first_child( \ name=Target.DISCOVERED) LOGGER.debug(discovered_target) if discovered_target is None: self.center_win.add_paragraph(DiskScreen.NO_DISKS, 1, 1, max_x=(self.win_size_x - 1)) return self.disks = discovered_target.get_children(class_type=Disk) if not self.disks: self.center_win.add_paragraph(DiskScreen.NO_TARGETS, 1, 1, max_x=(self.win_size_x - 1)) return if self._image_size is None: try: self._image_size = Size(str(get_image_size(LOGGER)) + \ Size.mb_units) LOGGER.debug("Image_size: %s", self._image_size) except: # Unable to get the image size for some reason, allow # the target controller to use it's default size. LOGGER.debug("Unable to get image size") self._image_size = FALLBACK_IMAGE_SIZE # initialize the target controller so the min/max size for # the installation can be calculated. Explicitly do not # want to select an initial disk at this time in case # none of the disks discovered is usable. The target controller # initialization needs to be done everytime we show the disk selection # screen so the desired target node in the DOC can be re-populated # with information from target discovery. self.tc.initialize(image_size=self._image_size, no_initial_disk=True) # Go through all the disks found and find ones that have # enough space for installation. At the same time, see if any # existing disk is the boot disk. If a boot disk is found, move # it to the front of the list num_usable_disks = 0 boot_disk = None for disk in self.disks: LOGGER.debug("size: %s, min: %s" % \ (disk.disk_prop.dev_size, self.minimum_size)) if disk.disk_prop.dev_size >= self.minimum_size: if disk.is_boot_disk(): boot_disk = disk num_usable_disks += 1 if boot_disk is not None: self.disks.remove(boot_disk) self.disks.insert(0, boot_disk) if num_usable_disks == 0: self.center_win.add_paragraph(DiskScreen.NO_DISKS, 1, 1, max_x=(self.win_size_x - 1)) return self.main_win.reset_actions() self.main_win.show_actions() y_loc = 1 self.center_win.add_text(DiskScreen.PARAGRAPH, y_loc, 1) y_loc += 1 self.center_win.add_text(self.size_line, y_loc, 1) y_loc += 2 self.center_win.add_text(self.disk_header_text, y_loc, 1) y_loc += 1 self.center_win.window.hline(y_loc, self.center_win.border_size[1] + 1, curses.ACS_HLINE, textwidth(self.disk_header_text)) y_loc += 1 disk_win_area = WindowArea(4, textwidth(self.disk_header_text) + 2, y_loc, 0) disk_win_area.scrollable_lines = len(self.disks) + 1 self.disk_win = ScrollWindow(disk_win_area, window=self.center_win) disk_item_area = WindowArea(1, disk_win_area.columns - 2, 0, 1) disk_index = 0 len_type = DiskScreen.DISK_HEADERS[0][0] - 1 len_size = DiskScreen.DISK_HEADERS[1][0] - 1 len_boot = DiskScreen.DISK_HEADERS[2][0] - 1 len_dev = DiskScreen.DISK_HEADERS[3][0] - 1 len_mftr = DiskScreen.DISK_HEADERS[4][0] - 1 for disk in self.disks: disk_text_fields = [] if disk.disk_prop is None or disk.disk_prop.dev_type is None: continue type_field = disk.disk_prop.dev_type[:len_type] type_field = ljust_columns(type_field, len_type) disk_text_fields.append(type_field) disk_size = disk.disk_prop.dev_size.get(Size.gb_units) size_field = locale.format("%*.1f", (len_size, disk_size)) disk_text_fields.append(size_field) if disk.is_boot_disk(): bootable_field = "+".center(len_boot) else: bootable_field = " " * (len_boot) disk_text_fields.append(bootable_field) device_field = disk.ctd[:len_dev] device_field = ljust_columns(device_field, len_dev) disk_text_fields.append(device_field) vendor = disk.disk_prop.dev_vendor if vendor is not None: mftr_field = vendor[:len_mftr] mftr_field = ljust_columns(mftr_field, len_mftr) else: mftr_field = " " * len_mftr disk_text_fields.append(mftr_field) selectable = True if disk.disk_prop.dev_size < self.minimum_size: note_field = self.too_small_text selectable = False elif disk_size > Size(MAX_VTOC).get(Size.gb_units): note_field = self.too_big_warn else: note_field = "" disk_text_fields.append(note_field) disk_text = " ".join(disk_text_fields) disk_item_area.y_loc = disk_index disk_list_item = ListItem(disk_item_area, window=self.disk_win, text=disk_text, add_obj=selectable) disk_list_item.on_make_active = on_activate disk_list_item.on_make_active_kwargs["disk"] = disk disk_list_item.on_make_active_kwargs["disk_select"] = self disk_index += 1 self.disk_win.no_ut_refresh() y_loc += 7 disk_detail_area = WindowArea(6, 70, y_loc, 1) self.disk_detail = DiskWindow(disk_detail_area, self.disks[0], target_controller=self.tc, window=self.center_win) self.main_win.do_update() self.center_win.activate_object(self.disk_win) self.disk_win.activate_object(self.selected_disk_index) def on_change_screen(self): ''' Save the index of the current selected object in case the user returns to this screen later ''' # Save the index of the selected object self.selected_disk_index = self.disk_win.active_object LOGGER.debug("disk_selection.on_change_screen, saved_index: %s", self.selected_disk_index) LOGGER.debug(self.doc.persistent) def start_discovery(self): # start target discovery if not self._target_discovery_completed: errsvc.clear_error_list() self.engine.execute_checkpoints(pause_before=TRANSFER_PREP, callback=self._td_callback) def validate(self): '''Validate the size of the disk.''' warning_txt = [] disk = self.disk_detail.ui_obj.doc_obj disk_size_gb = disk.disk_prop.dev_size.get(Size.gb_units) max_size_gb = Size(MAX_VTOC).get(Size.gb_units) if disk_size_gb > max_size_gb: warning_txt.append(self.disk_warning_too_big) warning_txt = " ".join(warning_txt) if warning_txt: # warn the user and give user a chance to change result = self.main_win.pop_up(DiskScreen.DISK_WARNING_HEADER, warning_txt, DiskScreen.CANCEL_BUTTON, DiskScreen.CONTINUE_BUTTON) if not result: raise UIMessage() # let user select different disk