Redesign OS distro selection UI to be faster to use

The current OS distro selection UI is fairly cumbersome to use. First
you need to decide on a variant, then decide a distro and then look for
the version you want. The list is filtered by default so only a subset
of OS are displayed. So for less common distros you'll then need to
start again and tell it to show all OS to try to find the one you want.

The core problem is that we have an incredibly large list and want to
make it easy for the user to find a specific entry. The modern UI
paradigm for this problem is to provide interactive search with
live updated results. The current UI does provide an interactive search
facility on the OS version results, but you still have to first select a
variant to be able to use the search which is unhelpful.

This patch attempts to better apply the search UI design to the OS selection
problem. We get rid of the notion of variants, distros and version, and
provide a single text entry box in which the user can type a few letters
of the OS name. As they type, a popover displays the matching results
filtered on OS name. By default end of life OS will be hidden, so in
general there will only be a small handful of results left after just
typing a few characters. This makes it very quick to find and select the
desired OS, without needing to provide a mutli-step navigation hierarchy.

https://bugzilla.redhat.com/show_bug.cgi?id=1464306

Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>

(crobinso: fix some pylint)
This commit is contained in:
Daniel P. Berrangé 2018-05-01 12:51:23 +01:00 committed by Cole Robinson
parent b1460ba065
commit d52d9885c8
6 changed files with 361 additions and 641 deletions

View File

@ -45,29 +45,3 @@ class TestOSDB(unittest.TestCase):
guest.type = "qemu" guest.type = "qemu"
res = OSDB.lookup_os("fedora21").get_recommended_resources(guest) res = OSDB.lookup_os("fedora21").get_recommended_resources(guest)
assert res["n-cpus"] == 1 assert res["n-cpus"] == 1
def test_list_os(self):
full_list = OSDB.list_os()
pref_list = OSDB.list_os(typename="linux", sortpref=["fedora", "rhel"])
support_list = OSDB.list_os(only_supported=True)
assert full_list[0] is not pref_list[0]
assert len(full_list) > len(support_list)
assert len(OSDB.list_os(typename="generic")) == 1
# Verify that sort order actually worked
found_fedora = False
found_rhel = False
for idx, osobj in enumerate(pref_list[:]):
if osobj.name.startswith("fedora"):
found_fedora = True
continue
for osobj2 in pref_list[idx:]:
if osobj2.name.startswith("rhel"):
found_rhel = True
continue
break
break
assert found_fedora and found_rhel

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.3 --> <!-- Generated with glade 3.20.4 -->
<interface> <interface>
<requires lib="gtk+" version="3.14"/> <requires lib="gtk+" version="3.14"/>
<object class="GtkAdjustment" id="adjustment2"> <object class="GtkAdjustment" id="adjustment2">
@ -18,8 +18,8 @@
<property name="stock">gtk-new</property> <property name="stock">gtk-new</property>
</object> </object>
<object class="GtkWindow" id="vmm-create"> <object class="GtkWindow" id="vmm-create">
<property name="width_request">400</property> <property name="width_request">600</property>
<property name="height_request">400</property> <property name="height_request">500</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">New VM</property> <property name="title" translatable="yes">New VM</property>
<property name="resizable">False</property> <property name="resizable">False</property>
@ -2027,184 +2027,89 @@ connections is not yet supported.&lt;/small&gt;</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkBox" id="install-os-distro-box"> <object class="GtkFrame" id="install-os-distro-box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="orientation">vertical</property> <property name="label_xalign">0</property>
<property name="spacing">6</property> <property name="shadow_type">none</property>
<child> <child>
<object class="GtkBox" id="install-detect-os-box"> <object class="GtkAlignment">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="orientation">vertical</property> <property name="top_padding">6</property>
<signal name="hide" handler="on_install_detect_os_box_hide" swapped="no"/> <property name="bottom_padding">6</property>
<signal name="show" handler="on_install_detect_os_box_show" swapped="no"/> <property name="left_padding">12</property>
<child> <child>
<object class="GtkCheckButton" id="install-detect-os"> <object class="GtkBox">
<property name="label" translatable="yes">A_utomatically detect operating system based on install media</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_install_detect_os_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="install-nodetect-label">
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Choose an operating system type and version</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment9">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">15</property>
<child>
<object class="GtkGrid" id="table1">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="row_spacing">4</property> <property name="orientation">vertical</property>
<property name="column_spacing">6</property> <property name="spacing">6</property>
<child> <child>
<object class="GtkLabel" id="install-os-version-label"> <object class="GtkSearchEntry" id="install-os-name">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">True</property>
<property name="halign">start</property> <property name="primary_icon_name">edit-find-symbolic</property>
<property name="valign">center</property> <property name="primary_icon_activatable">False</property>
<property name="label">-</property> <property name="primary_icon_sensitive">False</property>
<child internal-child="accessible"> <signal name="search-changed" handler="on_install_os_name_search_changed" swapped="no"/>
<object class="AtkObject" id="install-os-version-label-atkobject"> <signal name="stop-search" handler="on_install_os_name_stop_search" swapped="no"/>
<property name="AtkObject::accessible-name">install-os-version-label</property>
</object>
</child>
</object> </object>
<packing> <packing>
<property name="left_attach">2</property> <property name="expand">False</property>
<property name="top_attach">1</property> <property name="fill">True</property>
<property name="position">0</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkLabel" id="install-os-type-label"> <object class="GtkBox" id="install-detect-os-box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="halign">start</property> <child>
<property name="valign">center</property> <object class="GtkCheckButton" id="install-detect-os">
<property name="label">-</property> <property name="label" translatable="yes">A_utomatically detect from the installation media / source</property>
<child internal-child="accessible"> <property name="visible">True</property>
<object class="AtkObject" id="install-os-type-label-atkobject">
<property name="AtkObject::accessible-name">install-os-type-label</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="install-os-type">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<signal name="changed" handler="on_install_os_type_changed" swapped="no"/>
<child internal-child="accessible">
<object class="AtkObject" id="install-os-type-atkobject">
<property name="AtkObject::accessible-name">install-os-type</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label17">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="label" translatable="yes">_Version:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">install-os-version</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label16">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="valign">center</property>
<property name="label" translatable="yes">OS _type:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">install-os-type</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="install-os-version">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="has_entry">True</property>
<signal name="changed" handler="on_install_os_version_changed" swapped="no"/>
<child internal-child="entry">
<object class="GtkEntry" id="install-os-version-entry">
<property name="can_focus">True</property> <property name="can_focus">True</property>
<child internal-child="accessible"> <property name="receives_default">False</property>
<object class="AtkObject" id="install-os-version-entry-atkobject"> <property name="use_underline">True</property>
<property name="AtkObject::accessible-name">install-os-version-entry</property> <property name="active">True</property>
</object> <property name="draw_indicator">True</property>
</child> <signal name="toggled" handler="on_install_detect_os_toggled" swapped="no"/>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> </child>
<child internal-child="accessible"> <child>
<object class="AtkObject" id="install-os-version-atkobject"> <object class="GtkSpinner" id="install-detect-os-spinner">
<property name="AtkObject::accessible-name">install-os-version</property> <property name="visible">True</property>
<property name="can_focus">False</property>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child> </child>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="expand">False</property>
<property name="top_attach">1</property> <property name="fill">True</property>
<property name="position">1</property>
</packing> </packing>
</child> </child>
</object> </object>
</child> </child>
</object> </object>
<packing> </child>
<property name="expand">False</property> <child type="label">
<property name="fill">False</property> <object class="GtkLabel">
<property name="position">2</property> <property name="visible">True</property>
</packing> <property name="can_focus">False</property>
<property name="label" translatable="yes">Operating system distribution:</property>
</object>
</child> </child>
</object> </object>
<packing> <packing>
@ -2753,50 +2658,6 @@ connections is not yet supported.&lt;/small&gt;</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkAlignment" id="finish-warn-os-align">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">6</property>
<child>
<object class="GtkBox" id="finish-warn-os">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">3</property>
<child>
<object class="GtkImage" id="image4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-warning</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label47">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;small&gt;Specifying an operating system is required for best performance&lt;/small&gt;</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child> <child>
<object class="GtkExpander" id="advanced-expander"> <object class="GtkExpander" id="advanced-expander">
<property name="visible">True</property> <property name="visible">True</property>
@ -2884,14 +2745,13 @@ connections is not yet supported.&lt;/small&gt;</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">N_etwork selection</property> <property name="label" translatable="yes">N_etwork selection</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="mnemonic_widget">advanced-expander</property>
</object> </object>
</child> </child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">False</property>
<property name="position">2</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
</object> </object>

104
ui/oslist.ui Normal file
View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.4 -->
<interface>
<requires lib="gtk+" version="3.14"/>
<object class="GtkPopover" id="vmm-oslist">
<property name="width_request">400</property>
<property name="height_request">300</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-info</property>
<property name="icon_size">3</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Can't find the operating system you are looking for ?
Try selecting the next most recent version displayed,
or use the "Generic" entry.</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="vscrollbar_policy">always</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="os-list">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<property name="enable_search">False</property>
<property name="hover_selection">True</property>
<property name="enable_grid_lines">horizontal</property>
<property name="activate_on_single_click">True</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="include-eol">
<property name="label" translatable="yes">Include end of life operating systems</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_include_eol_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@ -29,6 +29,7 @@ from .engine import vmmEngine
from .mediacombo import vmmMediaCombo from .mediacombo import vmmMediaCombo
from .netlist import vmmNetworkList from .netlist import vmmNetworkList
from .storagebrowse import vmmStorageBrowser from .storagebrowse import vmmStorageBrowser
from .oslist import vmmOSList
# Number of seconds to wait for media detection # Number of seconds to wait for media detection
DETECT_TIMEOUT = 20 DETECT_TIMEOUT = 20
@ -124,6 +125,7 @@ class vmmCreate(vmmGObjectUI):
self._guest = None self._guest = None
self._failed_guest = None self._failed_guest = None
self._os = None
# Distro detection state variables # Distro detection state variables
self._detect_os_in_progress = False self._detect_os_in_progress = False
@ -171,10 +173,6 @@ class vmmCreate(vmmGObjectUI):
"on_install_container_source_toggle": self._container_source_toggle, "on_install_container_source_toggle": self._container_source_toggle,
"on_install_detect_os_toggled": self._toggle_detect_os, "on_install_detect_os_toggled": self._toggle_detect_os,
"on_install_os_type_changed": self._change_os_type,
"on_install_os_version_changed": self._change_os_version,
"on_install_detect_os_box_show": self._os_detect_visibility_changed,
"on_install_detect_os_box_hide": self._os_detect_visibility_changed,
"on_kernel_browse_clicked": self._browse_kernel, "on_kernel_browse_clicked": self._browse_kernel,
"on_initrd_browse_clicked": self._browse_initrd, "on_initrd_browse_clicked": self._browse_initrd,
@ -183,6 +181,9 @@ class vmmCreate(vmmGObjectUI):
"on_enable_storage_toggled": self._toggle_enable_storage, "on_enable_storage_toggled": self._toggle_enable_storage,
"on_create_vm_name_changed": self._name_changed, "on_create_vm_name_changed": self._name_changed,
"on_install_os_name_search_changed": self._os_name_search_changed,
"on_install_os_name_stop_search": self._os_name_stop_search,
}) })
self.bind_escape_key_close() self.bind_escape_key_close()
@ -294,46 +295,6 @@ class vmmCreate(vmmGObjectUI):
# Lists for OS container bootstrap # Lists for OS container bootstrap
set_model_list("install-oscontainer-source-url-combo") set_model_list("install-oscontainer-source-url-combo")
def sep_func(model, it, combo):
ignore = combo
return model[it][OS_COL_IS_SEP]
def make_os_model():
# [os value, os label, is seperator, is 'show all']
cols = []
cols.insert(OS_COL_ID, str)
cols.insert(OS_COL_LABEL, str)
cols.insert(OS_COL_IS_SEP, bool)
cols.insert(OS_COL_IS_SHOW_ALL, bool)
return Gtk.TreeStore(*cols)
def make_completion_model():
# [os value, os label]
cols = []
cols.insert(OS_COL_ID, str)
cols.insert(OS_COL_LABEL, str)
return Gtk.ListStore(*cols)
# Lists for distro type + variant
os_type_list = self.widget("install-os-type")
os_type_model = make_os_model()
os_type_list.set_model(os_type_model)
uiutil.init_combo_text_column(os_type_list, 1)
os_type_list.set_row_separator_func(sep_func, os_type_list)
os_variant_list = self.widget("install-os-version")
os_variant_model = make_os_model()
os_variant_list.set_model(os_variant_model)
uiutil.init_combo_text_column(os_variant_list, 1)
os_variant_list.set_row_separator_func(sep_func, os_variant_list)
entry = self.widget("install-os-version-entry")
completion = Gtk.EntryCompletion()
entry.set_completion(completion)
completion.set_text_column(1)
completion.set_inline_completion(True)
completion.set_model(make_completion_model())
# Archtecture # Archtecture
archList = self.widget("arch") archList = self.widget("arch")
# [label, guest.os.arch value] # [label, guest.os.arch value]
@ -365,6 +326,16 @@ class vmmCreate(vmmGObjectUI):
lst.set_model(model) lst.set_model(model)
uiutil.init_combo_text_column(lst, 0) uiutil.init_combo_text_column(lst, 0)
self._os_list = vmmOSList()
self._os_list.connect("os-selected", self._os_name_selected)
def _os_name_selected(self, ignore, osobj):
name = self.widget("install-os-name")
self._os = osobj
self._os_list.hide()
if self._os is not None:
name.set_text(self._os.label)
def _reset_state(self, urihint=None): def _reset_state(self, urihint=None):
""" """
@ -395,8 +366,6 @@ class vmmCreate(vmmGObjectUI):
# Distro/Variant # Distro/Variant
self._toggle_detect_os(self.widget("install-detect-os")) self._toggle_detect_os(self.widget("install-detect-os"))
self._populate_os_type_model()
self.widget("install-os-type").set_active(0)
def _populate_media_model(media_model, urls): def _populate_media_model(media_model, urls):
media_model.clear() media_model.clear()
@ -415,7 +384,6 @@ class vmmCreate(vmmGObjectUI):
self.widget("install-url-options").set_expanded(False) self.widget("install-url-options").set_expanded(False)
urlmodel = self.widget("install-url-combo").get_model() urlmodel = self.widget("install-url-combo").get_model()
_populate_media_model(urlmodel, self.config.get_media_urls()) _populate_media_model(urlmodel, self.config.get_media_urls())
self._set_distro_labels("-", "-")
# Install import # Install import
self.widget("install-import-entry").set_text("") self.widget("install-import-entry").set_text("")
@ -970,183 +938,6 @@ class vmmCreate(vmmGObjectUI):
# Helpers for populating OS type/variant UI # # Helpers for populating OS type/variant UI #
############################################# #############################################
def _add_os_row(self, model, name="", label="",
sep=False, action=False, parent=None):
"""
Helper for building an os type/version row and adding it to
the list model if necessary
"""
row = []
row.insert(OS_COL_ID, name)
row.insert(OS_COL_LABEL, label)
row.insert(OS_COL_IS_SEP, sep)
row.insert(OS_COL_IS_SHOW_ALL, action)
return model.append(parent, row)
def _add_completion_row(self, model, name, label):
row = []
row.insert(OS_COL_ID, name)
row.insert(OS_COL_LABEL, label)
model.append(row)
def _populate_os_type_model(self):
widget = self.widget("install-os-type")
model = widget.get_model()
model.clear()
# Kind of a hack, just show linux + windows by default since
# that's all 98% of people care about
supported = {"generic", "linux", "windows"}
# Move 'generic' to the front of the list
types = virtinst.OSDB.list_types()
types.remove("generic")
types.insert(0, "generic")
# Pretty names for OSes. If a new OS is not found here,
# its capitalized name is used.
oses = {
"bsd": _("BSD"),
"generic": _("Generic"),
"linux": _("Linux"),
"macos": _("macOS"),
"other": _("Others"),
"solaris": _("Solaris"),
"windows": _("Windows"),
}
# When only the "supported" types are requested,
# filter them.
if not self._show_all_os_was_selected:
types = [t for t in types if t in supported]
for typename in types:
try:
typelabel = oses[typename]
except KeyError:
typelabel = typename.capitalize()
self._add_os_row(model, typename, typelabel)
if not self._show_all_os_was_selected:
self._add_os_row(model, sep=True)
self._add_os_row(model, label=_("Show all OS options"), action=True)
# Select 'generic' by default
widget.set_active(0)
def _populate_os_variant_model(self, _type):
widget = self.widget("install-os-version")
model = widget.get_model()
model.clear()
completion_model = self.widget("install-os-version-entry").get_completion().get_model()
completion_model.clear()
preferred = self.config.preferred_distros
# All the subgroups for top-level types. Distributions not
# belonging to these groups will be shown either in a "Others"
# group, or top-level if there are no other groups.
groups = {
"altlinux": _("ALT Linux"),
"centos": _("CentOS"),
"debian": _("Debian"),
"fedora": _("Fedora"),
"freebsd": _("FreeBSD"),
"mageia": _("Mageia"),
"netbsd": _("NetBSD"),
"openbsd": _("OpenBSD"),
"opensuse": _("openSUSE"),
"rhel": _("Red Hat Enterprise Linux"),
"sled": _("SUSE Linux Enterprise Desktop"),
"sles": _("SUSE Linux Enterprise Server"),
"ubuntu": _("Ubuntu"),
}
if self._show_all_os_was_selected:
# List all the OSes, and determine which OSes have groups,
# and which do not.
variants = virtinst.OSDB.list_os(typename=_type,
sortpref=preferred)
all_distros = set([_os.distro for _os in variants])
distros = [_os for _os in all_distros if _os in groups]
distros.sort()
other_distros = [_os for _os in all_distros if _os not in groups]
parents = dict()
if len(distros) > 0:
# We have groups for the OSes, so create them.
for d in distros:
parents[d] = self._add_os_row(model, "", groups[d])
# Create the "Others" group at the end, for the OSes
# without a group.
if len(other_distros):
others_parent = self._add_os_row(model, "", _('Others'))
for d in other_distros:
parents[d] = others_parent
else:
# No groups, so assume the top-level will be the parent
# all the OSes.
for d in other_distros:
parents[d] = None
for v in variants:
self._add_os_row(model, v.name, v.label,
parent=parents[v.distro])
self._add_completion_row(completion_model, v.name, v.label)
else:
# We are showing only the supported systems, so query them,
# and add them directly to their type.
variants = virtinst.OSDB.list_os(typename=_type,
sortpref=preferred, only_supported=True)
for v in variants:
self._add_os_row(model, v.name, v.label)
self._add_completion_row(completion_model, v.name, v.label)
# Add the menu entries to show all the OSes
self._add_os_row(model, sep=True)
self._add_os_row(model, label=_("Show all OS options"), action=True)
widget.set_active(0)
def _set_distro_labels(self, distro, ver):
self.widget("install-os-type-label").set_text(distro)
self.widget("install-os-version-label").set_text(ver)
def _set_os_id_in_ui(self, os_widget, os_id):
"""
Helper method to set the os type/version widgets to the passed
OS ID value
"""
model = os_widget.get_model()
def find_row():
def cmp_func(model, path, it, user_data):
ignore = path
if model.get_value(it, OS_COL_ID) == os_id:
os_widget.set_active_iter(it)
user_data[0] = model.get_value(it, OS_COL_LABEL)
return True
return False
data = [None]
model.foreach(cmp_func, data)
label = data[0]
if not label:
os_widget.set_active(0)
return label
label = None
if os_id:
label = find_row()
if not label and not self._show_all_os_was_selected:
# We didn't find the OS in the variant UI, but we are only
# showing the reduced OS list. Trigger the _show_all_os option,
# and try again.
os_widget.set_active(len(model) - 1)
label = find_row()
return label or _("Unknown")
def _set_distro_selection(self, variant): def _set_distro_selection(self, variant):
""" """
Update the UI with the distro that was detected from the detection Update the UI with the distro that was detected from the detection
@ -1157,18 +948,16 @@ class vmmCreate(vmmGObjectUI):
# update the UI # update the UI
return return
distro_type = None name = self.widget("install-os-name")
distro_var = None
if variant: if variant:
osclass = virtinst.OSDB.lookup_os(variant) self._os = virtinst.OSDB.lookup_os(variant)
distro_type = osclass.get_typename() else:
distro_var = osclass.name self._os = None
dl = self._set_os_id_in_ui( if self._os is None:
self.widget("install-os-type"), distro_type) name.set_text(_("None detected"))
vl = self._set_os_id_in_ui( else:
self.widget("install-os-version"), distro_var) name.set_text(self._os.label)
self._set_distro_labels(dl, vl)
############################### ###############################
@ -1200,7 +989,6 @@ class vmmCreate(vmmGObjectUI):
self.widget("summary-storage-path").set_markup(storagepath) self.widget("summary-storage-path").set_markup(storagepath)
def _populate_summary(self): def _populate_summary(self):
distro, version, ignore1, dlabel, vlabel = self._get_config_os_info()
mem = _pretty_memory(int(self._guest.memory)) mem = _pretty_memory(int(self._guest.memory))
cpu = str(int(self._guest.vcpus)) cpu = str(int(self._guest.vcpus))
@ -1221,21 +1009,7 @@ class vmmCreate(vmmGObjectUI):
elif instmethod == INSTALL_PAGE_VZ_TEMPLATE: elif instmethod == INSTALL_PAGE_VZ_TEMPLATE:
install = _("Virtuozzo container") install = _("Virtuozzo container")
osstr = "" self.widget("summary-os").set_text(self._os and self._os.label or _("Unknown"))
have_os = True
if self._guest.os.is_container():
osstr = _("Linux")
elif not distro:
osstr = _("Generic")
have_os = False
elif not version:
osstr = _("Generic") + " " + dlabel
have_os = False
else:
osstr = vlabel
self.widget("finish-warn-os").set_visible(not have_os)
self.widget("summary-os").set_text(osstr)
self.widget("summary-install").set_text(install) self.widget("summary-install").set_text(install)
self.widget("summary-mem").set_text(mem) self.widget("summary-mem").set_text(mem)
self.widget("summary-cpu").set_text(cpu) self.widget("summary-cpu").set_text(cpu)
@ -1317,31 +1091,6 @@ class vmmCreate(vmmGObjectUI):
INSTALL_PAGE_CONTAINER_OS, INSTALL_PAGE_CONTAINER_OS,
INSTALL_PAGE_VZ_TEMPLATE] INSTALL_PAGE_VZ_TEMPLATE]
def _get_config_os_info(self):
drow = uiutil.get_list_selected_row(self.widget("install-os-type"))
distro = None
dlabel = None
variant = None
entry = self.widget("install-os-version-entry")
vlabel = entry.get_text()
for row in entry.get_completion().get_model():
if row[OS_COL_LABEL] == vlabel:
variant = row[OS_COL_ID]
break
if not variant:
return (None, None, False, None, None)
if drow:
distro = drow[OS_COL_ID]
dlabel = drow[OS_COL_LABEL]
return (distro and str(distro),
str(variant),
True,
str(dlabel), str(vlabel))
def _get_config_local_media(self, store_media=False): def _get_config_local_media(self, store_media=False):
if self.widget("install-cdrom-radio").get_active(): if self.widget("install-cdrom-radio").get_active():
return self._mediacombo.get_path() return self._mediacombo.get_path()
@ -1506,56 +1255,30 @@ class vmmCreate(vmmGObjectUI):
def _toggle_detect_os(self, src): def _toggle_detect_os(self, src):
dodetect = src.get_active() dodetect = src.get_active()
self.widget("install-os-type-label").set_visible(dodetect) self.widget("install-os-name").set_sensitive(not dodetect)
self.widget("install-os-version-label").set_visible(dodetect) self.widget("install-os-name").set_text("")
self.widget("install-os-type").set_visible(not dodetect) self._os = None
self.widget("install-os-version").set_visible(not dodetect)
if dodetect: if dodetect:
self.widget("install-os-version-entry").set_text("")
self._os_already_detected_for_media = False self._os_already_detected_for_media = False
self._start_detect_os_if_needed() self._start_detect_os_if_needed()
def _selected_os_row(self): def _os_name_search_changed(self, src):
return uiutil.get_list_selected_row(self.widget("install-os-type")) searchname = src.get_text().strip()
if self._os is None:
if src.get_sensitive() and searchname != "":
self._os_list.filter_name(searchname)
self._os_list.show(src)
else:
self._os_list.hide()
else:
if self._os.label != searchname:
self._os = None
self._os_list.hide()
def _change_os_type(self, box): def _os_name_stop_search(self, src):
ignore = box src.set_text("")
row = self._selected_os_row() self._os_list.hide()
if not row:
return
_type = row[OS_COL_ID]
self._populate_os_variant_model(_type)
if not row[OS_COL_IS_SHOW_ALL]:
return
self._show_all_os_was_selected = True
self._populate_os_type_model()
def _change_os_version(self, box):
show_all = uiutil.get_list_selection(box,
column=OS_COL_IS_SHOW_ALL, check_entry=False)
if not show_all:
return
# 'show all OS' was clicked
# Get previous type to reselect it later
type_row = self._selected_os_row()
if not type_row:
return
old_type = type_row[OS_COL_ID]
self._show_all_os_was_selected = True
self._populate_os_type_model()
# Reselect previous type row
os_type_list = self.widget("install-os-type")
os_type_model = os_type_list.get_model()
for idx, row in enumerate(os_type_model):
if row[OS_COL_ID] == old_type:
os_type_list.set_active(idx)
break
def _local_media_toggled(self, src): def _local_media_toggled(self, src):
usecdrom = src.get_active() usecdrom = src.get_active()
@ -1568,19 +1291,6 @@ class vmmCreate(vmmGObjectUI):
else: else:
self._iso_changed(self.widget("install-iso-entry")) self._iso_changed(self.widget("install-iso-entry"))
def _os_detect_visibility_changed(self, src, ignore=None):
is_visible = src.get_visible()
detect_chkbox = self.widget("install-detect-os")
nodetect_label = self.widget("install-nodetect-label")
detect_chkbox.set_active(is_visible)
detect_chkbox.toggled()
if is_visible:
nodetect_label.hide()
else:
nodetect_label.show()
def _browse_oscontainer(self, ignore): def _browse_oscontainer(self, ignore):
self._browse_file("install-oscontainer-fs", is_dir=True) self._browse_file("install-oscontainer-fs", is_dir=True)
def _browse_app(self, ignore): def _browse_app(self, ignore):
@ -1666,7 +1376,7 @@ class vmmCreate(vmmGObjectUI):
fs_dir = [os.environ['HOME'], fs_dir = [os.environ['HOME'],
'.local/share/libvirt/filesystems/'] '.local/share/libvirt/filesystems/']
fs = fs_dir + [self._generate_default_name(None, None)] fs = fs_dir + [self._generate_default_name(None)]
self.widget("install-oscontainer-fs").set_text(os.path.join(*fs)) self.widget("install-oscontainer-fs").set_text(os.path.join(*fs))
@ -1737,7 +1447,9 @@ class vmmCreate(vmmGObjectUI):
def _set_install_page(self): def _set_install_page(self):
instnotebook = self.widget("install-method-pages") instnotebook = self.widget("install-method-pages")
detectbox = self.widget("install-detect-os-box") detectbox = self.widget("install-detect-os-box")
detect = self.widget("install-detect-os")
osbox = self.widget("install-os-distro-box") osbox = self.widget("install-os-distro-box")
name = self.widget("install-os-name")
instpage = self._get_config_install_page() instpage = self._get_config_install_page()
# Setting OS value for a container guest doesn't really matter # Setting OS value for a container guest doesn't really matter
@ -1752,6 +1464,13 @@ class vmmCreate(vmmGObjectUI):
self._get_config_install_page() == INSTALL_PAGE_URL) self._get_config_install_page() == INSTALL_PAGE_URL)
detectbox.set_visible(enabledetect) detectbox.set_visible(enabledetect)
autodetect = detectbox.get_visible() and detect.get_active()
name.set_sensitive(not autodetect)
if enabledetect:
self._os = None
else:
if self._os is None:
name.set_text("")
if instpage == INSTALL_PAGE_PXE: if instpage == INSTALL_PAGE_PXE:
# Hide the install notebook for pxe, since there isn't anything # Hide the install notebook for pxe, since there isn't anything
@ -1915,18 +1634,16 @@ class vmmCreate(vmmGObjectUI):
return False return False
return True return True
def _generate_default_name(self, distro, variant): def _generate_default_name(self, osobj):
force_num = False force_num = False
if self._guest.os.is_container(): if self._guest.os.is_container():
basename = "container" basename = "container"
force_num = True force_num = True
elif not distro: elif not osobj or not osobj.distro:
basename = "vm" basename = "vm"
force_num = True force_num = True
elif not variant:
basename = distro
else: else:
basename = variant basename = osobj.distro
if self._guest.os.arch != self.conn.caps.host.cpu.arch: if self._guest.os.arch != self.conn.caps.host.cpu.arch:
basename += "-%s" % _pretty_arch(self._guest.os.arch) basename += "-%s" % _pretty_arch(self._guest.os.arch)
@ -1949,9 +1666,8 @@ class vmmCreate(vmmGObjectUI):
init = None init = None
fs = None fs = None
template = None template = None
distro, variant, valid, ignore1, ignore2 = self._get_config_os_info()
if not valid: if self._os is None:
return self.err.val_err(_("Please specify a valid OS variant.")) return self.err.val_err(_("Please specify a valid OS variant."))
if instmethod == INSTALL_PAGE_ISO: if instmethod == INSTALL_PAGE_ISO:
@ -2050,7 +1766,7 @@ class vmmCreate(vmmGObjectUI):
try: try:
# Overwrite the guest # Overwrite the guest
installer = instclass(self.conn.get_backend()) installer = instclass(self.conn.get_backend())
self._guest = self._build_guest(variant or distro) self._guest = self._build_guest(self._os.name)
if not self._guest: if not self._guest:
return False return False
self._guest.installer = installer self._guest.installer = installer
@ -2114,7 +1830,7 @@ class vmmCreate(vmmGObjectUI):
self._capsinfo.arch) self._capsinfo.arch)
try: try:
name = self._generate_default_name(distro, variant) name = self._generate_default_name(self._os)
self.widget("create-vm-name").set_text(name) self.widget("create-vm-name").set_text(name)
self._guest.name = name self._guest.name = name
except Exception as e: except Exception as e:
@ -2138,11 +1854,10 @@ class vmmCreate(vmmGObjectUI):
self, self.conn, path) self, self.conn, path)
res = None res = None
osobj = virtinst.OSDB.lookup_os(variant) if self._os is not None:
if osobj: res = self._os.get_recommended_resources(self._guest)
res = osobj.get_recommended_resources(self._guest) logging.debug("Recommended resources for os=%s: %s",
logging.debug("Recommended resources for variant=%s: %s", self._os.label, res)
variant, res)
# Change the default values suggested to the user. # Change the default values suggested to the user.
ram_size = DEFAULT_MEM ram_size = DEFAULT_MEM
@ -2313,6 +2028,9 @@ class vmmCreate(vmmGObjectUI):
if check_install_page and not is_install_page: if check_install_page and not is_install_page:
return return
if not media: if not media:
name = self.widget("install-os-name")
if not name.get_sensitive():
name.set_text(_("Waiting for install media / source"))
return return
if not self._is_os_detect_active(): if not self._is_os_detect_active():
return return
@ -2357,6 +2075,9 @@ class vmmCreate(vmmGObjectUI):
detectThread.setDaemon(True) detectThread.setDaemon(True)
detectThread.start() detectThread.start()
spin = self.widget("install-detect-os-spinner")
spin.start()
self._report_detect_os_progress(0, thread_results, self._report_detect_os_progress(0, thread_results,
forward_after_finish) forward_after_finish)
@ -2385,15 +2106,10 @@ class vmmCreate(vmmGObjectUI):
chance of the detection hanging (like slow URL lookup) chance of the detection hanging (like slow URL lookup)
""" """
try: try:
base = _("Detecting")
if (thread_results.in_progress() and if (thread_results.in_progress() and
(idx < (DETECT_TIMEOUT * 2))): (idx < (DETECT_TIMEOUT * 2))):
# Thread is still going and we haven't hit the timeout yet, # Thread is still going and we haven't hit the timeout yet,
# so update the UI labels and reschedule this function # so update the UI labels and reschedule this function
detect_str = base + ("." * ((idx % 3) + 1))
self._set_distro_labels(detect_str, detect_str)
self.timeout_add(500, self._report_detect_os_progress, self.timeout_add(500, self._report_detect_os_progress,
idx + 1, thread_results, forward_after_finish) idx + 1, thread_results, forward_after_finish)
return return
@ -2403,6 +2119,8 @@ class vmmCreate(vmmGObjectUI):
distro = None distro = None
logging.exception("Error in distro detect timeout") logging.exception("Error in distro detect timeout")
spin = self.widget("install-detect-os-spinner")
spin.stop()
logging.debug("Finished UI OS detection.") logging.debug("Finished UI OS detection.")
self.widget("create-forward").set_sensitive(True) self.widget("create-forward").set_sensitive(True)

106
virtManager/oslist.py Normal file
View File

@ -0,0 +1,106 @@
# Copyright (C) 2018 Red Hat, Inc.
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
import logging
from gi.repository import Gtk
import virtinst
from .baseclass import vmmGObjectUI
class vmmOSList(vmmGObjectUI):
__gsignals__ = {
"os-selected": (vmmGObjectUI.RUN_FIRST, None, [object])
}
def __init__(self):
vmmGObjectUI.__init__(self, "oslist.ui", "vmm-oslist")
self._cleanup_on_app_close()
self._filter_name = None
self._filter_eol = True
self.builder.connect_signals({
"on_include_eol_toggled": self._eol_toggled,
})
self._init_state()
def _init_state(self):
self.topwin.set_modal(False)
os_list = self.widget("os-list")
# (os object, label)
os_list_model = Gtk.ListStore(object, str)
all_os = virtinst.OSDB.list_os()
for os in all_os:
os_list_model.append([os, "%s (%s)" % (os.label, os.name)])
self._os_list_model = Gtk.TreeModelFilter(child_model=os_list_model)
self._os_list_model.set_visible_func(self._filter_os)
os_list.set_model(self._os_list_model)
nameCol = Gtk.TreeViewColumn(_("Name"))
nameCol.set_spacing(6)
text = Gtk.CellRendererText()
nameCol.pack_start(text, True)
nameCol.add_attribute(text, 'text', 1)
os_list.append_column(nameCol)
os_list.connect("row_activated", self._os_selected_cb)
def _eol_toggled(self, src):
self._filter_eol = not src.get_active()
self._refilter()
def _os_selected_cb(self, tree_view, path, column):
model, titer = tree_view.get_selection().get_selected()
if titer is None:
self.emit("os-selected", None)
else:
self.emit("os-selected", model[titer][0])
def _filter_os(self, model, titer, ignore1):
os = model.get(titer, 0)[0]
if self._filter_eol:
if os.eol:
return False
if self._filter_name is not None and self._filter_name != "":
label = os.label.lower()
name = os.name.lower()
if (label.find(self._filter_name) == -1 and
name.find(self._filter_name) == -1):
return False
return True
def _refilter(self):
os_list = self.widget("os-list")
sel = os_list.get_selection()
sel.unselect_all()
self._os_list_model.refilter()
def filter_name(self, partial_name):
self._filter_name = partial_name.lower()
self._refilter()
def show(self, parent):
logging.debug("Showing oslist")
self.topwin.set_relative_to(parent)
self.topwin.popup()
def hide(self):
self.topwin.popdown()
def _cleanup(self):
pass

View File

@ -10,62 +10,22 @@
import datetime import datetime
import logging import logging
import re import re
import time
import gi import gi
gi.require_version('Libosinfo', '1.0') gi.require_version('Libosinfo', '1.0')
from gi.repository import Libosinfo as libosinfo from gi.repository import Libosinfo as libosinfo
from gi.repository import GLib
################### ###################
# Sorting helpers # # Sorting helpers #
################### ###################
def _remove_older_point_releases(distro_list): def _sort(tosort):
ret = distro_list[:]
def _get_minor_version(osobj):
return int(osobj.name.rsplit(".", 1)[-1])
def _find_latest(prefix):
"""
Given a prefix like 'rhel4', find the latest 'rhel4.X',
and remove the rest from the os list
"""
latest_os = None
first_id = None
for osobj in ret[:]:
if not re.match("%s\.\d+" % prefix, osobj.name):
continue
if first_id is None:
first_id = ret.index(osobj)
ret.remove(osobj)
if (latest_os and
_get_minor_version(latest_os) > _get_minor_version(osobj)):
continue
latest_os = osobj
if latest_os:
ret.insert(first_id, latest_os)
_find_latest("rhel4")
_find_latest("rhel5")
_find_latest("rhel6")
_find_latest("rhel7")
_find_latest("freebsd9")
_find_latest("freebsd10")
_find_latest("freebsd11")
_find_latest("centos6")
_find_latest("centos7")
return ret
def _sort(tosort, sortpref=None, limit_point_releases=False):
sortby_mappings = {} sortby_mappings = {}
distro_mappings = {} distro_mappings = {}
retlist = [] retlist = []
sortpref = sortpref or []
for key, osinfo in tosort.items(): for key, osinfo in tosort.items():
# Libosinfo has some duplicate version numbers here, so append .1 # Libosinfo has some duplicate version numbers here, so append .1
@ -90,15 +50,8 @@ def _sort(tosort, sortpref=None, limit_point_releases=False):
distro_list.sort() distro_list.sort()
distro_list.reverse() distro_list.reverse()
# Move the sortpref values to the front of the list
sorted_distro_list = list(distro_mappings.keys()) sorted_distro_list = list(distro_mappings.keys())
sorted_distro_list.sort() sorted_distro_list.sort()
sortpref.reverse()
for prefer in sortpref:
if prefer not in sorted_distro_list:
continue
sorted_distro_list.remove(prefer)
sorted_distro_list.insert(0, prefer)
# Build the final list of sorted os objects # Build the final list of sorted os objects
for distro in sorted_distro_list: for distro in sorted_distro_list:
@ -107,10 +60,6 @@ def _sort(tosort, sortpref=None, limit_point_releases=False):
orig_key = sortby_mappings[key] orig_key = sortby_mappings[key]
retlist.append(tosort[orig_key]) retlist.append(tosort[orig_key])
# Filter out older point releases
if limit_point_releases:
retlist = _remove_older_point_releases(retlist)
return retlist return retlist
@ -236,25 +185,16 @@ class _OSDB(object):
"solaris", "other", "generic"] "solaris", "other", "generic"]
return approved_types return approved_types
def list_os(self, typename=None, only_supported=False, sortpref=None): def list_os(self):
""" """
List all OSes in the DB List all OSes in the DB
:param typename: Only list OSes of this type
:param only_supported: Only list OSses where self.supported == True
:param sortpref: Sort these OSes at the front of the list
""" """
sortmap = {} sortmap = {}
for name, osobj in self._all_variants.items(): for name, osobj in self._all_variants.items():
if typename and typename != osobj.get_typename():
continue
if only_supported and not osobj.get_supported():
continue
sortmap[name] = osobj sortmap[name] = osobj
return _sort(sortmap, sortpref=sortpref, return _sort(sortmap)
limit_point_releases=only_supported)
def latest_regex(self, regex): def latest_regex(self, regex):
""" """
@ -282,6 +222,24 @@ class _OsVariant(object):
self.label = self._os and self._os.get_name() or "Generic" self.label = self._os and self._os.get_name() or "Generic"
self.codename = self._os and self._os.get_codename() or "" self.codename = self._os and self._os.get_codename() or ""
self.distro = self._os and self._os.get_distro() or "" self.distro = self._os and self._os.get_distro() or ""
self.eol = False
eol = self._os and self._os.get_eol_date() or None
rel = self._os and self._os.get_release_date() or None
# End of life if an EOL date is present and has past,
# or if the release date is present and was 5 years or more
if eol is not None:
now = GLib.Date()
now.set_time_t(time.time())
if eol.compare(now) < 0:
self.eol = True
elif rel is not None:
then = GLib.Date()
then.set_time_t(time.time())
then.subtract_years(5)
if rel.compare(then) < 0:
self.eol = True
self.sortby = self._get_sortby() self.sortby = self._get_sortby()
self.urldistro = self._get_urldistro() self.urldistro = self._get_urldistro()