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