1 #!/usr/bin/python2.4
2 #
3 # CDDL HEADER START
4 #
5 # The contents of this file are subject to the terms of the
6 # Common Development and Distribution License (the "License").
7 # You may not use this file except in compliance with the License.
8 #
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 # or http://www.opensolaris.org/os/licensing.
11 # See the License for the specific language governing permissions
12 # and limitations under the License.
13 #
14 # When distributing Covered Code, include this CDDL HEADER in each
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 # If applicable, add the following below this CDDL HEADER, with the
17 # fields enclosed by brackets "[]" replaced with your own identifying
18 # information: Portions Copyright [yyyy] [name of copyright owner]
19 #
20 # CDDL HEADER END
21 #
22 # Copyright 2009 Sun Microsystems, Inc. All rights reserved.
23 # Use is subject to license terms.
24 #
25
26 import sys
27 import os
28 import pango
29 import time
30 import datetime
31 import locale
32 import pkg.pkgsubprocess as subprocess
33 from threading import Thread
34
35 try:
36 import gobject
37 gobject.threads_init()
38 import gnome
39 import gtk
40 import gtk.glade
41 import pygtk
42 pygtk.require("2.0")
43 except ImportError:
44 sys.exit(1)
45 import pkg.gui.misc as gui_misc
46
47 nobe = False
48
49 try:
50 import libbe as be
51 except ImportError:
52 # All actions are disabled when libbe can't be imported.
53 nobe = True
54 import pkg.misc
55
56 #BE_LIST
57 (
58 BE_ID,
59 BE_MARKED,
60 BE_NAME,
61 BE_ORIG_NAME,
62 BE_DATE_TIME,
63 BE_CURRENT_PIXBUF,
64 BE_ACTIVE_DEFAULT,
65 BE_SIZE,
66 BE_EDITABLE
67 ) = range(9)
68
69 class Beadmin:
70 def __init__(self, parent):
71 self.parent = parent
72
73 if nobe:
74 msg = _("The <b>libbe</b> library was not "
75 "found on your system."
76 "\nAll functions for managing Boot Environments are disabled")
77 msgbox = gtk.MessageDialog(
78 buttons = gtk.BUTTONS_CLOSE,
79 flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_INFO,
80 message_format = None)
81 msgbox.set_markup(msg)
82 msgbox.set_title(_("BE management"))
83 msgbox.run()
84 msgbox.destroy()
85 return
86
87 self.be_list = \
88 gtk.ListStore(
89 gobject.TYPE_INT, # BE_ID
90 gobject.TYPE_BOOLEAN, # BE_MARKED
91 gobject.TYPE_STRING, # BE_NAME
92 gobject.TYPE_STRING, # BE_ORIG_NAME
93 gobject.TYPE_STRING, # BE_DATE_TIME
94 gtk.gdk.Pixbuf, # BE_CURRENT_PIXBUF
95 gobject.TYPE_BOOLEAN, # BE_ACTIVE_DEFAULT
96 gobject.TYPE_STRING, # BE_SIZE
97 gobject.TYPE_BOOLEAN, # BE_EDITABLE
98 )
99 self.progress_stop_thread = False
100 self.initial_active = 0
101 self.initial_default = 0
102 gladefile = os.path.join(self.parent.application_dir,
103 "usr/share/package-manager/packagemanager.glade")
104 w_tree_beadmin = gtk.glade.XML(gladefile, "beadmin")
105 w_tree_progress = gtk.glade.XML(gladefile, "progressdialog")
106 w_tree_beconfirmation = gtk.glade.XML(gladefile,
107 "beconfirmationdialog")
108 self.w_beadmin_dialog = w_tree_beadmin.get_widget("beadmin")
109 self.w_be_treeview = w_tree_beadmin.get_widget("betreeview")
110 self.w_cancel_button = w_tree_beadmin.get_widget("cancelbebutton")
111 self.w_ok_button = w_tree_beadmin.get_widget("okbebutton")
112 w_active_gtkimage = w_tree_beadmin.get_widget("activebeimage")
113 self.w_progress_dialog = w_tree_progress.get_widget("progressdialog")
114 self.w_progress_dialog.connect('delete-event', lambda stub1, stub2: True)
115 self.w_progressinfo_label = w_tree_progress.get_widget("progressinfo")
116 progress_button = w_tree_progress.get_widget("progresscancel")
117 self.w_progressbar = w_tree_progress.get_widget("progressbar")
118 self.w_beconfirmation_dialog = \
119 w_tree_beconfirmation.get_widget("beconfirmationdialog")
120 self.w_beconfirmation_textview = \
121 w_tree_beconfirmation.get_widget("beconfirmtext")
122 self.w_cancelbe_button = w_tree_beconfirmation.get_widget("cancel_be")
123 self.w_ok_button.set_sensitive(False)
124 progress_button.hide()
125 self.w_progressbar.set_pulse_step(0.1)
126 self.list_filter = self.be_list.filter_new()
127 self.w_be_treeview.set_model(self.list_filter)
128 self.__init_tree_views()
129 self.active_image = gui_misc.get_icon(
130 self.parent.icon_theme, "status_checkmark")
131 w_active_gtkimage.set_from_pixbuf(self.active_image)
132
133 bebuffer = self.w_beconfirmation_textview.get_buffer()
134 bebuffer.create_tag("bold", weight=pango.WEIGHT_BOLD)
135
136 try:
137 dic = \
138 {
139 "on_cancel_be_clicked": \
140 self.__on_cancel_be_clicked,
141 "on_ok_be_clicked": \
142 self.__on_ok_be_clicked,
143 "on_help_bebutton_clicked": \
144 self.__on_help_bebutton_clicked,
145 }
146 dic_conf = \
147 {
148 "on_cancel_be_conf_clicked": \
149 self.__on_cancel_be_conf_clicked,
150 "on_ok_be_conf_clicked": \
151 self.__on_ok_be_conf_clicked,
152 "on_beconfirmationdialog_delete_event": \
153 self.__on_beconfirmationdialog_delete_event,
154 }
155 w_tree_beadmin.signal_autoconnect(dic)
156 w_tree_beconfirmation.signal_autoconnect(dic_conf)
157 except AttributeError, error:
158 print _("GUI will not respond to any event! %s. "
159 "Check beadmin.py signals") \
160 % error
161 Thread(target = self.__progress_pulse).start()
162 Thread(target = self.__prepare_beadmin_list).start()
163 sel = self.w_be_treeview.get_selection()
164 self.w_cancel_button.grab_focus()
165 sel.set_mode(gtk.SELECTION_SINGLE)
166 self.w_beadmin_dialog.show_all()
167 self.w_progress_dialog.set_title(
168 _("Loading Boot Environment Information"))
169 self.w_progressinfo_label.set_text(
170 _("Fetching BE entries..."))
171 self.w_progress_dialog.show()
172
173 def __progress_pulse(self):
174 while not self.progress_stop_thread:
175 gobject.idle_add(self.w_progressbar.pulse)
176 time.sleep(0.1)
177 gobject.idle_add(self.w_progress_dialog.hide)
178
179 def __prepare_beadmin_list(self):
180 be_list = be.beList()
181 gobject.idle_add(self.__create_view_with_be, be_list)
182 self.progress_stop_thread = True
183 return
184
185 def __init_tree_views(self):
186 model = self.w_be_treeview.get_model()
187
188 column = gtk.TreeViewColumn()
189 column.set_title("")
190 render_pixbuf = gtk.CellRendererPixbuf()
191 column.pack_start(render_pixbuf, expand = True)
192 column.add_attribute(render_pixbuf, "pixbuf", BE_CURRENT_PIXBUF)
193 self.w_be_treeview.append_column(column)
194
195 name_renderer = gtk.CellRendererText()
196 name_renderer.connect('edited', self.__be_name_edited, model)
197 column = gtk.TreeViewColumn(_("Boot Environment"),
198 name_renderer, text = BE_NAME)
199 column.set_cell_data_func(name_renderer, self.__cell_data_function, None)
200 column.set_expand(True)
201 if "beVerifyBEName" in be.__dict__:
202 column.add_attribute(name_renderer, "editable",
203 BE_EDITABLE)
204 self.w_be_treeview.append_column(column)
205
206 datetime_renderer = gtk.CellRendererText()
207 datetime_renderer.set_property('xalign', 0.0)
208 column = gtk.TreeViewColumn(_("Created"), datetime_renderer,
209 text = BE_DATE_TIME)
210 column.set_cell_data_func(datetime_renderer,
211 self.__cell_data_function, None)
212 column.set_expand(True)
213 self.w_be_treeview.append_column(column)
214
215 size_renderer = gtk.CellRendererText()
216 size_renderer.set_property('xalign', 1.0)
217 column = gtk.TreeViewColumn(_("Size"), size_renderer,
218 text = BE_SIZE)
219 column.set_cell_data_func(size_renderer, self.__cell_data_function, None)
220 column.set_expand(False)
221 self.w_be_treeview.append_column(column)
222
223 radio_renderer = gtk.CellRendererToggle()
224 radio_renderer.connect('toggled', self.__active_pane_default, model)
225 column = gtk.TreeViewColumn(_("Active on Reboot"),
226 radio_renderer, active = BE_ACTIVE_DEFAULT)
227 radio_renderer.set_property("activatable", True)
228 radio_renderer.set_property("radio", True)
229 column.set_cell_data_func(radio_renderer,
230 self.__cell_data_default_function, None)
231 column.set_expand(False)
232 self.w_be_treeview.append_column(column)
233
234 toggle_renderer = gtk.CellRendererToggle()
235 toggle_renderer.connect('toggled', self.__active_pane_toggle, model)
236 column = gtk.TreeViewColumn(_("Delete"), toggle_renderer,
237 active = BE_MARKED)
238 toggle_renderer.set_property("activatable", True)
239 column.set_cell_data_func(toggle_renderer,
240 self.__cell_data_delete_function, None)
241 column.set_expand(False)
242 self.w_be_treeview.append_column(column)
243
244 def __on_help_bebutton_clicked(self, widget):
245 if self.parent != None:
246 gui_misc.display_help(self.parent.application_dir, "manage_be")
247 else:
248 gui_misc.display_help()
249
250 def __on_ok_be_clicked(self, widget):
251 self.w_progress_dialog.set_title(_("Applying changes"))
252 self.w_progressinfo_label.set_text(
253 _("Applying changes, please wait ..."))
254 if self.w_ok_button.get_property('sensitive') == 0:
255 self.progress_stop_thread = True
256 self.__on_beadmin_delete_event(None, None)
257 return
258 Thread(target = self.__activate).start()
259
260 def __on_cancel_be_clicked(self, widget):
261 self.__on_beadmin_delete_event(None, None)
262 return False
263
264 def __on_beconfirmationdialog_delete_event(self, widget, event):
265 self.__on_cancel_be_conf_clicked(widget)
266 return True
267
268 def __on_cancel_be_conf_clicked(self, widget):
269 self.w_beconfirmation_dialog.hide()
270
271 def __on_ok_be_conf_clicked(self, widget):
272 self.w_beconfirmation_dialog.hide()
273 self.progress_stop_thread = False
274 Thread(target = self.__on_progressdialog_progress).start()
275 Thread(target = self.__delete_activate_be).start()
276
277 def __on_beadmin_delete_event(self, widget, event, stub=None):
278 self.w_beadmin_dialog.destroy()
279 return True
280
281 def __activate(self):
282 active_text = _("Active on reboot:\n")
283 delete_text = _("Delete boot environments:\n")
284 rename_text = _("Rename boot environments:\n")
285 active = ""
286 delete = ""
287 rename = {}
288 for row in self.be_list:
289
290 if row[BE_MARKED]:
291 delete += row[BE_NAME] + "\n"
292 if row[BE_ACTIVE_DEFAULT] == True and row[BE_ID] != \
293 self.initial_default:
294 active += row[BE_NAME] + "\n"
295 if row[BE_NAME] != row[BE_ORIG_NAME]:
296 rename[row[BE_ORIG_NAME]] = row[BE_NAME]
297 textbuf = self.w_beconfirmation_textview.get_buffer()
298 textbuf.set_text("")
299 textiter = textbuf.get_end_iter()
300 if len(active) > 0:
301 textbuf.insert_with_tags_by_name(textiter,
302 active_text, "bold")
303 textbuf.insert_with_tags_by_name(textiter,
304 active)
305 if len(delete) > 0:
306 if len(active) > 0:
307 textbuf.insert_with_tags_by_name(textiter,
308 "\n")
309 textbuf.insert_with_tags_by_name(textiter,
310 delete_text, "bold")
311 textbuf.insert_with_tags_by_name(textiter,
312 delete)
313 if len(rename) > 0:
314 if len(delete) > 0 or len(active) > 0:
315 textbuf.insert_with_tags_by_name(textiter,
316 "\n")
317 textbuf.insert_with_tags_by_name(textiter,
318 rename_text, "bold")
319 for orig in rename:
320 textbuf.insert_with_tags_by_name(textiter,
321 orig)
322 textbuf.insert_with_tags_by_name(textiter,
323 _(" to "), "bold")
324 textbuf.insert_with_tags_by_name(textiter,
325 rename.get(orig) + "\n")
326 self.w_cancelbe_button.grab_focus()
327 gobject.idle_add(self.w_beconfirmation_dialog.show)
328 self.progress_stop_thread = True
329
330 def __on_progressdialog_progress(self):
331 # This needs to be run in gobject.idle_add, otherwise we will get
332 # Xlib: unexpected async reply (sequence 0x2db0)!
333 gobject.idle_add(self.w_progress_dialog.show)
334 while not self.progress_stop_thread:
335 gobject.idle_add(self.w_progressbar.pulse)
336 time.sleep(0.1)
337 gobject.idle_add(self.w_progress_dialog.hide)
338
339 def __delete_activate_be(self):
340 not_deleted = []
341 not_default = None
342 not_renamed = {}
343 # The while gtk.events_pending():
344 # gtk.main_iteration(False)
345 # Is not working if we are calling libbe, so it is required
346 # To have sleep in few places in this function
347 # Remove
348 for row in self.be_list:
349 if row[BE_MARKED]:
350 time.sleep(0.1)
351 result = self.__destroy_be(row[BE_NAME])
352 if result != 0:
353 not_deleted.append(row[BE_NAME])
354 # Rename
355 for row in self.be_list:
356 if row[BE_NAME] != row[BE_ORIG_NAME]:
357 time.sleep(0.1)
358 result = self.__rename_be(row[BE_ORIG_NAME],
359 row[BE_NAME])
360 if result !=0:
361 not_renamed[row[BE_ORIG_NAME]] = row[BE_NAME]
362 # Set active
363 for row in self.be_list:
364 if row[BE_ACTIVE_DEFAULT] == True and row[BE_ID] != \
365 self.initial_default:
366 time.sleep(0.1)
367 result = self.__set_default_be(row[BE_NAME])
368 if result != 0:
369 not_default = row[BE_NAME]
370 if len(not_deleted) == 0 and not_default == None \
371 and len(not_renamed) == 0:
372 self.progress_stop_thread = True
373 else:
374 self.progress_stop_thread = True
375 msg = ""
376 if not_default:
377 msg += _("<b>Couldn't change Active "
378 "Boot Environment to:</b>\n") + not_default
379 if len(not_deleted) > 0:
380 if not_default:
381 msg += "\n\n"
382 msg += _("<b>Couldn't delete Boot "
383 "Environments:</b>\n")
384 for row in not_deleted:
385 msg += row + "\n"
386 if len(not_renamed) > 0:
387 if not_default or len(not_deleted):
388 msg += "\n"
389 msg += _("<b>Couldn't rename Boot "
390 "Environments:</b>\n")
391 for orig in not_renamed:
392 msg += _("%s <b>to</b> %s\n") % (orig,
393 not_renamed.get(orig))
394 gobject.idle_add(self.__error_occurred, msg)
395 return
396 gobject.idle_add(self.__on_cancel_be_clicked, None)
397
398 def __rename_cell(self, model, itr, new_name):
399 model.set_value(itr, BE_NAME, new_name)
400
401 def __rename_be(self, orig_name, new_name):
402 return be.beRename(orig_name, new_name)
403
404 def __error_occurred(self, error_msg, reset=True):
405 gui_misc.error_occurred(self.w_beadmin_dialog,
406 error_msg,
407 _("BE error"),
408 gtk.MESSAGE_ERROR,
409 True)
410 if reset:
411 self.__on_reset_be()
412
413 def __on_reset_be(self):
414 self.be_list.clear()
415 self.w_progress_dialog.show()
416 self.progress_stop_thread = False
417 Thread(target = self.__progress_pulse).start()
418 Thread(target = self.__prepare_beadmin_list).start()
419 self.w_ok_button.set_sensitive(False)
420
421 def __active_pane_toggle(self, cell, filtered_path, filtered_model):
422 model = filtered_model.get_model()
423 path = filtered_model.convert_path_to_child_path(filtered_path)
424 itr = model.get_iter(path)
425 if itr:
426 modified = model.get_value(itr, BE_MARKED)
427 # Do not allow to set active if selected for removal
428 model.set_value(itr, BE_MARKED, not modified)
429 # Do not allow to rename if we are removing be.
430 model.set_value(itr, BE_EDITABLE, modified)
431 self.__enable_disable_ok()
432
433 def __enable_disable_ok(self):
434 for row in self.be_list:
435 if row[BE_MARKED] == True:
436 self.w_ok_button.set_sensitive(True)
437 return
438 if row[BE_ID] == self.initial_default:
439 if row[BE_ACTIVE_DEFAULT] == False:
440 self.w_ok_button.set_sensitive(True)
441 return
442 if row[BE_NAME] != row[BE_ORIG_NAME]:
443 self.w_ok_button.set_sensitive(True)
444 return
445 self.w_ok_button.set_sensitive(False)
446 return
447
448 def __be_name_edited(self, cell, filtered_path, new_name, filtered_model):
449 model = filtered_model.get_model()
450 path = filtered_model.convert_path_to_child_path(filtered_path)
451 itr = model.get_iter(path)
452 if itr:
453 if model.get_value(itr, BE_NAME) == new_name:
454 return
455 if self.__verify_be_name(new_name) != 0:
456 return
457 self.__rename_cell(model, itr, new_name)
458 self.__enable_disable_ok()
459 return
460
461 #TBD: Notify user if name clash using same logic as Repo Add and warning text
462 def __verify_be_name(self, new_name):
463 if be.beVerifyBEName(new_name) != 0:
464 return -1
465 for row in self.be_list:
466 if new_name == row[BE_NAME]:
467 return -1
468 return 0
469
470 def __active_pane_default(self, cell, filtered_path, filtered_model):
471 model = filtered_model.get_model()
472 path = filtered_model.convert_path_to_child_path(filtered_path)
473 for row in model:
474 row[BE_ACTIVE_DEFAULT] = False
475 itr = model.get_iter(path)
476 if itr:
477 modified = model.get_value(itr, BE_ACTIVE_DEFAULT)
478 model.set_value(itr, BE_ACTIVE_DEFAULT, not modified)
479 self.__enable_disable_ok()
480
481 def __create_view_with_be(self, be_list):
482 dates = None
483 i = 0
484 j = 0
485 error_code = None
486 be_list_loop = None
487 if len(be_list) > 1 and type(be_list[0]) == type(-1):
488 error_code = be_list[0]
489 if error_code != None and error_code == 0:
490 be_list_loop = be_list[1]
491 elif error_code != None and error_code != 0:
492 msg = _("The <b>libbe</b> library couldn't "
493 "prepare list of Boot Environments."
494 "\nAll functions for managing Boot Environments are disabled")
495 self.__error_occurred(msg, False)
496 return
497 else:
498 be_list_loop = be_list
499
500 for bee in be_list_loop:
501 if bee.get("orig_be_name"):
502 name = bee.get("orig_be_name")
503 active = bee.get("active")
504 active_boot = bee.get("active_boot")
505 be_size = bee.get("space_used")
506 be_date = bee.get("date")
507 converted_size = \
508 self.__convert_size_of_be_to_string(be_size)
509 active_img = None
510 if not be_date and j == 0:
511 dates = self.__get_dates_of_creation(be_list_loop)
512 if dates:
513 try:
514 date_time = repr(dates[i])[1:-3]
515 date_tmp = time.strptime(date_time, \
516 "%a %b %d %H:%M %Y")
517 date_tmp2 = \
518 datetime.datetime(*date_tmp[0:5])
519 try:
520 date_format = \
521 unicode(
522 _("%m/%d/%y %H:%M"),
523 "utf-8").encode(
524 locale.getpreferredencoding())
525 except (UnicodeError, LookupError,
526 locale.Error):
527 date_format = "%F %H:%M"
528 date_time = \
529 date_tmp2.strftime(date_format)
530 i += 1
531 except (NameError, ValueError, TypeError):
532 date_time = None
533 else:
534 date_tmp = time.localtime(be_date)
535 try:
536 date_format = \
537 unicode(
538 _("%m/%d/%y %H:%M"),
539 "utf-8").encode(
540 locale.getpreferredencoding())
541 except (UnicodeError, LookupError, locale.Error):
542 date_format = "%F %H:%M"
543 date_time = \
544 time.strftime(date_format, date_tmp)
545 if active:
546 active_img = self.active_image
547 self.initial_active = j
548 if active_boot:
549 self.initial_default = j
550 if date_time != None:
551 try:
552 date_time = unicode(date_time,
553 locale.getpreferredencoding()).encode(
554 "utf-8")
555 except (UnicodeError, LookupError, locale.Error):
556 pass
557 self.be_list.insert(j, [j, False,
558 name, name,
559 date_time, active_img,
560 active_boot, converted_size, active_img == None])
561 j += 1
562 self.w_be_treeview.set_cursor(self.initial_active, None,
563 start_editing=True)
564 self.w_be_treeview.scroll_to_cell(self.initial_active)
565
566 def __destroy_be(self, be_name):
567 return be.beDestroy(be_name, 1, True)
568
569 def __set_default_be(self, be_name):
570 return be.beActivate(be_name)
571
572 def __cell_data_default_function(self, column, renderer, model, itr, data):
573 if itr:
574 if model.get_value(itr, BE_MARKED):
575 self.__set_renderer_active(renderer, False)
576 else:
577 self.__set_renderer_active(renderer, True)
578
579 def __cell_data_delete_function(self, column, renderer, model, itr, data):
580 if itr:
581 if model.get_value(itr, BE_ACTIVE_DEFAULT) or \
582 (self.initial_active == model.get_value(itr, BE_ID)) or \
583 (model.get_value(itr, BE_NAME) !=
584 model.get_value(itr, BE_ORIG_NAME)):
585 self.__set_renderer_active(renderer, False)
586 else:
587 self.__set_renderer_active(renderer, True)
588
589 @staticmethod
590 def __set_renderer_active(renderer, active):
591 if active:
592 renderer.set_property("sensitive", True)
593 renderer.set_property("mode", gtk.CELL_RENDERER_MODE_ACTIVATABLE)
594 else:
595 renderer.set_property("sensitive", False)
596 renderer.set_property("mode", gtk.CELL_RENDERER_MODE_INERT)
597
598 @staticmethod
599 def __get_dates_of_creation(be_list):
600 #zfs list -H -o creation rpool/ROOT/opensolaris-1
601 cmd = [ "/sbin/zfs", "list", "-H", "-o","creation" ]
602 for bee in be_list:
603 if bee.get("orig_be_name"):
604 name = bee.get("orig_be_name")
605 pool = bee.get("orig_be_pool")
606 cmd += [pool+"/ROOT/"+name]
607 if len(cmd) <= 5:
608 return None
609 list_of_dates = []
610 try:
611 proc = subprocess.Popen(cmd, stdout = subprocess.PIPE,
612 stderr = subprocess.PIPE,)
613 line_out = proc.stdout.readline()
614 while line_out:
615 list_of_dates.append(line_out)
616 line_out = proc.stdout.readline()
617 except OSError:
618 return list_of_dates
619 return list_of_dates
620
621 @staticmethod
622 def __convert_size_of_be_to_string(be_size):
623 if not be_size:
624 be_size = 0
625 return pkg.misc.bytes_to_str(be_size)
626
627 @staticmethod
628 def __cell_data_function(column, renderer, model, itr, data):
629 if itr:
630 if model.get_value(itr, BE_CURRENT_PIXBUF):
631 renderer.set_property("weight", pango.WEIGHT_BOLD)
632 else:
633 renderer.set_property("weight", pango.WEIGHT_NORMAL)