src/gui/modules/beadmin.py
author Padraig O'Briain <padraig.obriain@sun.com>
Thu, 17 Dec 2009 08:27:14 +0000
changeset 1587 e46d4e51d02f
parent 1516 8c950a3b4171
child 1673 6897b80bca9e
permissions -rw-r--r--
11243 PM Help needs update with new Help tags

#!/usr/bin/python
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#

import sys
import os
import pango
import time
import datetime
import locale
import pkg.pkgsubprocess as subprocess
from threading import Thread

try:
        import gobject
        gobject.threads_init()
        import gtk
        import gtk.glade
        import pygtk
        pygtk.require("2.0")
except ImportError:
        sys.exit(1)
import pkg.gui.misc as gui_misc

nobe = False

try:
        import libbe as be
except ImportError:
        # All actions are disabled when libbe can't be imported. 
        nobe = True
import pkg.misc

#BE_LIST
(
BE_ID,
BE_MARKED,
BE_NAME,
BE_ORIG_NAME,
BE_DATE_TIME,
BE_CURRENT_PIXBUF,
BE_ACTIVE_DEFAULT,
BE_SIZE,
BE_EDITABLE
) = range(9)

class Beadmin:
        def __init__(self, parent):
                self.parent = parent

                if nobe:
                        msg = _("The <b>libbe</b> library was not "
                            "found on your system."
                            "\nAll functions for managing Boot Environments are disabled")
                        msgbox = gtk.MessageDialog(
                            buttons = gtk.BUTTONS_CLOSE,
                            flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_INFO,
                            message_format = None)
                        msgbox.set_markup(msg)
                        msgbox.set_title(_("BE management"))
                        msgbox.run()
                        msgbox.destroy()
                        return

                self.be_list = \
                    gtk.ListStore(
                        gobject.TYPE_INT,         # BE_ID
                        gobject.TYPE_BOOLEAN,     # BE_MARKED
                        gobject.TYPE_STRING,      # BE_NAME
                        gobject.TYPE_STRING,      # BE_ORIG_NAME
                        gobject.TYPE_STRING,      # BE_DATE_TIME
                        gtk.gdk.Pixbuf,           # BE_CURRENT_PIXBUF
                        gobject.TYPE_BOOLEAN,     # BE_ACTIVE_DEFAULT
                        gobject.TYPE_STRING,      # BE_SIZE
                        gobject.TYPE_BOOLEAN,     # BE_EDITABLE
                        )
                self.progress_stop_thread = False
                self.initial_active = 0
                self.initial_default = 0
                gladefile = os.path.join(self.parent.application_dir,
                    "usr/share/package-manager/packagemanager.glade")
                w_tree_beadmin = gtk.glade.XML(gladefile, "beadmin")
                w_tree_progress = gtk.glade.XML(gladefile, "progressdialog")
                # Dialog reused in the repository.py
                w_tree_beconfirmation = gtk.glade.XML(gladefile,
                    "confirmationdialog")
                self.w_beadmin_dialog = w_tree_beadmin.get_widget("beadmin")
                self.w_beadmin_dialog.set_icon(self.parent.window_icon)
                self.w_be_treeview = w_tree_beadmin.get_widget("betreeview")
                self.w_cancel_button = w_tree_beadmin.get_widget("cancelbebutton")
                self.w_ok_button = w_tree_beadmin.get_widget("okbebutton")
                w_active_gtkimage = w_tree_beadmin.get_widget("activebeimage")
                self.w_progress_dialog = w_tree_progress.get_widget("progressdialog")
                self.w_progress_dialog.connect('delete-event', lambda stub1, stub2: True)
                self.w_progress_dialog.set_icon(self.parent.window_icon)
                self.w_progressinfo_label = w_tree_progress.get_widget("progressinfo")
                progress_button = w_tree_progress.get_widget("progresscancel")
                self.w_progressbar = w_tree_progress.get_widget("progressbar")
                self.w_beconfirmation_dialog =  \
                    w_tree_beconfirmation.get_widget("confirmationdialog")
                self.w_beconfirmation_dialog.set_icon(self.parent.window_icon)
                self.w_beconfirmation_textview = \
                    w_tree_beconfirmation.get_widget("confirmtext")
                self.w_okbe_button = w_tree_beconfirmation.get_widget("ok_conf")
                self.w_ok_button.set_sensitive(False)
                progress_button.hide()
                self.w_progressbar.set_pulse_step(0.1)
                self.list_filter = self.be_list.filter_new()
                self.w_be_treeview.set_model(self.list_filter)
                self.__init_tree_views()
                self.active_image = gui_misc.get_icon(
                    self.parent.icon_theme, "status_checkmark")
                w_active_gtkimage.set_from_pixbuf(self.active_image)

                bebuffer = self.w_beconfirmation_textview.get_buffer()
                bebuffer.create_tag("bold", weight=pango.WEIGHT_BOLD)

                try:
                        dic = \
                            {
                                "on_cancel_be_clicked": \
                                    self.__on_cancel_be_clicked,
                                "on_ok_be_clicked": \
                                    self.__on_ok_be_clicked,
                                "on_help_bebutton_clicked": \
                                    self.__on_help_bebutton_clicked,
                            }
                        dic_conf = \
                            {
                                "on_cancel_conf_clicked": \
                                    self.__on_cancel_be_conf_clicked,
                                "on_ok_conf_clicked": \
                                    self.__on_ok_be_conf_clicked,
                                "on_confirmationdialog_delete_event": \
                                    self.__on_beconfirmationdialog_delete_event,
                            }            
                        w_tree_beadmin.signal_autoconnect(dic)
                        w_tree_beconfirmation.signal_autoconnect(dic_conf)
                except AttributeError, error:
                        print _("GUI will not respond to any event! %s. "
                            "Check beadmin.py signals") \
                            % error
                Thread(target = self.__progress_pulse).start()
                Thread(target = self.__prepare_beadmin_list).start()
                sel = self.w_be_treeview.get_selection()
                self.w_cancel_button.grab_focus()
                sel.set_mode(gtk.SELECTION_SINGLE)
                self.w_beconfirmation_dialog.set_title(
                    _("Boot Environment Confirmation"))
                self.w_beadmin_dialog.show_all()
                self.w_progress_dialog.set_title(
                    _("Loading Boot Environment Information"))
                self.w_progressinfo_label.set_text(
                    _("Fetching BE entries..."))
                self.w_progress_dialog.show()

        def __progress_pulse(self):
                while not self.progress_stop_thread:
                        gobject.idle_add(self.w_progressbar.pulse)
                        time.sleep(0.1)
                gobject.idle_add(self.w_progress_dialog.hide)

        def __prepare_beadmin_list(self):
                be_list = be.beList()
                gobject.idle_add(self.__create_view_with_be, be_list)
                self.progress_stop_thread = True
                return

        def __init_tree_views(self):
                model = self.w_be_treeview.get_model()

                column = gtk.TreeViewColumn()
                column.set_title("")
                render_pixbuf = gtk.CellRendererPixbuf()
                column.pack_start(render_pixbuf, expand = True)
                column.add_attribute(render_pixbuf, "pixbuf", BE_CURRENT_PIXBUF)
                self.w_be_treeview.append_column(column)

                name_renderer = gtk.CellRendererText()
                name_renderer.connect('edited', self.__be_name_edited, model)
                column = gtk.TreeViewColumn(_("Boot Environment"),
                    name_renderer, text = BE_NAME)
                column.set_cell_data_func(name_renderer, self.__cell_data_function, None)
                column.set_expand(True)
                if "beVerifyBEName" in be.__dict__:
                        column.add_attribute(name_renderer, "editable", 
                            BE_EDITABLE)
                self.w_be_treeview.append_column(column)
                
                datetime_renderer = gtk.CellRendererText()
                datetime_renderer.set_property('xalign', 0.0)
                column = gtk.TreeViewColumn(_("Created"), datetime_renderer,
                    text = BE_DATE_TIME)
                column.set_cell_data_func(datetime_renderer,
                    self.__cell_data_function, None)
                column.set_expand(True)
                self.w_be_treeview.append_column(column)

                size_renderer = gtk.CellRendererText()
                size_renderer.set_property('xalign', 1.0)
                column = gtk.TreeViewColumn(_("Size"), size_renderer,
                    text = BE_SIZE)
                column.set_cell_data_func(size_renderer, self.__cell_data_function, None)
                column.set_expand(False)
                self.w_be_treeview.append_column(column)
              
                radio_renderer = gtk.CellRendererToggle()
                radio_renderer.connect('toggled', self.__active_pane_default, model)
                column = gtk.TreeViewColumn(_("Active on Reboot"),
                    radio_renderer, active = BE_ACTIVE_DEFAULT)
                radio_renderer.set_property("activatable", True)
                radio_renderer.set_property("radio", True)
                column.set_cell_data_func(radio_renderer,
                    self.__cell_data_default_function, None)
                column.set_expand(False)
                self.w_be_treeview.append_column(column)

                toggle_renderer = gtk.CellRendererToggle()
                toggle_renderer.connect('toggled', self.__active_pane_toggle, model)
                column = gtk.TreeViewColumn(_("Delete"), toggle_renderer,
                    active = BE_MARKED)
                toggle_renderer.set_property("activatable", True)
                column.set_cell_data_func(toggle_renderer,
                    self.__cell_data_delete_function, None)
                column.set_expand(False)
                self.w_be_treeview.append_column(column)

        def __on_help_bebutton_clicked(self, widget):
                if self.parent != None:
                        gui_misc.display_help("manage-be")
                else:
                        gui_misc.display_help()
                
        def __on_ok_be_clicked(self, widget):
                self.w_progress_dialog.set_title(_("Applying changes"))
                self.w_progressinfo_label.set_text(
                    _("Applying changes, please wait ..."))
                if self.w_ok_button.get_property('sensitive') == 0:
                        self.progress_stop_thread = True
                        self.__on_beadmin_delete_event(None, None)
                        return
                Thread(target = self.__activate).start()
                
        def __on_cancel_be_clicked(self, widget):
                self.__on_beadmin_delete_event(None, None)
                return False

        def __on_beconfirmationdialog_delete_event(self, widget, event):
                self.__on_cancel_be_conf_clicked(widget)
                return True

        def __on_cancel_be_conf_clicked(self, widget):
                self.w_beconfirmation_dialog.hide()

        def __on_ok_be_conf_clicked(self, widget):
                self.w_beconfirmation_dialog.hide()
                self.progress_stop_thread = False
                Thread(target = self.__on_progressdialog_progress).start()
                Thread(target = self.__delete_activate_be).start()
                
        def __on_beadmin_delete_event(self, widget, event, stub=None):
                self.w_beadmin_dialog.destroy()
                return True

        def __activate(self):
                active_text = _("Active on reboot\n")
                delete_text = _("Delete\n")
                rename_text = _("Rename\n")
                active = ""
                delete = ""
                rename = {}
                for row in self.be_list:

                        if row[BE_MARKED]:
                                delete += "\t" + row[BE_NAME] + "\n"
                        if row[BE_ACTIVE_DEFAULT] == True and row[BE_ID] != \
                            self.initial_default:
                                active += "\t" + row[BE_NAME] + "\n"
                        if row[BE_NAME] != row[BE_ORIG_NAME]:
                                rename[row[BE_ORIG_NAME]] = row[BE_NAME]
                textbuf = self.w_beconfirmation_textview.get_buffer()
                textbuf.set_text("")
                textiter = textbuf.get_end_iter()
                if len(active) > 0:
                        textbuf.insert_with_tags_by_name(textiter,
                            active_text, "bold")
                        textbuf.insert_with_tags_by_name(textiter,
                            active)
                if len(delete) > 0:
                        if len(active) > 0:
                                textbuf.insert_with_tags_by_name(textiter,
                                    "\n")                                
                        textbuf.insert_with_tags_by_name(textiter,
                            delete_text, "bold")
                        textbuf.insert_with_tags_by_name(textiter,
                            delete)
                if len(rename) > 0:
                        if len(delete) > 0 or len(active) > 0:
                                textbuf.insert_with_tags_by_name(textiter,
                                    "\n")                                
                        textbuf.insert_with_tags_by_name(textiter,
                            rename_text, "bold")
                        for orig in rename:
                                textbuf.insert_with_tags_by_name(textiter,
                                    "\t")
                                textbuf.insert_with_tags_by_name(textiter,
                                    orig)
                                textbuf.insert_with_tags_by_name(textiter,
                                    _(" to "), "bold")
                                textbuf.insert_with_tags_by_name(textiter,
                                    rename.get(orig) + "\n")
                self.w_okbe_button.grab_focus()
                gobject.idle_add(self.w_beconfirmation_dialog.show)
                self.progress_stop_thread = True                

        def __on_progressdialog_progress(self):
                # This needs to be run in gobject.idle_add, otherwise we will get
                # Xlib: unexpected async reply (sequence 0x2db0)!
                gobject.idle_add(self.w_progress_dialog.show)
                while not self.progress_stop_thread:
                        gobject.idle_add(self.w_progressbar.pulse)
                        time.sleep(0.1)
                gobject.idle_add(self.w_progress_dialog.hide)

        def __delete_activate_be(self):
                not_deleted = []
                not_default = None
                not_renamed = {}
		# The while gtk.events_pending():
                #        gtk.main_iteration(False)
		# Is not working if we are calling libbe, so it is required
		# To have sleep in few places in this function
                # Remove
                for row in self.be_list:
                        if row[BE_MARKED]:
                                time.sleep(0.1)
                                result = self.__destroy_be(row[BE_NAME])
                                if result != 0:
                                        not_deleted.append(row[BE_NAME])
                # Rename
                for row in self.be_list:
                        if row[BE_NAME] != row[BE_ORIG_NAME]:
                                time.sleep(0.1)
                                result = self.__rename_be(row[BE_ORIG_NAME],
                                    row[BE_NAME])
                                if result != 0:
                                        not_renamed[row[BE_ORIG_NAME]] = row[BE_NAME]
                # Set active
                for row in self.be_list:
                        if row[BE_ACTIVE_DEFAULT] == True and row[BE_ID] != \
                            self.initial_default:
                                time.sleep(0.1)
                                result = self.__set_default_be(row[BE_NAME])
                                if result != 0:
                                        not_default = row[BE_NAME]
                if len(not_deleted) == 0 and not_default == None \
                    and len(not_renamed) == 0:
                        self.progress_stop_thread = True
                else:
                        self.progress_stop_thread = True
                        msg = ""
                        if not_default:
                                msg += _("<b>Couldn't change Active "
                                    "Boot Environment to:</b>\n") + not_default
                        if len(not_deleted) > 0:
                                if not_default:
                                        msg += "\n\n"
                                msg += _("<b>Couldn't delete Boot "
                                    "Environments:</b>\n")
                                for row in not_deleted:
                                        msg += row + "\n"
                        if len(not_renamed) > 0:
                                if not_default or len(not_deleted):
                                        msg += "\n"
                                msg += _("<b>Couldn't rename Boot "
                                    "Environments:</b>\n")
                                for orig in not_renamed:
                                        msg += _("%s <b>to</b> %s\n") % (orig, \
                                            not_renamed.get(orig))
                        gobject.idle_add(self.__error_occurred, msg)
                        return
                gobject.idle_add(self.__on_cancel_be_clicked, None)
                                
        @staticmethod
        def __rename_cell(model, itr, new_name):
                model.set_value(itr, BE_NAME, new_name)

        @staticmethod
        def __rename_be(orig_name, new_name):
                return be.beRename(orig_name, new_name)

        def __error_occurred(self, error_msg, reset=True):
                gui_misc.error_occurred(self.w_beadmin_dialog,
                    error_msg,
                    _("BE error"),
                    gtk.MESSAGE_ERROR,
                    True)
                if reset:
                        self.__on_reset_be()

        def __on_reset_be(self):
                self.be_list.clear()
                self.w_progress_dialog.show()
                self.progress_stop_thread = False
                Thread(target = self.__progress_pulse).start()
                Thread(target = self.__prepare_beadmin_list).start()
                self.w_ok_button.set_sensitive(False)

        def __active_pane_toggle(self, cell, filtered_path, filtered_model):
                model = filtered_model.get_model()
                path = filtered_model.convert_path_to_child_path(filtered_path)
                itr = model.get_iter(path)
                if itr:
                        modified = model.get_value(itr, BE_MARKED)
                        # Do not allow to set active if selected for removal
                        model.set_value(itr, BE_MARKED, not modified)
                        # Do not allow to rename if we are removing be.
                        model.set_value(itr, BE_EDITABLE, modified)
                self.__enable_disable_ok()
                
        def __enable_disable_ok(self):
                for row in self.be_list:
                        if row[BE_MARKED] == True:
                                self.w_ok_button.set_sensitive(True)
                                return
                        if row[BE_ID] == self.initial_default:
                                if row[BE_ACTIVE_DEFAULT] == False:
                                        self.w_ok_button.set_sensitive(True)
                                        return
                        if row[BE_NAME] != row[BE_ORIG_NAME]:
                                self.w_ok_button.set_sensitive(True)
                                return
                self.w_ok_button.set_sensitive(False)
                return

        def __be_name_edited(self, cell, filtered_path, new_name, filtered_model):
                model = filtered_model.get_model()
                path = filtered_model.convert_path_to_child_path(filtered_path)
                itr = model.get_iter(path)
                if itr:
                        if model.get_value(itr, BE_NAME) == new_name:
                                return
                        if self.__verify_be_name(new_name) != 0:
                                return
                        self.__rename_cell(model, itr, new_name)
                        self.__enable_disable_ok()                
                        return

        #TBD: Notify user if name clash using same logic as Repo Add and warning text
        def __verify_be_name(self, new_name):
                if be.beVerifyBEName(new_name) != 0:
                        return -1
                for row in self.be_list:
                        if new_name == row[BE_NAME]:
                                return -1
                return 0

        def __active_pane_default(self, cell, filtered_path, filtered_model):
                model = filtered_model.get_model()
                path = filtered_model.convert_path_to_child_path(filtered_path)
                for row in model:
                        row[BE_ACTIVE_DEFAULT] = False
                itr = model.get_iter(path)
                if itr:
                        modified = model.get_value(itr, BE_ACTIVE_DEFAULT)
                        model.set_value(itr, BE_ACTIVE_DEFAULT, not modified)
                        self.__enable_disable_ok()

        def __create_view_with_be(self, be_list):
                dates = None
                i = 0
                j = 0
                error_code = None
                be_list_loop = None
                if len(be_list) > 1 and type(be_list[0]) == type(-1):
                        error_code = be_list[0]
                if error_code != None and error_code == 0:
                        be_list_loop = be_list[1]
                elif error_code != None and error_code != 0:
                        msg = _("The <b>libbe</b> library couldn't "
                            "prepare list of Boot Environments."
                            "\nAll functions for managing Boot Environments are disabled")
                        self.__error_occurred(msg, False)
                        return
                else:
                        be_list_loop = be_list

                for bee in be_list_loop:
                        if bee.get("orig_be_name"):
                                name = bee.get("orig_be_name")
                                active = bee.get("active")
                                active_boot = bee.get("active_boot")
                                be_size = bee.get("space_used")
                                be_date = bee.get("date")
                                converted_size = \
                                    self.__convert_size_of_be_to_string(be_size)
                                active_img = None
                                if not be_date and j == 0:
                                        dates = self.__get_dates_of_creation(be_list_loop)
                                if dates:
                                        try:
                                                date_time = repr(dates[i])[1:-3]
                                                date_tmp = time.strptime(date_time, \
                                                    "%a %b %d %H:%M %Y")
                                                date_tmp2 = \
                                                        datetime.datetime(*date_tmp[0:5])
                                                try:
                                                        date_format = \
                                                        unicode(
                                                            _("%m/%d/%y %H:%M"),
                                                            "utf-8").encode(
                                                            locale.getpreferredencoding())
                                                except (UnicodeError, LookupError,
                                                    locale.Error):
                                                        date_format = "%F %H:%M"
                                                date_time = \
                                                    date_tmp2.strftime(date_format)
                                                i += 1
                                        except (NameError, ValueError, TypeError):
                                                date_time = None
                                else:
                                        date_tmp = time.localtime(be_date)
                                        try:
                                                date_format = \
                                                    unicode(
                                                        _("%m/%d/%y %H:%M"),
                                                        "utf-8").encode(
                                                        locale.getpreferredencoding())
                                        except (UnicodeError, LookupError, locale.Error):
                                                date_format = "%F %H:%M"
                                        date_time = \
                                            time.strftime(date_format, date_tmp)
                                if active:
                                        active_img = self.active_image
                                        self.initial_active = j
                                if active_boot:
                                        self.initial_default = j
                                if date_time != None:
                                        try:
                                                date_time = unicode(date_time,
                                                locale.getpreferredencoding()).encode(
                                                        "utf-8")
                                        except (UnicodeError, LookupError, locale.Error):
                                                pass 
                                self.be_list.insert(j, [j, False,
                                    name, name,
                                    date_time, active_img,
                                    active_boot, converted_size, active_img == None])
                                j += 1
                self.w_be_treeview.set_cursor(self.initial_active, None,
                    start_editing=True)
                self.w_be_treeview.scroll_to_cell(self.initial_active)

        @staticmethod
        def __destroy_be(be_name):
                return be.beDestroy(be_name, 1, True)

        @staticmethod
        def __set_default_be(be_name):
                return be.beActivate(be_name)

        def __cell_data_default_function(self, column, renderer, model, itr, data):
                if itr:
                        if model.get_value(itr, BE_MARKED):
                                self.__set_renderer_active(renderer, False)
                        else:
                                self.__set_renderer_active(renderer, True)
                                
        def __cell_data_delete_function(self, column, renderer, model, itr, data):
                if itr:
                        if model.get_value(itr, BE_ACTIVE_DEFAULT) or \
                            (self.initial_active == model.get_value(itr, BE_ID)) or \
                            (model.get_value(itr, BE_NAME) !=
                            model.get_value(itr, BE_ORIG_NAME)):
                                self.__set_renderer_active(renderer, False)
                        else:
                                self.__set_renderer_active(renderer, True)

        @staticmethod
        def __set_renderer_active(renderer, active):
                if active:
                        renderer.set_property("sensitive", True)
                        renderer.set_property("mode", gtk.CELL_RENDERER_MODE_ACTIVATABLE)
                else:
                        renderer.set_property("sensitive", False)
                        renderer.set_property("mode", gtk.CELL_RENDERER_MODE_INERT)

        @staticmethod
        def __get_dates_of_creation(be_list):
                #zfs list -H -o creation rpool/ROOT/opensolaris-1
                cmd = [ "/sbin/zfs", "list", "-H", "-o","creation" ]
                for bee in be_list:
                        if bee.get("orig_be_name"):
                                name = bee.get("orig_be_name")
                                pool = bee.get("orig_be_pool")
                                cmd += [pool+"/ROOT/"+name]
                if len(cmd) <= 5:
                        return None
                list_of_dates = []
                try:
                        proc = subprocess.Popen(cmd, stdout = subprocess.PIPE,
                            stderr = subprocess.PIPE,)
                        line_out = proc.stdout.readline()
                        while line_out:
                                list_of_dates.append(line_out)
                                line_out =  proc.stdout.readline()
                except OSError:
                        return list_of_dates
                return list_of_dates

        @staticmethod
        def __convert_size_of_be_to_string(be_size):
                if not be_size:
                        be_size = 0
                return pkg.misc.bytes_to_str(be_size)

        @staticmethod
        def __cell_data_function(column, renderer, model, itr, data):
                if itr:
                        if model.get_value(itr, BE_CURRENT_PIXBUF):
                                renderer.set_property("weight", pango.WEIGHT_BOLD)
                        else:
                                renderer.set_property("weight", pango.WEIGHT_NORMAL)