2013-01-23 15:55:14 +01:00
#!/usr/bin/python2
2012-01-31 14:29:13 +01:00
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2012 Agnieszka Kostrzewa <agnieszka.kostrzewa@gmail.com>
# Copyright (C) 2012 Marek Marczykowski <marmarek@mimuw.edu.pl>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
#
import sys
import os
2012-04-11 18:47:43 +02:00
import signal
2012-01-31 14:29:13 +01:00
from PyQt4 . QtCore import *
from PyQt4 . QtGui import *
from qubes . qubes import QubesVmCollection
from qubes . qubes import QubesException
from qubes . qubes import QubesDaemonPidfile
from qubes . qubes import QubesHost
2013-11-28 03:50:17 +01:00
from qubes import backup
2012-02-20 07:56:38 +01:00
from qubes import qubesutils
2012-01-31 14:29:13 +01:00
import qubesmanager . resources_rc
from pyinotify import WatchManager , Notifier , ThreadedNotifier , EventsCodes , ProcessEvent
import subprocess
import time
2012-02-20 07:56:38 +01:00
from thread_monitor import *
2012-01-31 14:29:13 +01:00
from operator import itemgetter
2012-02-20 07:56:38 +01:00
from datetime import datetime
from string import replace
2012-01-31 14:29:13 +01:00
from ui_backupdlg import *
from multiselectwidget import *
2012-02-22 06:09:25 +01:00
from backup_utils import *
2012-04-04 01:40:13 +02:00
import grp , pwd
2012-01-31 14:29:13 +01:00
class BackupVMsWindow ( Ui_Backup , QWizard ) :
2012-02-20 07:56:38 +01:00
__pyqtSignals__ = ( " backup_progress(int) " , )
2012-03-29 23:26:16 +02:00
def __init__ ( self , app , qvm_collection , blk_manager , shutdown_vm_func , parent = None ) :
2012-01-31 14:29:13 +01:00
super ( BackupVMsWindow , self ) . __init__ ( parent )
2012-02-20 07:56:38 +01:00
self . app = app
self . qvm_collection = qvm_collection
self . blk_manager = blk_manager
2012-03-29 23:26:16 +02:00
self . shutdown_vm_func = shutdown_vm_func
2012-01-31 14:29:13 +01:00
2012-02-22 06:09:25 +01:00
self . dev_mount_path = None
2012-02-20 07:56:38 +01:00
self . func_output = [ ]
2013-11-28 03:50:17 +01:00
self . selected_vms = [ ]
2012-01-31 14:29:13 +01:00
2014-01-12 05:17:41 +01:00
self . vm = self . qvm_collection [ 0 ]
2014-01-15 05:59:45 +01:00
self . files_to_backup = None
2013-11-28 03:45:22 +01:00
2012-02-20 07:56:38 +01:00
assert self . vm != None
2012-01-31 14:29:13 +01:00
2012-02-20 07:56:38 +01:00
self . setupUi ( self )
2012-01-31 14:29:13 +01:00
2013-11-28 04:07:39 +01:00
self . progress_status . text = " Backup in progress... "
2012-03-29 23:26:16 +02:00
self . show_running_vms_warning ( False )
2013-09-28 12:33:56 +02:00
self . dir_line_edit . setReadOnly ( False )
2012-01-31 14:29:13 +01:00
2012-02-20 07:56:38 +01:00
self . select_vms_widget = MultiSelectWidget ( self )
self . verticalLayout . insertWidget ( 1 , self . select_vms_widget )
2012-01-31 14:29:13 +01:00
2012-02-20 07:56:38 +01:00
self . connect ( self , SIGNAL ( " currentIdChanged(int) " ) , self . current_page_changed )
2012-03-29 23:26:16 +02:00
self . connect ( self . select_vms_widget , SIGNAL ( " selected_changed() " ) , self . check_running )
2013-03-01 20:45:33 +01:00
self . connect ( self . select_vms_widget , SIGNAL ( " items_removed(PyQt_PyObject) " ) , self . vms_removed )
self . connect ( self . select_vms_widget , SIGNAL ( " items_added(PyQt_PyObject) " ) , self . vms_added )
2012-03-29 23:26:16 +02:00
self . refresh_button . clicked . connect ( self . check_running )
self . shutdown_running_vms_button . clicked . connect ( self . shutdown_all_running_selected )
2012-02-20 07:56:38 +01:00
self . connect ( self . dev_combobox , SIGNAL ( " activated(int) " ) , self . dev_combobox_activated )
self . connect ( self , SIGNAL ( " backup_progress(int) " ) , self . progress_bar . setValue )
2013-11-28 03:52:02 +01:00
self . dir_line_edit . connect ( self . dir_line_edit , SIGNAL ( " textChanged(QString) " ) , self . backup_location_changed )
2014-01-12 05:20:48 +01:00
self . connect ( self . dev_combobox , SIGNAL ( " activated(int) " ) ,
self . update_device_appvm_enabled )
self . connect ( self . appvm_combobox , SIGNAL ( " activated(int) " ) ,
self . update_device_appvm_enabled )
2012-02-20 07:56:38 +01:00
self . select_vms_page . isComplete = self . has_selected_vms
2013-11-28 03:52:02 +01:00
self . select_dir_page . isComplete = self . has_selected_dir_and_pass
2012-02-20 07:56:38 +01:00
#FIXME
#this causes to run isComplete() twice, I don't know why
2013-11-28 03:52:02 +01:00
self . select_vms_page . connect (
self . select_vms_widget ,
SIGNAL ( " selected_changed() " ) ,
SIGNAL ( " completeChanged() " ) )
self . passphrase_line_edit . connect (
self . passphrase_line_edit ,
SIGNAL ( " textChanged(QString) " ) ,
self . backup_location_changed )
self . passphrase_line_edit_verify . connect (
self . passphrase_line_edit_verify ,
SIGNAL ( " textChanged(QString) " ) ,
self . backup_location_changed )
2012-04-04 01:40:13 +02:00
self . total_size = 0
2012-02-20 07:56:38 +01:00
self . __fill_vms_list__ ( )
2012-02-22 06:09:25 +01:00
fill_devs_list ( self )
2012-02-20 07:56:38 +01:00
2013-09-28 12:33:56 +02:00
fill_appvms_list ( self )
2012-03-29 23:26:16 +02:00
def show_running_vms_warning ( self , show ) :
self . running_vms_warning . setVisible ( show )
self . shutdown_running_vms_button . setVisible ( show )
self . refresh_button . setVisible ( show )
class VmListItem ( QListWidgetItem ) :
def __init__ ( self , vm ) :
self . vm = vm
2012-04-04 01:40:13 +02:00
if vm . qid == 0 :
local_user = grp . getgrnam ( ' qubes ' ) . gr_mem [ 0 ]
home_dir = pwd . getpwnam ( local_user ) . pw_dir
2013-11-28 03:50:17 +01:00
self . size = backup . get_disk_usage ( home_dir )
2012-04-04 01:40:13 +02:00
else :
2013-11-28 03:45:22 +01:00
self . size = self . get_vm_size ( vm )
2012-04-04 01:40:13 +02:00
super ( BackupVMsWindow . VmListItem , self ) . __init__ ( vm . name + " ( " + qubesutils . size_to_human ( self . size ) + " ) " )
2013-11-28 03:45:22 +01:00
2012-04-04 01:40:13 +02:00
def get_vm_size ( self , vm ) :
size = 0
if vm . private_img is not None :
size + = vm . get_disk_usage ( vm . private_img )
if vm . updateable :
size + = vm . get_disk_usage ( vm . root_img )
return size
2012-03-29 23:26:16 +02:00
2012-02-20 07:56:38 +01:00
def __fill_vms_list__ ( self ) :
for vm in self . qvm_collection . values ( ) :
if vm . is_appvm ( ) and vm . internal :
continue
if vm . is_template ( ) and vm . installed_by_rpm :
continue
2012-03-29 23:26:16 +02:00
item = BackupVMsWindow . VmListItem ( vm )
2012-03-07 20:42:18 +01:00
if vm . include_in_backups == True :
2012-03-29 23:26:16 +02:00
self . select_vms_widget . selected_list . addItem ( item )
2012-04-04 01:40:13 +02:00
self . total_size + = item . size
2012-03-29 23:26:16 +02:00
else :
self . select_vms_widget . available_list . addItem ( item )
self . check_running ( )
2012-04-04 01:40:13 +02:00
self . total_size_label . setText ( qubesutils . size_to_human ( self . total_size ) )
def vms_added ( self , items ) :
for i in items :
self . total_size + = i . size
self . total_size_label . setText ( qubesutils . size_to_human ( self . total_size ) )
def vms_removed ( self , items ) :
for i in items :
self . total_size - = i . size
self . total_size_label . setText ( qubesutils . size_to_human ( self . total_size ) )
2012-03-29 23:26:16 +02:00
def check_running ( self ) :
some_selected_vms_running = False
for i in range ( self . select_vms_widget . selected_list . count ( ) ) :
item = self . select_vms_widget . selected_list . item ( i )
if item . vm . is_running ( ) and item . vm . qid != 0 :
item . setForeground ( QBrush ( QColor ( 255 , 0 , 0 ) ) )
some_selected_vms_running = True
2012-03-07 20:42:18 +01:00
else :
2012-03-29 23:26:16 +02:00
item . setForeground ( QBrush ( QColor ( 0 , 0 , 0 ) ) )
self . show_running_vms_warning ( some_selected_vms_running )
2013-11-28 03:45:22 +01:00
2012-03-29 23:26:16 +02:00
for i in range ( self . select_vms_widget . available_list . count ( ) ) :
item = self . select_vms_widget . available_list . item ( i )
if item . vm . is_running ( ) and item . vm . qid != 0 :
item . setForeground ( QBrush ( QColor ( 255 , 0 , 0 ) ) )
else :
item . setForeground ( QBrush ( QColor ( 0 , 0 , 0 ) ) )
return some_selected_vms_running
def shutdown_all_running_selected ( self ) :
2012-05-13 16:56:27 +02:00
( names , vms ) = self . get_running_vms ( )
2012-03-29 23:26:16 +02:00
if len ( vms ) == 0 :
return ;
2012-05-25 13:59:58 +02:00
for vm in vms :
self . blk_manager . check_if_serves_as_backend ( vm )
2012-03-29 23:26:16 +02:00
reply = QMessageBox . question ( None , " VM Shutdown Confirmation " ,
" Are you sure you want to power down the following VMs: <b> {0} </b>?<br> "
" <small>This will shutdown all the running applications within them.</small> " . format ( ' , ' . join ( names ) ) ,
QMessageBox . Yes | QMessageBox . Cancel )
self . app . processEvents ( )
if reply == QMessageBox . Yes :
2013-11-28 03:45:22 +01:00
2012-05-13 16:56:27 +02:00
wait_time = 60.0
2012-03-29 23:26:16 +02:00
for vm in vms :
2012-05-13 16:56:27 +02:00
self . shutdown_vm_func ( vm , wait_time * 1000 )
2012-03-29 23:26:16 +02:00
2012-04-01 19:50:55 +02:00
progress = QProgressDialog ( " Shutting down VMs <b> {0} </b>... " . format ( ' , ' . join ( names ) ) , " " , 0 , 0 )
progress . setModal ( True )
progress . show ( )
2012-03-29 23:26:16 +02:00
2012-04-01 19:50:55 +02:00
wait_for = wait_time
while self . check_running ( ) and wait_for > 0 :
self . app . processEvents ( )
2012-05-13 16:56:27 +02:00
time . sleep ( 0.5 )
wait_for - = 0.5
2012-03-29 23:26:16 +02:00
2012-04-01 19:50:55 +02:00
progress . hide ( )
2012-03-29 23:26:16 +02:00
2012-05-13 16:56:27 +02:00
def get_running_vms ( self ) :
names = [ ]
vms = [ ]
for i in range ( self . select_vms_widget . selected_list . count ( ) ) :
item = self . select_vms_widget . selected_list . item ( i )
if item . vm . is_running ( ) and item . vm . qid != 0 :
names . append ( item . vm . name )
vms . append ( item . vm )
return ( names , vms )
2012-02-20 07:56:38 +01:00
def dev_combobox_activated ( self , idx ) :
2012-02-22 06:09:25 +01:00
dev_combobox_activated ( self , idx )
2012-02-20 07:56:38 +01:00
2014-01-12 05:20:48 +01:00
def update_device_appvm_enabled ( self , idx ) :
update_device_appvm_enabled ( self , idx )
2012-02-20 07:56:38 +01:00
@pyqtSlot ( name = ' on_select_path_button_clicked ' )
def select_path_button_clicked ( self ) :
2012-02-22 06:09:25 +01:00
select_path_button_clicked ( self )
2012-02-20 07:56:38 +01:00
def validateCurrentPage ( self ) :
if self . currentPage ( ) is self . select_vms_page :
2013-11-28 03:50:17 +01:00
if self . check_running ( ) :
QMessageBox . information ( None , " Wait! " , " Some selected VMs are running. Running VMs can not be backuped. Please shut them down or remove them from the list. " )
return False
self . selected_vms = [ ]
2012-03-29 23:26:16 +02:00
for i in range ( self . select_vms_widget . selected_list . count ( ) ) :
2013-11-28 03:50:17 +01:00
self . selected_vms . append ( self . select_vms_widget . selected_list . item ( i ) . vm )
2013-11-28 04:07:39 +01:00
elif self . currentPage ( ) is self . select_dir_page :
2014-01-11 22:57:34 +01:00
backup_location = str ( self . dir_line_edit . text ( ) )
if not backup_location :
2013-11-28 04:07:39 +01:00
QMessageBox . information ( None , " Wait! " , " Enter backup target location first. " )
return False
if self . appvm_combobox . currentIndex ( ) == 0 and \
2014-01-11 22:57:34 +01:00
not os . path . isdir ( backup_location ) :
2013-11-28 04:07:39 +01:00
QMessageBox . information ( None , " Wait! " ,
2014-01-11 22:57:34 +01:00
" Selected directory do not exists or not a directory ( %s ). " % backup_location )
2013-11-28 04:07:39 +01:00
return False
if not len ( self . passphrase_line_edit . text ( ) ) :
QMessageBox . information ( None , " Wait! " , " Enter passphrase for backup encryption/verification first. " )
return False
if self . passphrase_line_edit . text ( ) != self . passphrase_line_edit_verify . text ( ) :
QMessageBox . information ( None , " Wait! " , " Enter the same passphrase in both fields. " )
return False
2012-02-20 07:56:38 +01:00
return True
def gather_output ( self , s ) :
self . func_output . append ( s )
def update_progress_bar ( self , value ) :
2013-11-28 04:07:39 +01:00
self . emit ( SIGNAL ( " backup_progress(int) " ) , value )
2012-02-20 07:56:38 +01:00
def __do_backup__ ( self , thread_monitor ) :
msg = [ ]
2013-09-28 12:33:56 +02:00
2012-02-20 07:56:38 +01:00
try :
2014-01-11 22:57:34 +01:00
backup . backup_do ( str ( self . dir_line_edit . text ( ) ) ,
2013-11-28 03:50:17 +01:00
self . files_to_backup ,
str ( self . passphrase_line_edit . text ( ) ) ,
progress_callback = self . update_progress_bar ,
2014-01-15 05:59:16 +01:00
encrypted = self . encryption_checkbox . isChecked ( ) ,
2013-11-28 03:50:17 +01:00
appvm = self . target_appvm )
#simulate_long_lasting_proces(10, self.update_progress_bar)
2012-02-20 07:56:38 +01:00
except Exception as ex :
2013-09-28 12:33:56 +02:00
print " Exception: " , ex
2012-02-20 07:56:38 +01:00
msg . append ( str ( ex ) )
if len ( msg ) > 0 :
thread_monitor . set_error_msg ( ' \n ' . join ( msg ) )
thread_monitor . set_finished ( )
2013-11-28 03:45:22 +01:00
2012-01-31 14:29:13 +01:00
def current_page_changed ( self , id ) :
2012-02-20 07:56:38 +01:00
if self . currentPage ( ) is self . confirm_page :
2013-09-28 12:33:56 +02:00
self . target_appvm = None
2013-11-28 03:50:17 +01:00
if self . appvm_combobox . currentIndex ( ) != 0 : #An existing appvm chosen
self . target_appvm = self . qvm_collection . get_vm_by_name (
self . appvm_combobox . currentText ( ) )
2013-09-28 12:33:56 +02:00
2012-02-20 07:56:38 +01:00
del self . func_output [ : ]
2012-03-29 23:26:16 +02:00
try :
2013-11-28 03:50:17 +01:00
self . files_to_backup = backup . backup_prepare (
self . selected_vms ,
print_callback = self . gather_output ,
hide_vm_names = self . encryption_checkbox . isChecked ( ) )
2012-03-29 23:26:16 +02:00
except Exception as ex :
2013-09-28 12:33:56 +02:00
print " Exception: " , ex
QMessageBox . critical ( None , " Error while preparing backup. " , " ERROR: {0} " . format ( ex ) )
2012-02-22 06:09:25 +01:00
2012-02-20 07:56:38 +01:00
self . textEdit . setReadOnly ( True )
self . textEdit . setFontFamily ( " Monospace " )
self . textEdit . setText ( " \n " . join ( self . func_output ) )
elif self . currentPage ( ) is self . commit_page :
self . button ( self . FinishButton ) . setDisabled ( True )
2012-04-11 18:47:43 +02:00
self . button ( self . CancelButton ) . setDisabled ( True )
2012-02-20 07:56:38 +01:00
self . thread_monitor = ThreadMonitor ( )
thread = threading . Thread ( target = self . __do_backup__ , args = ( self . thread_monitor , ) )
thread . daemon = True
thread . start ( )
2012-04-11 18:47:43 +02:00
self . button ( self . CancelButton ) . setDisabled ( False )
2012-02-20 07:56:38 +01:00
2012-04-07 23:09:06 +02:00
counter = 0
2012-02-20 07:56:38 +01:00
while not self . thread_monitor . is_finished ( ) :
self . app . processEvents ( )
time . sleep ( 0.1 )
if not self . thread_monitor . success :
2014-01-15 05:59:45 +01:00
self . progress_status . setText ( " Backup error. " )
QMessageBox . warning ( self , " Backup error! " , " ERROR: {} " . format (
self . thread_monitor . error_msg ) )
2013-11-28 04:07:39 +01:00
else :
self . progress_bar . setValue ( 100 )
2014-01-15 05:59:45 +01:00
self . progress_status . setText ( " Backup finished. " )
2012-03-02 02:50:12 +01:00
if self . dev_mount_path != None :
umount_device ( self . dev_mount_path )
2014-01-12 05:42:19 +01:00
detach_device ( self , str ( self . dev_combobox . itemData (
self . dev_combobox . currentIndex ( ) ) . toString ( ) ) )
2012-02-20 07:56:38 +01:00
self . button ( self . FinishButton ) . setEnabled ( True )
2013-11-28 03:45:22 +01:00
2012-03-02 02:50:12 +01:00
def reject ( self ) :
2012-04-11 18:47:43 +02:00
#cancell clicked while the backup is in progress.
2013-11-28 04:07:39 +01:00
#calling kill on tar.
2012-04-11 18:47:43 +02:00
if self . currentPage ( ) is self . commit_page :
manager_pid = os . getpid ( )
2013-11-28 04:07:39 +01:00
archive_pid_cmd = [ " ps " , " --ppid " , str ( manager_pid ) ]
2012-04-11 18:47:43 +02:00
while not self . thread_monitor . is_finished ( ) :
2013-11-28 04:07:39 +01:00
archive_pid = subprocess . Popen ( archive_pid_cmd , stdout = subprocess . PIPE )
output = archive_pid . stdout . readlines ( )
2012-04-11 18:47:43 +02:00
for l in output :
2013-11-28 04:07:39 +01:00
if l . strip ( ) . endswith ( " tar " ) :
os . kill ( int ( l . split ( " " ) [ 0 ] ) , signal . SIGTERM )
time . sleep ( 0.1 )
2012-04-11 18:47:43 +02:00
2012-03-02 02:50:12 +01:00
if self . dev_mount_path != None :
umount_device ( self . dev_mount_path )
self . done ( 0 )
2013-11-28 03:45:22 +01:00
2012-03-02 02:50:12 +01:00
2012-02-20 07:56:38 +01:00
def has_selected_vms ( self ) :
return self . select_vms_widget . selected_list . count ( ) > 0
2013-11-28 03:52:02 +01:00
def has_selected_dir_and_pass ( self ) :
if not len ( self . passphrase_line_edit . text ( ) ) :
return False
if self . passphrase_line_edit . text ( ) != self . passphrase_line_edit_verify . text ( ) :
return False
2014-01-11 22:57:34 +01:00
return len ( self . dir_line_edit . text ( ) ) > 0
2012-01-31 14:29:13 +01:00
2013-11-28 03:52:02 +01:00
def backup_location_changed ( self , new_dir = None ) :
self . select_dir_page . emit ( SIGNAL ( " completeChanged() " ) )
2012-02-20 07:56:38 +01:00
2012-01-31 14:29:13 +01:00
# Bases on the original code by:
# Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
def handle_exception ( exc_type , exc_value , exc_traceback ) :
import sys
import os . path
import traceback
filename , line , dummy , dummy = traceback . extract_tb ( exc_traceback ) . pop ( )
filename = os . path . basename ( filename )
error = " %s : %s " % ( exc_type . __name__ , exc_value )
QMessageBox . critical ( None , " Houston, we have a problem... " ,
" Whoops. A critical error has occured. This is most likely a bug "
" in Qubes Restore VMs application.<br><br> "
" <b><i> %s </i></b> " % error +
" at <b>line %d </b> of file <b> %s </b>.<br/><br/> "
% ( line , filename ) )
def main ( ) :
global qubes_host
qubes_host = QubesHost ( )
global app
app = QApplication ( sys . argv )
app . setOrganizationName ( " The Qubes Project " )
app . setOrganizationDomain ( " http://qubes-os.org " )
2012-02-20 07:56:38 +01:00
app . setApplicationName ( " Qubes Backup VMs " )
2012-01-31 14:29:13 +01:00
sys . excepthook = handle_exception
qvm_collection = QubesVmCollection ( )
qvm_collection . lock_db_for_reading ( )
qvm_collection . load ( )
qvm_collection . unlock_db ( )
global backup_window
backup_window = BackupVMsWindow ( )
backup_window . show ( )
app . exec_ ( )
app . exit ( )
if __name__ == " __main__ " :
main ( )