class MountLocalPath: def __init__(self, builder, callback, source_path, destination_path): self.destination_path = destination_path self.source_path = source_path self.destination_path = destination_path self.callback = callback self.please_wait_popup = PleaseWaitModalPopup( builder, title=_("Please wait..."), message=_("Mounting...")) self.please_wait_popup.show() thread = threading.Thread(target=self._do_mount_command, args=( source_path, destination_path, )) thread.daemon = True thread.start() def _do_mount_command(self, source_path, destination_path): try: if not os.path.exists(destination_path) and not os.path.isdir( destination_path): os.mkdir(destination_path, 0o755) is_unmounted, message = Utility.umount_warn_on_busy( destination_path) if not is_unmounted: GLib.idle_add(self.callback, False, message) GLib.idle_add(self.please_wait_popup.destroy) return is_unmounted, message = Utility.umount_warn_on_busy(source_path) if not is_unmounted: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.please_wait_popup.destroy) return mount_cmd_list = ['mount', source_path, destination_path] process, flat_command_string, failed_message = Utility.run( "Mounting selected partition: ", mount_cmd_list, use_c_locale=False) if process.returncode != 0: GLib.idle_add(self.callback, False, failed_message) GLib.idle_add(self.please_wait_popup.destroy) return else: GLib.idle_add(self.callback, True, "", destination_path) GLib.idle_add(self.please_wait_popup.destroy) except Exception as e: tb = traceback.format_exc() print(tb) GLib.idle_add(self.callback, False, "Error mounting folder: " + tb) GLib.idle_add(self.please_wait_popup.destroy)
def mount_partition(self, selected_partition_key): self.image_explorer_in_progress = True with self.requested_stop_lock: self.requested_stop = False if self.is_partition_mounted: # Unmount partition please_wait_popup = PleaseWaitModalPopup( self.builder, title=_("Please wait..."), message=_("Unmounting: {path}").format( path=selected_partition_key), on_close_callback=self.cancel_image_explorer) please_wait_popup.show() self.mount_thread = threading.Thread( target=self._do_unmount_wrapper, args=(please_wait_popup, self._post_backup_image_unmount_callback, IMAGE_EXPLORER_DIR)) self.mount_thread.daemon = True self.mount_thread.start() else: mount_msg = _( "Mounting as read-only..." ) + "\n\nFor 'gzip' compressed images often the entire backup image needs to be decompressed to a temporary file before a single file can be accessed.\nFor very large backup images this MAY TAKE HOURS depending on the speed of your computer.\n\nFor near-instantaneous file access, a future version of Rescuezilla may switch the default compression away from 'gzip'.\n\nTo cancel the mount operation close this dialog box." please_wait_popup = PleaseWaitModalPopup( self.builder, title=_("Please wait..."), message=mount_msg, on_close_callback=self.cancel_image_explorer) please_wait_popup.show() thread = threading.Thread( target=self._do_mount_command, args=( please_wait_popup, self._post_backup_image_mount_callback, self.selected_image, selected_partition_key, IMAGE_EXPLORER_DIR, )) thread.daemon = True thread.start()
class ImageFolderQuery: def __init__(self, builder, image_list_store): self.image_dict = {} self.ignore_folder_set = set() # Relying on CPython GIL to communicate between threads. self.failed_to_read_image_dict = {} self.builder = builder self.image_list_store = image_list_store self.win = self.builder.get_object("main_window") self.icon_pixbufs = { "RESCUEZILLA_1.0.5_FORMAT": self.builder.get_object( "rescuezilla_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "REDOBACKUP_0.9.2_FORMAT": self.builder.get_object( "redobackup_v092_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "REDOBACKUP_0.9.3_1.0.4_FORMAT": self.builder.get_object( "redobackup_v093_to_v104_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "CLONEZILLA_FORMAT": self.builder.get_object( "clonezilla_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "QEMU_FORMAT": self.builder.get_object( "qemu_nbd_placeholder_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "FOGPROJECT_FORMAT": self.builder.get_object( "fogproject_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "REDORESCUE_FORMAT": self.builder.get_object( "redorescue_placeholder_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "FOXCLONE_FORMAT": self.builder.get_object("foxclone_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "FSARCHIVER_FORMAT": self.builder.get_object( "fsarchiver_placeholder_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "APART_GTK_FORMAT": self.builder.get_object( "apart_gtk_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "warning": self.builder.get_object("warning_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "padlock": self.builder.get_object("padlock_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR) } self.backup_label = self.builder.get_object("backup_folder_label") self.restore_label = self.builder.get_object("restore_folder_label") self.verify_label = self.builder.get_object("verify_folder_label") self.image_explorer_folder_label = self.builder.get_object( "image_explorer_folder_label") self.query_path = MOUNT_DIR self.requested_stop_lock = threading.Lock() self.requested_stop = False self.image_folder_query_in_progress = False def is_stop_requested(self): with self.requested_stop_lock: return self.requested_stop def is_image_folder_query_in_progress(self): return self.image_folder_query_in_progress def cancel_image_folder_query(self): with self.requested_stop_lock: self.requested_stop = True return def query_folder(self, path): with self.requested_stop_lock: self.requested_stop = False self.query_path = path self.image_list_store.clear() print("Starting scan of provided path " + self.query_path) self.backup_label.set_text(self.query_path) self.restore_label.set_text(self.query_path) self.verify_label.set_text(self.query_path) self.image_explorer_folder_label.set_text(self.query_path) self.image_list_store.clear() self.failed_to_read_image_dict.clear() self.win.set_sensitive(False) self.please_wait_popup = PleaseWaitModalPopup( self.builder, title=_("Please wait..."), message=_("Scanning folder for backup images...") + "\n\n" + _("Close this popup to cancel scanning the selected folder and subfolders." ), on_close_callback=self.cancel_image_folder_query) self.please_wait_popup.show() thread = threading.Thread(target=self.scan_image_directory) thread.daemon = True thread.start() def _populate_image_list_table(self): print("Populating image list table. Image dict is length: " + str(len(self.image_dict))) self.image_list_store.clear() traceback_messages = "" for key in self.image_dict.keys(): try: image = self.image_dict[key] format = image.image_format if len(image.warning_dict.keys()) > 0: warning_icon = self.icon_pixbufs['warning'] else: warning_icon = None if image.is_needs_decryption: lock_icon = self.icon_pixbufs['padlock'] else: lock_icon = None self.image_list_store.append([ key, format, warning_icon, lock_icon, self.icon_pixbufs[format], image.enduser_filename, image.enduser_readable_size, str(image.last_modified_timestamp), image.user_notes, image.get_enduser_friendly_partition_description() ]) except Exception as e: tb = traceback.format_exc() traceback_messages += image.enduser_filename + ":\n" + tb + "\n\n" # Highlight first image if there is only 1 image. if len(self.image_dict.keys()) == 1: self.builder.get_object( "restore_image_selection_treeselection").select_path(0) if len(self.failed_to_read_image_dict.keys()) > 0: for key in self.failed_to_read_image_dict.keys(): traceback_messages += key + ": " + self.failed_to_read_image_dict[ key] + "\n\n" if len(traceback_messages) > 0: ErrorMessageModalPopup( self.builder, str(traceback_messages), error_heading=_("Error processing the following images:")) self.please_wait_popup.destroy() def scan_file(self, absolute_path, enduser_filename): print("Scan file " + absolute_path) is_image = False try: temp_image_dict = {} dirname = os.path.dirname(absolute_path) if isfile(absolute_path) and dirname not in self.ignore_folder_set: head, filename = os.path.split(absolute_path) # Identify Clonezilla images by presence of a file named "parts". Cannot use "clonezilla-img" or # "dev-fs.list" because these files were not created by in earlier Clonezilla versions. Cannot use # "disk" as Clonezilla's 'saveparts' function does not create it. But both 'savedisk' and 'saveparts' # always creates a file named 'parts' across every version of Clonezilla tested. error_suffix = "" # Ignore [/mnt/backup/]/bin/parts and [/mnt/backup/]/sbin/parts if filename == "parts" and not filename == "bin" and not filename == "sbin": print("Found Clonezilla image " + filename) GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanning: {filename}").format( filename=absolute_path)) temp_image_dict = ClonezillaImage.get_clonezilla_image_dict( absolute_path, enduser_filename) error_suffix = _( "This can happen when loading images which Clonezilla was unable to completely backup." ) error_suffix += " " + _( "Any other filesystems within the image should be restorable as normal." ) # Only 1 Clonezilla image per folder, so consider the image scanned is_image = True elif absolute_path.endswith(".backup"): # The legacy Redo Backup and Recovery v0.9.3-v1.0.4 format was adapted and extended Foxclone, so # care is taken here to delineate the image formats by a simple heuristic: the existence of Foxclone's MBR backup. foxclone_mbr = absolute_path.split(".backup")[0] + ".grub" if os.path.exists(foxclone_mbr): print("Found a Foxclone image " + filename) GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanning: {filename}").format( filename=absolute_path)) temp_image_dict = { absolute_path: FoxcloneImage(absolute_path, enduser_filename, filename) } error_suffix = _( "Any other filesystems within the image should be restorable as normal." ) is_image = True else: print( "Found a legacy Redo Backup / Rescuezilla v1.0.5 image " + filename) GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanning: {filename}").format( filename=absolute_path)) temp_image_dict = { absolute_path: RedoBackupLegacyImage(absolute_path, enduser_filename, filename) } error_suffix = _( "Any other filesystems within the image should be restorable as normal." ) is_image = True elif absolute_path.endswith(".redo"): # The Redo Rescue format's metadata is a JSON file ending in .redo. Unfortunately this conflicts # with the legacy Redo Backup and Recovery 0.9.2 format, which also uses a metadata file ending in # .redo, so care is taken here to delineate the image formats by a simple heuristic: whether or not # the file is valid JSON. if RedoRescueImage.is_valid_json(absolute_path): # ".redo" is used for Redo Rescue format and Redo Backup and Recovery 0.9.2 format print("Found Redo Rescue image " + filename) GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanning: {filename}").format( filename=absolute_path)) temp_image_dict = { absolute_path: RedoRescueImage(absolute_path, enduser_filename, filename) } error_suffix = _( "Any other filesystems within the image should be restorable as normal." ) is_image = True else: print( "Found a legacy Redo Backup and Recovery v0.9.2 image " + filename) GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanning: {filename}").format( filename=absolute_path)) temp_image_dict = { absolute_path: RedoBackupLegacyImage(absolute_path, enduser_filename, filename) } error_suffix = _( "Any other filesystems within the image should be restorable as normal." ) is_image = True elif absolute_path.endswith( ".partitions" ) and not absolute_path.endswith(".minimum.partitions"): print("Found FOG Project image " + filename) GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanning: {filename}").format( filename=absolute_path)) temp_image_dict = { absolute_path: FogProjectImage(absolute_path, enduser_filename, filename) } error_suffix = _( "Any other filesystems within the image should be restorable as normal." ) is_image = True elif absolute_path.endswith(".fsa"): print("Found FSArchiver image " + filename) GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanning: {filename}").format( filename=absolute_path)) temp_image_dict = { absolute_path: FsArchiverImage(absolute_path, enduser_filename, filename) } error_suffix = "" is_image = True elif ".apt." in absolute_path: # Apart GTK images within a single folder are combined into one ApartGTKImage instance, so ensure # the folder hasn't already been scanned. print("Found Apart GTK image " + filename + " (will include other images in the same folder)") GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanning: {filename}").format( filename=absolute_path)) temp_image_dict = { absolute_path: ApartGtkImage(absolute_path) } error_suffix = _( "Any other filesystems within the image should be restorable as normal." ) # Only 1 Apart GTK image per folder (which may contain a huge number of images, often of the # same partition). Need to add image to the ignore fodler set to prevent double scanning self.ignore_folder_set.add(dirname) is_image = True # If haven't found an image for this file, try scanning for QemuImages. Due to slow scan, do not look # in subfolders else: is_qemu_candidate, extension = QemuImage.is_supported_extension( filename) if is_qemu_candidate: # TODO: Considering skipping raw images, for speedup. # is_raw = QemuImage.does_file_extension_refer_to_raw_image(extension) if QemuImage.has_conflict_img_format_in_same_folder( absolute_path, extension): print( "Not considering " + filename + " as QemuImage as found exiting image it probably belongs to" ) else: print( "Found an extension that should be compatible with qemu-nbd: " + filename) timeout_seconds = 10 GLib.idle_add( self.please_wait_popup. set_secondary_label_text, _("Scanning: {filename}").format( filename=absolute_path) + " " + _("({timeout_seconds} second timeout)").format( timeout_seconds=timeout_seconds)) qemu_img = QemuImage(absolute_path, enduser_filename, timeout_seconds) if qemu_img.has_initialized: temp_image_dict = {absolute_path: qemu_img} error_suffix = _( "Support for virtual machine images is still experimental." ) is_image = True if is_image: image_warning_message = "" for key in temp_image_dict.keys(): for warning_dict_key in temp_image_dict[ key].warning_dict.keys(): image_warning_message += " " + warning_dict_key + ": "\ + temp_image_dict[key].warning_dict[warning_dict_key] + "\n" if len(image_warning_message) > 0: self.failed_to_read_image_dict[absolute_path] = _( "Unable to fully process the image associated with the following partitions:" ) + "\n" + image_warning_message + error_suffix for key in temp_image_dict.keys(): self.image_dict[key] = temp_image_dict[key] except Exception as e: print("Failed to read: " + absolute_path) tb = traceback.format_exc() self.failed_to_read_image_dict[enduser_filename] = tb traceback.print_exc() return is_image def scan_image_directory(self): self.image_dict.clear() self.ignore_folder_set.clear() self.failed_to_read_image_dict.clear() try: # list files and directories for filename in os.listdir(self.query_path): if self.is_stop_requested(): break abs_base_scan_path = os.path.abspath( join(self.query_path, filename)) print("Scanning " + abs_base_scan_path) if isfile(abs_base_scan_path): print("Scanning file " + abs_base_scan_path) self.scan_file(abs_base_scan_path, filename) elif isdir(abs_base_scan_path): GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanning: {filename}").format( filename=abs_base_scan_path)) # List the subdirectory (1 level deep) for subdir_filename in os.listdir(abs_base_scan_path): if self.is_stop_requested(): break absolute_path = join(abs_base_scan_path, subdir_filename) enduser_filename = os.path.join( filename, subdir_filename) if isfile(absolute_path): print("Scanning subdir file " + absolute_path) self.scan_file(absolute_path, enduser_filename) except Exception as e: tb = traceback.format_exc() GLib.idle_add( ErrorMessageModalPopup.display_nonfatal_warning_message, self.builder, "Failed to scan for images: " + tb) # Relying on CPython GIL to access the self.image_dict GLib.idle_add(self._populate_image_list_table)
class PartitionsToRestore: def __init__(self, builder): self.builder = builder self.destination_partition_combobox_list = self.builder.get_object("destination_partition_combobox_list") self.NOT_RESTORING_PARTITION_KEY = "DISABLED" self.NOT_RESTORING_PARTITION_ENDUSER_FRIENDLY = _("Not restoring this partition") self.win = self.builder.get_object("main_window") self.overwriting_partition_table_message = "<b>" + _("You will be overwriting the partition table.") + "</b>" self.not_overwriting_partition_table_message = _("You will <b>not</b> be overwriting the partition table.") self.no_partition_table_message = _("The source does not contain a partition table.") self.lvm_lv_path_list = [] self.lvm_lv_path_lock = threading.Lock() # FIXME: Refactor the code to remove the need for this ugly initialization. self.dest_drive_dict = {"partitions": {}} self.mode_list = [Mode.RESTORE, Mode.CLONE] self.partition_table_checkbutton_dict = {Mode.RESTORE: self.builder.get_object("restore_overwrite_partition_table_checkbutton"), Mode.CLONE: self.builder.get_object("clone_overwrite_partition_table_checkbutton")} self.overwrite_partition_table_warning_label_dict = {Mode.RESTORE: self.builder.get_object("restore_step4_overwrite_partition_table_warning_label"), Mode.CLONE: self.builder.get_object("clone_step4_overwrite_partition_table_warning_label")} self.selected_image_text_dict = {Mode.RESTORE: self.builder.get_object("restore_step4_selected_image_text"), Mode.CLONE: self.builder.get_object("clone_step4_selected_drives_text")} self.partition_selection_list = self.builder.get_object("partition_selection_list") self.destination_partition_combobox_cell_renderer_dict = {Mode.RESTORE: self.builder.get_object("restore_destination_partition_combobox_cell_renderer"), Mode.CLONE: self.builder.get_object("clone_destination_partition_combobox_cell_renderer")} self.selected_image = None self.please_wait_popup = None self.dest_drive_key = "" self.dest_drive_node = {} def set_overwriting_partition_warning_label(self, is_overwriting): if is_overwriting: for mode in self.mode_list: overwrite_partition_table_warning_text = self.overwriting_partition_table_message + " " + _("The \"destination partition\" column has been updated using the information stored within the backup image.\n\n<b>If partitions have been resized, new partitions added, or additional operating systems installed <i>since the backup image was created</i>, then the destination drive's partition table will not match the backup image, and overwriting the destination drive's partition table will render these resized and additional partitions permanently inaccessible.</b> If you have not modified the partition table in such a way since creating this backup then overwriting the partition table is completely safe and will have no negative effects.") self.overwrite_partition_table_warning_label_dict[mode].set_markup(overwrite_partition_table_warning_text) self._use_image_partition_table() else: pt_warning = self.not_overwriting_partition_table_message if not self.selected_image.has_partition_table(): pt_warning = self.no_partition_table_message target_node_warning_text = pt_warning + " " + _("The \"destination partition\" column has been updated with destination drive's existing partition table information.\n\n<b>The destination partition column can be modified as a dropdown menu. Incorrectly mapping the destination partitions may cause operating systems to no longer boot.</b> If you are unsure of the mapping, consider if it's more suitable to instead overwrite the partition table.") for mode in self.mode_list: self.overwrite_partition_table_warning_label_dict[mode].set_markup(target_node_warning_text) self._use_existing_drive_partition_table() def initialize_individual_partition_restore_list(self, selected_image, dest_drive_node, dest_drive_desc, dest_drive_dict): self.selected_image = selected_image self.dest_drive_node = dest_drive_node self.dest_drive_desc = dest_drive_desc self.dest_drive_dict = dest_drive_dict if isinstance(self.selected_image, ClonezillaImage): print("Got selected Clonezilla image: " + str(selected_image.image_format_dict_dict)) elif isinstance(self.selected_image, RedoBackupLegacyImage): print("Got selected RedoBackupLegacy image: " + str(selected_image.normalized_sfdisk_dict)) self._use_image_partition_table() info_string = "<b>" + _("Selected image") + "</b> " + GObject.markup_escape_text(self.selected_image.absolute_path) + "\n" + "<b>" + _("Destination device") + "</b> " + GObject.markup_escape_text(self.dest_drive_desc) for mode in self.mode_list: self.selected_image_text_dict[mode].set_markup(info_string) print("Have selected image " + str(self.selected_image)) print("Have drive dict " + str(self.dest_drive_dict)) # If the image has a partition table, the overwrite toggle is enabled and defaults to True. Otherwise # it's not possible to overwrite the partition table. for overwrite_partition_table_checkbutton in self.partition_table_checkbutton_dict.values(): overwrite_partition_table_checkbutton.set_sensitive(self.selected_image.has_partition_table()) overwrite_partition_table_checkbutton.set_active(self.selected_image.has_partition_table()) self.set_overwriting_partition_warning_label(self.selected_image.has_partition_table()) # Starts LVM and umounts all relevant logical volumes # FIXME: Similar code is is duplicated elsewhere in the codebase. def _scan_and_unmount_existing_lvm(self, dest_partitions, is_overwriting_partition_table): with self.lvm_lv_path_lock: self.lvm_lv_path_list.clear() error_message = "" try: # Gathering LVM logical volumes. # Start the Logical Volume Manager (LVM). Caller raises Exception on failure Lvm.start_lvm2(logger=None) vg_state_dict = Lvm.get_volume_group_state_dict(logger=None) for dest_partition_key in dest_partitions: # Figure out LVM Volume Groups and Logical Volumes relevant_vg_name_list = [] for report_dict in vg_state_dict['report']: for vg_dict in report_dict['vg']: if 'pv_name' in vg_dict.keys() and dest_partition_key == vg_dict['pv_name']: if 'vg_name' in vg_dict.keys(): vg_name = vg_dict['vg_name'] else: error_message += "Could not find volume group name vg_name in " + str(vg_dict) + "\n" # TODO: Re-evaluate how exactly Clonezilla uses /NOT_FOUND and whether introducing it here # TODO: could improve Rescuezilla/Clonezilla interoperability. continue if 'pv_uuid' in vg_dict.keys(): pv_uuid = vg_dict['pv_uuid'] else: error_message += "Could not find physical volume UUID pv_uuid in " + str(vg_dict) + "\n" continue relevant_vg_name_list.append(vg_name) lv_state_dict = Lvm.get_logical_volume_state_dict(logger=None) for report_dict in lv_state_dict['report']: for lv_dict in report_dict['lv']: # Only consider VGs that match the partitions to backup list if 'vg_name' in lv_dict.keys() and lv_dict['vg_name'] in relevant_vg_name_list: vg_name = lv_dict['vg_name'] if 'lv_path' in lv_dict.keys(): lv_path = lv_dict['lv_path'] is_unmounted, message = Utility.umount_warn_on_busy(lv_path) if not is_unmounted: error_message += message else: # TODO: Make this logic better with self.lvm_lv_path_lock: self.lvm_lv_path_list.append(lv_path) else: continue # Stop the Logical Volume Manager (LVM) failed_logical_volume_list, failed_volume_group_list = Lvm.shutdown_lvm2(self.builder, None) for failed_volume_group in failed_volume_group_list: error_message += "Failed to shutdown Logical Volume Manager (LVM) Volume Group (VG): " + failed_volume_group[ 0] + "\n\n" + failed_volume_group[1] GLib.idle_add(self.post_lvm_preparation, is_overwriting_partition_table, False, error_message) return for failed_logical_volume in failed_logical_volume_list: error_message += "Failed to shutdown Logical Volume Manager (LVM) Logical Volume (LV): " + \ failed_logical_volume[0] + "\n\n" + failed_logical_volume[1] GLib.idle_add(self.post_lvm_preparation, is_overwriting_partition_table, False, error_message) return GLib.idle_add(self.post_lvm_preparation, is_overwriting_partition_table, True, error_message) except Exception as e: tb = traceback.format_exc() traceback.print_exc() message = "Unable to process Logical Volume Manager (LVMs): " + tb GLib.idle_add(self.post_lvm_preparation, is_overwriting_partition_table, False, error_message) GLib.idle_add(self.post_lvm_preparation, is_overwriting_partition_table, True, error_message) def overwrite_partition_table_toggle(self, is_overwriting_partition_table): print("Overwrite partition table toggle changed to " + str(is_overwriting_partition_table)) if is_overwriting_partition_table: # Don't need to scan for existing LVM logical volumes when overwriting partition table. self.post_lvm_preparation(is_overwriting_partition_table, True, "") else: self.win.set_sensitive(False) # Protect against accidentally overwriting the please_wait_popup reference when the checkbox is toggled # FIXME: Improve logic so this is not required if self.please_wait_popup is not None: self.please_wait_popup.destroy() self.please_wait_popup = None self.please_wait_popup = PleaseWaitModalPopup(self.builder, title=_("Please wait..."), message=_("Scanning and unmounting any Logical Volume Manager (LVM) Logical Volumes...")) self.please_wait_popup.show() if 'partitions' in self.dest_drive_dict.keys(): partitions = self.dest_drive_dict['partitions'] else: # TODO: Make this logic better. partitions = self.dest_drive_dict thread = threading.Thread(target=self._scan_and_unmount_existing_lvm, args=[copy.deepcopy(partitions).keys(), is_overwriting_partition_table]) thread.daemon = True thread.start() def post_lvm_preparation(self, is_overwriting_partition_table, is_lvm_shutdown_success, lvm_error_message): if self.please_wait_popup is not None: self.please_wait_popup.destroy() self.please_wait_popup = None if not is_lvm_shutdown_success or len(lvm_error_message) != 0: error = ErrorMessageModalPopup(self.builder, lvm_error_message) # Ensure that the overwrite partition table button stays checked. is_overwriting_partition_table = True for overwrite_partition_table_checkbutton in self.partition_table_checkbutton_dict: overwrite_partition_table_checkbutton.set_sensitive(self.selected_image.has_partition_table()) overwrite_partition_table_checkbutton.set_active(self.selected_image.has_partition_table()) self.set_overwriting_partition_warning_label(is_overwriting_partition_table) for mode in self.mode_list: self.destination_partition_combobox_cell_renderer_dict[mode].set_sensitive(not is_overwriting_partition_table) def change_combo_box(self, path_string, target_node_string, enduser_friendly_string): print( "Changing the combobox on row " + path_string + " to " + target_node_string + " / " + enduser_friendly_string) liststore_iter = self.partition_selection_list.get_iter(path_string) self._swap_destination_partition_node_with_backup(liststore_iter) self.partition_selection_list.set_value(liststore_iter, 3, target_node_string) self.partition_selection_list.set_value(liststore_iter, 4, enduser_friendly_string) # Automatically tick the restore checkbox self.partition_selection_list.set_value(liststore_iter, 1, True) def toggle_restore_of_row(self, iter, new_toggle_state): # Need to be able to disable restore of individual partitions. is_empty_dest_partition = False if self.partition_selection_list.get_value(iter, 5) == self.NOT_RESTORING_PARTITION_KEY: is_empty_dest_partition = True if new_toggle_state and is_empty_dest_partition: print("Blocking enabling the toggle when the destination partition is not set") error = ErrorMessageModalPopup(self.builder, _("No destination partition selected. Use the destination partition drop-down menu to select the destination")) return # Update the underlying model to ensure the checkbox will reflect the new state self.partition_selection_list.set_value(iter, 1, new_toggle_state) self._swap_destination_partition_node_with_backup(iter) # If the row has been disabled, update the combobox if not new_toggle_state: self.partition_selection_list.set_value(iter, 3, self.NOT_RESTORING_PARTITION_KEY) self.partition_selection_list.set_value(iter, 4, self.NOT_RESTORING_PARTITION_ENDUSER_FRIENDLY) def _use_image_partition_table(self): # Populate image partition list self.destination_partition_combobox_list.clear() self.partition_selection_list.clear() if isinstance(self.selected_image, ClonezillaImage) or isinstance(self.selected_image, RedoBackupLegacyImage) or \ isinstance(self.selected_image, FogProjectImage) or isinstance(self.selected_image, RedoRescueImage) or \ isinstance(self.selected_image, FoxcloneImage) or isinstance(self.selected_image, MetadataOnlyImage): for image_format_dict_key in self.selected_image.image_format_dict_dict.keys(): print("ClonezillaImage contains partition " + image_format_dict_key) if self.selected_image.does_image_key_belong_to_device(image_format_dict_key): if self.selected_image.image_format_dict_dict[image_format_dict_key]['is_lvm_logical_volume']: # The destination of an LVM logical volume within a partition (eg /dev/cl/root) is unchanged dest_partition = self.selected_image.image_format_dict_dict[image_format_dict_key][ 'logical_volume_long_device_node'] flat_description = "Logical Volume " + image_format_dict_key + ": " + self.selected_image.flatten_partition_string(image_format_dict_key) else: # The destination partition of a regular partition in the image (eg, /dev/sda4) is dependent on # the destination drive node (eg /dev/sdb) so we need to split and join the device so the # mapping correctly reads "/dev/sdb4". image_base_device_node, image_partition_number = Utility.split_device_string(image_format_dict_key) # Combine image partition number with destination device node base dest_partition = Utility.join_device_string(self.dest_drive_node, image_partition_number) flat_description = _("Partition {partition_number}").format(partition_number=str( image_partition_number)) + ": " + self.selected_image.flatten_partition_string(image_format_dict_key) self.destination_partition_combobox_list.append([dest_partition, flat_description]) self.partition_selection_list.append( [image_format_dict_key, True, flat_description, dest_partition, flat_description, dest_partition, flat_description]) elif isinstance(self.selected_image, FsArchiverImage): # Doesn't appear that FsArchiver images ever have an partition table backup associated with it. But # keeping this section for reference, especially if a frontend like qt-fsarchiver adds partition table # backups. for fs_key in self.selected_image.fsa_dict['filesystems'].keys(): long_device_node = self.selected_image.fsa_dict['filesystems'][fs_key]['original_long_device_node'] image_base_device_node, image_partition_number = Utility.split_device_string(long_device_node) # Combine image partition number with destination device node base dest_partition = Utility.join_device_string(self.dest_drive_node, image_partition_number) flat_description = _("Partition {partition_number}").format(partition_number=str( image_partition_number)) + " (" + dest_partition + "): " + self.selected_image.flatten_partition_string( fs_key) self.destination_partition_combobox_list.append([dest_partition, flat_description]) self.partition_selection_list.append( [fs_key, True, flat_description, dest_partition, flat_description, dest_partition, flat_description]) elif isinstance(self.selected_image, ApartGtkImage): # Shouldn't be called because ApartGTK images don't have a partition table print("Error: Images created with apart-gtk don't have partition tables") for mode in self.mode_list: self.destination_partition_combobox_cell_renderer_dict[mode].set_sensitive(False) def _use_existing_drive_partition_table(self): self.destination_partition_combobox_list.clear() self.partition_selection_list.clear() num_destination_partitions = 0 with self.lvm_lv_path_lock: for lvm_lv_path in self.lvm_lv_path_list: self.destination_partition_combobox_list.append([lvm_lv_path, "Logical Volume: " + lvm_lv_path]) num_destination_partitions += 1 print("Looking at " + str(self.selected_image) + " and " + str(self.dest_drive_dict)) # For the safety of end-users, ensure the initial combobox mapping is blank. It's possible to autogenerate a # mapping, but this could be wrong so far simpler for now to leave the mapping blank and rely on end-user # decisions. flattened_part_description = self.NOT_RESTORING_PARTITION_ENDUSER_FRIENDLY dest_partition_key = self.NOT_RESTORING_PARTITION_KEY is_restoring_partition = False # Populate image partition selection list (left-hand side column) if isinstance(self.selected_image, ClonezillaImage) or isinstance(self.selected_image, RedoBackupLegacyImage) or \ isinstance(self.selected_image, FogProjectImage) or isinstance(self.selected_image, RedoRescueImage) or \ isinstance(self.selected_image, FoxcloneImage) or isinstance(self.selected_image, ApartGtkImage) or \ isinstance(self.selected_image, MetadataOnlyImage): for image_format_dict_key in self.selected_image.image_format_dict_dict.keys(): if self.selected_image.does_image_key_belong_to_device(image_format_dict_key): if self.selected_image.image_format_dict_dict[image_format_dict_key]['is_lvm_logical_volume']: flat_image_part_description = "Logical Volume " + image_format_dict_key + ": "\ + self.selected_image.flatten_partition_string(image_format_dict_key) elif isinstance(self.selected_image, ApartGtkImage): # ApartGtkImage may contain multiple partitions, so the key contains the timestamp too. Therefore # need to make sure the split device string function doesn't get called flat_image_part_description = image_format_dict_key + ": "\ + self.selected_image.flatten_partition_string(image_format_dict_key) else: image_base_device_node, image_partition_number = Utility.split_device_string(image_format_dict_key) flat_image_part_description = _("Partition {partition_number}").format(partition_number=str( image_partition_number)) + ": "\ + self.selected_image.flatten_partition_string(image_format_dict_key) self.partition_selection_list.append( [image_format_dict_key, is_restoring_partition, flat_image_part_description, dest_partition_key, flattened_part_description, dest_partition_key, flattened_part_description]) num_destination_partitions += 1 elif isinstance(self.selected_image, FsArchiverImage): for fs_key in self.selected_image.fsa_dict['filesystems'].keys(): flat_image_part_description = "Filesystem " + str( fs_key) + ": " + self.selected_image.flatten_partition_string(fs_key) self.partition_selection_list.append( [fs_key, is_restoring_partition, flat_image_part_description, dest_partition_key, flattened_part_description, dest_partition_key, flattened_part_description]) num_destination_partitions += 1 if num_destination_partitions == 0: # The destination disk must be empty. self.partition_selection_list.append( [self.dest_drive_node, is_restoring_partition, flat_image_part_description, self.dest_drive_node, flattened_part_description, dest_partition_key, flattened_part_description]) # Populate combobox (right-hand side column) num_combo_box_entries = 0 is_destination_partition_target_drive = False if 'partitions' in self.dest_drive_dict.keys() and len(self.dest_drive_dict['partitions'].keys()) > 0: # Loop over the partitions in in the destination drive for dest_partition_key in self.dest_drive_dict['partitions'].keys(): if 'type' in self.dest_drive_dict['partitions'][dest_partition_key].keys() and self.dest_drive_dict['partitions'][dest_partition_key]['type'] == "extended": # Do not add a destination combobox entry for any Extended Boot Record (EBR) destination partition # nodes to reduce risk of user confusion. continue if dest_partition_key == self.dest_drive_node: is_destination_partition_target_drive = True flattened_part_description = dest_partition_key + ": " + CombinedDriveState.flatten_part( self.dest_drive_dict['partitions'][dest_partition_key]) self.destination_partition_combobox_list.append([dest_partition_key, flattened_part_description]) num_combo_box_entries += 1 # If there is no partitions on the destination disk, provide the option to remap the partitions to the whole # destination disk. If the source image doesn't have a partition table, also want to be able to remap partitons # to the destination disk. Finally, if the destination disk already has a filesystem directly on disk then # that would have already been handled above and there's no need to add a new entry to the combobox. if (num_combo_box_entries == 0 or not self.selected_image.has_partition_table()) and not is_destination_partition_target_drive: flattened_disk_description = self.dest_drive_node + ": " + CombinedDriveState.flatten_drive(self.dest_drive_dict) # If there are no partitions in the destination drive, we place the entire drive as the destination self.destination_partition_combobox_list.append([self.dest_drive_node, "WHOLE DRIVE " + flattened_disk_description]) for mode in self.mode_list: self.destination_partition_combobox_cell_renderer_dict[mode].set_sensitive(True) def _swap_destination_partition_node_with_backup(self, liststore_iter): current_value = self.partition_selection_list.get_value(liststore_iter, 3) old_value = self.partition_selection_list.get_value(liststore_iter, 5) self.partition_selection_list.set_value(liststore_iter, 3, old_value) self.partition_selection_list.set_value(liststore_iter, 5, current_value) current_value = self.partition_selection_list.get_value(liststore_iter, 4) old_value = self.partition_selection_list.get_value(liststore_iter, 6) self.partition_selection_list.set_value(liststore_iter, 4, old_value) self.partition_selection_list.set_value(liststore_iter, 6, current_value)
class ImageFolderQuery: def __init__(self, builder, image_list_store): self.image_dict = {} # Relying on CPython GIL to communicate between threads. self.failed_to_read_image_dict = {} self.builder = builder self.image_list_store = image_list_store self.win = self.builder.get_object("main_window") self.icon_pixbufs = { "RESCUEZILLA_1.5_FORMAT": self.builder.get_object( "rescuezilla_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "REDOBACKUP_0.9.8_1.0.4_FORMAT": self.builder.get_object( "redobackup_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "CLONEZILLA_FORMAT": self.builder.get_object( "clonezilla_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR), "warning": self.builder.get_object("warning_icon").get_pixbuf().scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR) } self.backup_label = self.builder.get_object("backup_folder_label") self.restore_label = self.builder.get_object("restore_folder_label") self.verify_label = self.builder.get_object("verify_folder_label") self.image_explorer_folder_label = self.builder.get_object( "image_explorer_folder_label") self.query_path = MOUNT_DIR self.requested_stop_lock = threading.Lock() self.requested_stop = False self.image_folder_query_in_progress = False def is_stop_requested(self): with self.requested_stop_lock: return self.requested_stop def is_image_folder_query_in_progress(self): return self.image_folder_query_in_progress def cancel_image_folder_query(self): with self.requested_stop_lock: self.requested_stop = True return def query_folder(self, path): with self.requested_stop_lock: self.requested_stop = False self.query_path = path self.image_list_store.clear() print("Starting scan of provided path " + self.query_path) self.backup_label.set_text(self.query_path) self.restore_label.set_text(self.query_path) self.verify_label.set_text(self.query_path) self.image_explorer_folder_label.set_text(self.query_path) self.image_list_store.clear() self.failed_to_read_image_dict.clear() self.win.set_sensitive(False) self.please_wait_popup = PleaseWaitModalPopup( self.builder, title=_("Please wait..."), message=_("Scanning folder for backup images...") + "\n\n" + _("Close this popup to cancel scanning the selected folder and subfolders." ), on_close_callback=self.cancel_image_folder_query) self.please_wait_popup.show() thread = threading.Thread(target=self.scan_image_directory) thread.daemon = True thread.start() def _populate_image_list_table(self): print("Populating image list table. Image dict is length: " + str(len(self.image_dict))) self.image_list_store.clear() traceback_messages = "" for key in self.image_dict.keys(): try: image = self.image_dict[key] format = image.image_format if len(image.warning_dict.keys()) > 0: warning_icon = self.icon_pixbufs['warning'] else: warning_icon = None self.image_list_store.append([ key, format, warning_icon, self.icon_pixbufs[format], image.enduser_filename, image.enduser_readable_size, str(image.last_modified_timestamp), image.get_enduser_friendly_partition_description() ]) except Exception as e: tb = traceback.format_exc() traceback_messages += tb + "\n\n" # Highlight first image if there is only 1 image. if len(self.image_dict.keys()) == 1: self.builder.get_object( "restore_image_selection_treeselection").select_path(0) if len(self.failed_to_read_image_dict.keys()) > 0: for key in self.failed_to_read_image_dict.keys(): traceback_messages += key + ": " + self.failed_to_read_image_dict[ key] + "\n\n" if len(traceback_messages) > 0: ErrorMessageModalPopup( self.builder, _("Error processing the following images:") + "\n\n" + str(traceback_messages)) self.please_wait_popup.destroy() def scan_file(self, absolute_path, filename, enduser_filename): print("Scan file " + absolute_path) is_image = False try: image = None if isfile(absolute_path): # Identify Clonezilla images by presence of a file named "parts". Cannot use "clonezilla-img" or # "dev-fs.list" because these files were not created by in earlier Clonezilla versions. Cannot use # "disk" as Clonezilla's 'saveparts' function does not create it. But both 'savedisk' and 'saveparts' # always creates a file named 'parts' across every version of Clonezilla tested. error_suffix = "" if absolute_path.endswith("parts"): print("Found Clonezilla image " + filename) image = ClonezillaImage(absolute_path, enduser_filename) error_suffix = _( "This can happen when loading images which Clonezilla was unable to completely backup. Any other filesystems within the image should be restorable as normal." ) is_image = True elif absolute_path.endswith(".backup"): print( "Found a legacy Redo Backup / Rescuezilla v1.0.5 image " + filename) image = RedoBackupLegacyImage(absolute_path, enduser_filename, filename) error_suffix = _( "Any other filesystems within the image should be restorable as normal." ) is_image = True if is_image: image_warning_message = "" for short_partition_key in image.warning_dict.keys(): image_warning_message += " " + short_partition_key + ": " + image.warning_dict[ short_partition_key] + "\n" if len(image_warning_message) > 0: self.failed_to_read_image_dict[absolute_path] = _( "Unable to fully process the image associated with the following partitions:" ) + "\n" + image_warning_message + error_suffix if image is not None: self.image_dict[image.absolute_path] = image except Exception as e: print("Failed to read: " + absolute_path) tb = traceback.format_exc() self.failed_to_read_image_dict[enduser_filename] = tb traceback.print_exc() return is_image def scan_image_directory(self): self.image_dict.clear() self.failed_to_read_image_dict.clear() try: # list files and directories for filename in os.listdir(self.query_path): if self.is_stop_requested(): break abs_base_scan_path = os.path.abspath( join(self.query_path, filename)) print("Scanning " + abs_base_scan_path) if isfile(abs_base_scan_path): print("Scanning file " + abs_base_scan_path) is_image = self.scan_file(abs_base_scan_path, filename, filename) if is_image: GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Scanned: {filename}").format(filename=filename)) elif isdir(abs_base_scan_path): # List the subdirectory (1 level deep) for subdir_filename in os.listdir(abs_base_scan_path): if self.is_stop_requested(): break absolute_path = join(abs_base_scan_path, subdir_filename) enduser_filename = os.path.join( filename, subdir_filename) if isfile(absolute_path): print("Scanning subdir file " + absolute_path) is_image = self.scan_file(absolute_path, subdir_filename, enduser_filename) if is_image: GLib.idle_add( self.please_wait_popup. set_secondary_label_text, _("Scanned: {filename}").format( filename=enduser_filename)) except Exception as e: tb = traceback.format_exc() GLib.idle_add( ErrorMessageModalPopup.display_nonfatal_warning_message, self.builder, "Failed to scan for images: " + tb) # Relying on CPython GIL to access the self.image_list GLib.idle_add(self._populate_image_list_table)
class DriveQuery: def __init__(self, builder, drive_list_store, save_partition_list_store, mount_partition_list_store): self.builder = builder self.drive_list_store = drive_list_store self.save_partition_list_store = save_partition_list_store self.mount_partition_list_store = mount_partition_list_store self.win = self.builder.get_object("main_window") self._is_displaying_advanced_information_lock = threading.Lock() self._is_displaying_advanced_information = False def cancel_query(self): with self.requested_stop_lock: self.requested_stop = True return def is_stop_requested(self): with self.requested_stop_lock: return self.requested_stop def start_query(self, error_message_callback): print("Starting drive query...") self.win.set_sensitive(False) self.requested_stop_lock = threading.Lock() self.requested_stop = False self.please_wait_popup = PleaseWaitModalPopup( self.builder, title=_("Please wait..."), message=_("Identifying disk drives..."), on_close_callback=self.cancel_query) self.please_wait_popup.show() self.error_message_callback = error_message_callback thread = threading.Thread(target=self._do_drive_query_wrapper) thread.daemon = True thread.start() def set_show_hidden_information(self, is_displaying_advanced_information): # User-interface sensitivity acting as further crude protection prevent inconsistent state. TODO: Improve this design. with self._is_displaying_advanced_information_lock: self._is_displaying_advanced_information = is_displaying_advanced_information def populate_drive_selection_table(self): self.drive_list_store.clear() index = 0 for drive_key in self.drive_state.keys(): try: drive = self.drive_state[drive_key] with self._is_displaying_advanced_information_lock: if self._is_displaying_advanced_information: # Display a advanced-user partition name eg, "nvme0n1". Users coming from Clonezilla will often # like to know the device node. human_friendly_drive_name = drive_key else: # Display a user-friendly drive name eg, "3" to refer to nvme0n1.Some Rescuezilla users may prefer # drives identified by a simple digit (eg, drive #3), because they may not understand what a short # device node like "nvme0n1" means. human_friendly_drive_name = "#" + str(index + 1) if (drive['type'] != 'disk' and not drive['type'].startswith("raid"))\ or drive['has_raid_member_filesystem'] or 'nbd' in drive_key : # Hiding LVMs, loop devices, empty drives etc from initial drive selection list. This # should greatly reduce the risk a user accidentally picks a logical volume (of their # say, encrypted Debian system) when they were actually intending on picking the entire # block device (including boot partition etc). # # Don't display non-block device if we are hiding them (like /dev/loop) continue flattened_partition_list = CombinedDriveState.flatten_partition_list( drive) print("For key " + drive_key + ", flattened partition list is " + flattened_partition_list) enduser_readable_capacity = Utility.human_readable_filesize( int(drive['capacity'])) self.drive_list_store.append([ drive_key, human_friendly_drive_name, enduser_readable_capacity, drive['model'], drive['serial'], flattened_partition_list ]) index = index + 1 except Exception as e: traceback.print_exc(file=sys.stdout) print("Could not process " + drive_key) continue # TODO: Don't populate mount partition here self.populate_mount_partition_table() if self.please_wait_popup is not None: self.please_wait_popup.destroy() self.please_wait_popup = None def populate_partition_selection_table(self, drive_key): print('Received drive key ' + drive_key) print('drive state is ' + str(self.drive_state)) self.save_partition_list_store.clear() try: if 'partitions' in self.drive_state[drive_key].keys(): for partition_key in self.drive_state[drive_key][ 'partitions'].keys(): flattened_partition_description = CombinedDriveState.flatten_partition_description( self.drive_state, drive_key, partition_key) # Add row that's ticked self.save_partition_list_store.append( [partition_key, True, flattened_partition_description]) else: # Add the drive itself flattened_partition_description = CombinedDriveState.flatten_partition_description( self.drive_state, drive_key, drive_key) # Add row that's ticked self.save_partition_list_store.append( [drive_key, True, flattened_partition_description]) except Exception as exception: tb = traceback.format_exc() traceback.print_exc() ErrorMessageModalPopup.display_nonfatal_warning_message( self.builder, tb) return def populate_mount_partition_table(self, ignore_drive_key=None): print('drive state is ' + str(self.drive_state)) self.mount_partition_list_store.clear() index = 0 for drive_key in self.drive_state.keys(): try: if drive_key == ignore_drive_key or 'nbd' in drive_key: continue if 'partitions' not in self.drive_state[drive_key].keys(): continue for partition_key in self.drive_state[drive_key][ 'partitions'].keys(): with self._is_displaying_advanced_information_lock: if self._is_displaying_advanced_information: # Display a advanced-user partition name eg, "nvme0n1p1". human_friendly_partition_name = partition_key else: if self.drive_state[drive_key][ 'type'] == 'loop' or self.drive_state[ drive_key][ 'has_raid_member_filesystem']: # Don't display certain non-block device if user has chosen to hide them. # TODO: Evaluate other partition types to be hidden. continue # Display a advanced-user partition name eg, "#4". human_friendly_partition_name = "#" + str(index + 1) flattened_partition_description = CombinedDriveState.flatten_partition_description( self.drive_state, drive_key, partition_key) if 'size' in self.drive_state[drive_key]['partitions'][ partition_key].keys(): size_in_bytes = self.drive_state[drive_key][ 'partitions'][partition_key]['size'] enduser_readable_size = Utility.human_readable_filesize( int(size_in_bytes)) else: enduser_readable_size = "unknown_size" self.mount_partition_list_store.append([ partition_key, human_friendly_partition_name, enduser_readable_size, flattened_partition_description ]) index = index + 1 except Exception as exception: tb = traceback.format_exc() traceback.print_exc() ErrorMessageModalPopup.display_nonfatal_warning_message( self.builder, tb) return def _get_parted_cmd_list(self, partition_longdevname): # TODO: Consider switching to using parted's --machine flag to get easily parseable output for internal # TODO: Rescuezilla usage. Note: The Clonezilla image format does *not* use the --machine flag. return ["parted", "-s", partition_longdevname, "unit", "B", "print"] def _get_sfdisk_cmd_list(self, partition_longdevname): return ["sfdisk", "--dump", partition_longdevname] def _do_drive_query_wrapper(self): try: self._do_drive_query() except Exception as exception: tb = traceback.format_exc() traceback.print_exc() GLib.idle_add(self.error_message_callback, False, _("Error querying drives: ") + tb) return def _do_drive_query(self): env_C_locale = Utility.get_env_C_locale() drive_query_start_time = datetime.now() GLib.idle_add(self.please_wait_popup.set_secondary_label_text, _("Unmounting: {path}").format(path=IMAGE_EXPLORER_DIR)) returncode, failed_message = ImageExplorerManager._do_unmount( IMAGE_EXPLORER_DIR) if not returncode: GLib.idle_add( self.error_message_callback, False, _("Unable to shutdown Image Explorer") + "\n\n" + failed_message) GLib.idle_add(self.please_wait_popup.destroy) return if self.is_stop_requested(): GLib.idle_add(self.error_message_callback, False, _("Operation cancelled by user.")) return GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Unmounting: {path}").format(path=RESCUEZILLA_MOUNT_TMP_DIR)) returncode, failed_message = ImageExplorerManager._do_unmount( RESCUEZILLA_MOUNT_TMP_DIR) if not returncode: GLib.idle_add( self.error_message_callback, False, _("Unable to unmount {path}").format( path=RESCUEZILLA_MOUNT_TMP_DIR) + "\n\n" + failed_message) GLib.idle_add(self.please_wait_popup.destroy) return if self.is_stop_requested(): GLib.idle_add(self.error_message_callback, False, _("Operation cancelled by user.")) return lsblk_cmd_list = [ "lsblk", "-o", "KNAME,NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL,SERIAL", "--paths", "--bytes", "--json" ] blkid_cmd_list = ["blkid"] os_prober_cmd_list = ["os-prober"] lsblk_json_dict = {} blkid_dict = {} os_prober_dict = {} parted_dict_dict = collections.OrderedDict([]) sfdisk_dict_dict = collections.OrderedDict([]) # Clonezilla combines drive, partition and filesystem from multiple data sources (lsblk, blkid, parted etc) # Rescuezilla continues this approach to reach best possible Clonezilla compatibility. # # However this sequential querying is slow. A parallel approach should be in theory much faster (but might be # less reliable if internal commands are creating file locks etc.) # # In practice, the sequential approach was about 25% faster than a first-cut (polling-based) parallel approach. # Parallel mode currently disabled, but kept for further development/analysis. mode = "sequential-drive-query" if mode == "sequential-drive-query": print("Running drive query in sequential mode") # TODO: Run with Utility.interruptable_run() so that even long-lived commands can have a signal sent to it # to shutdown early. # Not checking return codes here because Clonezilla does not, and some of these commands are expected to # fail. The Utility.run() command prints the output to stdout. GLib.idle_add(self.please_wait_popup.set_secondary_label_text, _("Running: {app}").format(app="lsblk")) process, flat_command_string, fail_description = Utility.run( "lsblk", lsblk_cmd_list, use_c_locale=True) lsblk_json_dict = json.loads(process.stdout) if self.is_stop_requested(): GLib.idle_add(self.error_message_callback, False, _("Operation cancelled by user.")) return GLib.idle_add(self.please_wait_popup.set_secondary_label_text, _("Running: {app}").format(app="blkid")) process, flat_command_string, fail_description = Utility.run( "blkid", blkid_cmd_list, use_c_locale=True) blkid_dict = Blkid.parse_blkid_output(process.stdout) if self.is_stop_requested(): GLib.idle_add(self.error_message_callback, False, _("Operation cancelled by user.")) return GLib.idle_add(self.please_wait_popup.set_secondary_label_text, _("Running: {app}").format(app="os-prober")) # Use os-prober to get OS information (running WITH original locale information process, flat_command_string, fail_description = Utility.run( "osprober", os_prober_cmd_list, use_c_locale=True) os_prober_dict = OsProber.parse_os_prober_output(process.stdout) if self.is_stop_requested(): GLib.idle_add(self.error_message_callback, False, _("Operation cancelled by user.")) return for lsblk_dict in lsblk_json_dict['blockdevices']: partition_longdevname = lsblk_dict['name'] print("Going to run parted and sfdisk on " + partition_longdevname) try: GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Running {app} on {device}").format( app="parted", device=partition_longdevname)) process, flat_command_string, fail_description = Utility.run( "parted", self._get_parted_cmd_list(partition_longdevname), use_c_locale=True) if "unrecognized disk label" not in process.stderr: parted_dict_dict[ partition_longdevname] = Parted.parse_parted_output( process.stdout) else: print("Parted says " + process.stderr) if self.is_stop_requested(): GLib.idle_add(self.error_message_callback, False, _("Operation cancelled by user.")) return GLib.idle_add( self.please_wait_popup.set_secondary_label_text, _("Running {app} on {device}").format( app="sfdisk", device=partition_longdevname)) process, flat_command_string, fail_description = Utility.run( "sfdisk", self._get_sfdisk_cmd_list(partition_longdevname), use_c_locale=True) sfdisk_dict_dict[ partition_longdevname] = Sfdisk.parse_sfdisk_dump_output( process.stdout) if self.is_stop_requested(): GLib.idle_add(self.error_message_callback, False, _("Operation cancelled by user.")) return except Exception: print("Could run run parted on " + partition_longdevname) elif mode == "parallel-drive-query": print("Running drive query in parallel mode") # Launch drive query in parallel. Parallel Python subprocess.Popen() approach adapted from [1] # [1] https://stackoverflow.com/a/636601 cmd_dict = { ('lsblk', ""): subprocess.Popen(lsblk_cmd_list, env=env_C_locale, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", universal_newlines=True), ('blkid', ""): subprocess.Popen(blkid_cmd_list, env=env_C_locale, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", universal_newlines=True), ('os_prober', ""): subprocess.Popen(os_prober_cmd_list, env=env_C_locale, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", universal_newlines=True), } while cmd_dict: print("drive_query_process is length " + str(len(cmd_dict)) + " with contents " + str(cmd_dict)) for key in list(cmd_dict.keys()): proc = cmd_dict[key] retcode = proc.poll() if retcode is not None: # Process finished. cmd_dict.pop(key, None) if key[0] == "lsblk" and retcode == 0: # lsblk is complete, partition information can be used to launch the parted/sfdisk lsblk_json_dict = json.loads(proc.stdout.read()) for lsblk_dict in lsblk_json_dict['blockdevices']: partition_longdevname = lsblk_dict['name'] print("Launching parted and sfdisk on " + partition_longdevname) try: cmd_dict[("parted", partition_longdevname )] = subprocess.Popen( self._get_parted_cmd_list( partition_longdevname), env=env_C_locale, encoding="utf-8", universal_newlines=True) cmd_dict[("sfdisk", partition_longdevname )] = subprocess.Popen( self._get_sfdisk_cmd_list( partition_longdevname), env=env_C_locale, encoding="utf-8", universal_newlines=True) except Exception: print("Could launch sfdisk or parted on " + partition_longdevname) elif key[0] == "blkid" and retcode == 0: blkid_dict = Blkid.parse_blkid_output( proc.stdout.read()) elif key[0] == "osprober" and retcode == 0: os_prober_dict = OsProber.parse_os_prober_output( proc.stdout.read()) elif key[ 0] == "sfdisk" and retcode == 0 and proc.stdout is not None: sfdisk_dict_dict[ key[1]] = Sfdisk.parse_sfdisk_dump_output( proc.stdout.read()) elif key[ 0] == "parted" and retcode == 0 and proc.stdout is not None: if proc.stderr is not None: stderr = proc.stderr.read() print("parted with key " + str(key) + " had stderr " + stderr) if "unrecognized disk label" not in stderr: parted_dict_dict[ key[1]] = Parted.parse_parted_output( proc.stdout.read()) else: print( "COULD NOT PROCESS process launched with key " + str(key) + " return code" + str(retcode)) if proc.stdout is not None: print("stdout:" + proc.stdout.read()) if proc.stderr is not None: print(" stderr:" + proc.stderr.read()) else: # No process is done, wait a bit and check again. time.sleep(0.1) continue else: raise Exception("Invalid drive query mode") self.drive_state = CombinedDriveState.construct_combined_drive_state_dict( lsblk_json_dict, blkid_dict, os_prober_dict, parted_dict_dict, sfdisk_dict_dict) pp = pprint.PrettyPrinter(indent=4) pp.pprint(self.drive_state) drive_query_end_time = datetime.now() print("Drive query took: " + str((drive_query_end_time - drive_query_start_time))) GLib.idle_add(self.populate_drive_selection_table)
class MountNetworkPath: def __init__(self, builder, callback, mode, network_widget_dict, destination_path): # Lowercase mode (eg "backup", "restore", "verify") mode_prefix = mode.name.lower() settings = { 'server': network_widget_dict["network_server"][mode].get_text().strip(), 'username': network_widget_dict["network_username"][mode].get_text().strip(), # For protocols that specify the remote path separately from the server 'remote_path': network_widget_dict["network_remote_path"] [mode].get_text().strip(), # NOT stripping whitespace from the password 'password': network_widget_dict["network_password"][mode].get_text(), 'domain': network_widget_dict["network_domain"][mode].get_text().strip(), 'version': network_widget_dict["network_version"][mode].get_text().strip(), 'ssh_idfile': network_widget_dict["network_ssh_idfile"][mode].get_text().strip(), 'destination_path': destination_path } network_protocol_key = Utility.get_combobox_key( network_widget_dict['network_protocol_combobox'][mode]) # restore_network_version self.callback = callback self.requested_stop_lock = threading.Lock() self.requested_stop = False self.please_wait_popup = PleaseWaitModalPopup( builder, title=_("Please wait..."), message=_("Mounting...") + "\n\n" + _("Close this popup to cancel the mount operation."), on_close_callback=self.cancel_mount) self.please_wait_popup.show() if network_protocol_key == "SMB": thread = threading.Thread(target=self._do_smb_mount_command, args=(settings, )) elif network_protocol_key == "SSH": thread = threading.Thread(target=self._do_ssh_mount_command, args=(settings, )) elif network_protocol_key == "NFS": thread = threading.Thread(target=self._do_nfs_mount_command, args=(settings, )) else: raise ValueError("Unknown network protocol: " + network_protocol_key) thread.daemon = True thread.start() def cancel_mount(self): with self.requested_stop_lock: self.requested_stop = True return def is_stop_requested(self): with self.requested_stop_lock: return self.requested_stop def _do_smb_mount_command(self, settings): destination_path = settings['destination_path'] try: if not os.path.exists(destination_path) and not os.path.isdir( destination_path): os.mkdir(destination_path, 0o755) is_unmounted, message = Utility.umount_warn_on_busy( destination_path) if not is_unmounted: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, message) return smb_arguments = "" credentials_string = "" tmp = tempfile.NamedTemporaryFile() if settings['username'] != "": credentials_string = "username="******"\n" else: # The mount.cifs man page states "If [the username field] is not given, then the environment variable # USER is used". However in practice, for anonymous Windows network shared folders some username must # be specified. Trying to pass in a blank username returns makes mount.cifs return "username # specified with no parameter". Some users have set an asterisk character [1], but any username works. # Therefore choosing using 'rescuezilla' to provide more descriptive logs for system administrators. # https://github.com/rescuezilla/rescuezilla/issues/190 credentials_string = "username=rescuezilla" + "\n" if settings['password'] != "": credentials_string += "password="******"\n" if settings['domain'] != "": credentials_string += "domain=" + settings['domain'] + "\n" smb_arguments += "credentials=" + tmp.name if settings['password'] == "": smb_arguments += ",guest" if settings['version'] != "": if smb_arguments != "": smb_arguments += "," smb_arguments += "vers=" + settings['version'] with open(tmp.name, 'w') as f: f.write(credentials_string) f.flush() mount_cmd_list = [ 'mount.cifs', settings['server'], settings['destination_path'], "-o", smb_arguments ] mount_process, mount_flat_command_string, mount_failed_message = Utility.interruptable_run( "Mounting SMB/CIFS network shared folder: ", mount_cmd_list, use_c_locale=False, is_shutdown_fn=self.is_stop_requested) shred_cmd_list = ['shred', tmp.name] shred_process, shred_flat_command_string, failed_message = Utility.run( "Shredding credentials temp file: ", shred_cmd_list, use_c_locale=False) # Delete temp file tmp.close() if shred_process.returncode != 0: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, failed_message) return if mount_process.returncode != 0: check_password_msg = _( "Please ensure the username, password and other fields provided are correct, and try again." ) GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add( self.callback, False, mount_failed_message + "\n\n" + check_password_msg) return else: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, True, "", destination_path) except Exception as e: tb = traceback.format_exc() print(tb) GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, "Error mounting SMB/CIFS folder: " + tb) def _do_ssh_mount_command(self, settings): destination_path = settings['destination_path'] try: if not os.path.exists(destination_path) and not os.path.isdir( destination_path): os.mkdir(destination_path, 0o755) is_unmounted, message = Utility.umount_warn_on_busy( destination_path) if not is_unmounted: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, message) return source_string = "" # Username is optional in SSH, it uses the current user if not specified. if settings['username'] != "": source_string = settings['username'] + "@" if settings['server'] != "": source_string += settings['server'] else: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, "Must specify server.") return if settings['remote_path'] != "": source_string += ":" + settings['remote_path'] else: # If no remote path specified, assume the user wants to mount the root directory of their remote server. source_string += ":/" mount_cmd_list = [ "sshfs", source_string, settings['destination_path'] ] if settings['password'] == "" and settings['ssh_idfile'] == "": GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add( self.callback, False, _("Must provide either password or SSH identity file.")) return ssh_cmd = "" tmp = tempfile.NamedTemporaryFile(delete=False) if settings['password'] != "": with open(tmp.name, 'w') as f: f.write(settings['password'] + "\n") f.flush() ssh_cmd += "sshpass -f " + tmp.name + " " ssh_cmd += "ssh -o StrictHostKeyChecking=no" if settings['ssh_idfile'] != "": ssh_cmd += ",IdentityFile=" + settings[ 'ssh_idfile'] + ",BatchMode=yes" mount_cmd_list.append('-o') # In the Python subprocess.run() cmd_list, the ssh_cmd variable cannot be surrounded by quotes mount_cmd_list.append('ssh_command=' + ssh_cmd) mount_process, mount_flat_command_string, mount_failed_message = Utility.interruptable_run( "Mounting network shared folder with SSH: ", mount_cmd_list, use_c_locale=False, is_shutdown_fn=self.is_stop_requested) shred_cmd_list = ['shred', tmp.name] shred_process, shred_flat_command_string, failed_message = Utility.run( "Shredding credentials temp file: ", shred_cmd_list, use_c_locale=False) # Delete temp file os.remove(tmp.name) if shred_process.returncode != 0: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, failed_message) return if mount_process.returncode != 0: check_password_msg = _( "Please ensure the username, password and other fields provided are correct, and try again." ) GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add( self.callback, False, mount_failed_message + "\n\n" + check_password_msg) return else: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, True, "", destination_path) except Exception as e: tb = traceback.format_exc() print(tb) GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, "Error mounting SSH folder: " + tb) def _do_nfs_mount_command(self, settings): destination_path = settings['destination_path'] try: if not os.path.exists(destination_path) and not os.path.isdir( destination_path): os.mkdir(destination_path, 0o755) is_unmounted, message = Utility.umount_warn_on_busy( destination_path) if not is_unmounted: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, message) return if settings['server'] != "": server = settings['server'] else: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, "Must specify server.") return if settings['remote_path'] != "": exported_dir = settings['remote_path'] else: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, "Must specify exported directory.") return mount_cmd_list = [ "mount.nfs", server + ":" + exported_dir, settings['destination_path'] ] mount_process, mount_flat_command_string, mount_failed_message = Utility.interruptable_run( "Mounting network shared folder with NFS: ", mount_cmd_list, use_c_locale=False, is_shutdown_fn=self.is_stop_requested) if mount_process.returncode != 0: check_password_msg = _( "Please ensure the server and exported path are correct, and try again." ) GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add( self.callback, False, mount_failed_message + "\n\n" + check_password_msg) return else: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, True, "", destination_path) except Exception as e: tb = traceback.format_exc() print(tb) GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, "Error mounting NFS folder: " + tb)
class MountNetworkPath: def __init__(self, builder, callback, mode, destination_path): # Lowercase mode (eg "backup", "restore", "verify") mode_prefix = mode.name.lower() settings = { 'server': builder.get_object(mode_prefix + "_network_server").get_text(), 'username': builder.get_object(mode_prefix + "_network_username").get_text(), 'password': builder.get_object(mode_prefix + "_network_password").get_text(), 'domain': builder.get_object(mode_prefix + "_network_domain").get_text(), 'version': builder.get_object(mode_prefix + "_network_version").get_text(), 'destination_path': destination_path } # restore_network_version self.callback = callback self.please_wait_popup = PleaseWaitModalPopup( builder, title=_("Please wait..."), message=_("Mounting...")) self.please_wait_popup.show() thread = threading.Thread(target=self._do_mount_command, args=(settings, )) thread.daemon = True thread.start() def _do_mount_command(self, settings): destination_path = settings['destination_path'] try: if not os.path.exists(destination_path) and not os.path.isdir( destination_path): os.mkdir(destination_path, 0o755) is_unmounted, message = Utility.umount_warn_on_busy( destination_path) if not is_unmounted: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, message) return smb_arguments = "" credentials_string = "" tmp = tempfile.NamedTemporaryFile() if settings['username'] != "": credentials_string = "username="******"\n" if settings['password'] != "": credentials_string += "password="******"\n" if settings['domain'] != "": credentials_string += "domain=" + settings['domain'] + "\n" if settings['username'] != "" or settings['password'] != "": smb_arguments += "credentials=" + tmp.name if credentials_string == "": smb_arguments += "guest" elif settings['password'] == "": smb_arguments += ",guest" if settings['version'] != "": if smb_arguments != "": smb_arguments += "," smb_arguments += "vers=" + settings['version'] with open(tmp.name, 'w') as f: f.write(credentials_string) f.flush() mount_cmd_list = [ 'mount.cifs', settings['server'], settings['destination_path'], "-o", smb_arguments ] mount_process, mount_flat_command_string, mount_failed_message = Utility.run( "Mounting network shared folder: ", mount_cmd_list, use_c_locale=False) shred_cmd_list = ['shred', "-u", tmp.name] shred_process, shred_flat_command_string, failed_message = Utility.run( "Shredding credentials temp file: ", shred_cmd_list, use_c_locale=False) if shred_process.returncode != 0: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, failed_message) return if mount_process.returncode != 0: check_password_msg = _( "Please ensure the username, password and other fields provided are correct, and try again." ) GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add( self.callback, False, mount_failed_message + "\n\n" + check_password_msg) return else: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, True, "", destination_path) except Exception as e: tb = traceback.format_exc() print(tb) GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, "Error mounting folder: " + tb)
class MountLocalPath: def __init__(self, builder, callback, source_path, destination_path): self.destination_path = destination_path self.source_path = source_path self.destination_path = destination_path self.callback = callback self.requested_stop_lock = threading.Lock() self.requested_stop = False self.please_wait_popup = PleaseWaitModalPopup( builder, title=_("Please wait..."), message=_("Mounting...") + "\n\n" + _("Close this popup to cancel the mount operation."), on_close_callback=self.cancel_mount) self.please_wait_popup.show() thread = threading.Thread(target=self._do_mount_command, args=( source_path, destination_path, )) thread.daemon = True thread.start() def cancel_mount(self): with self.requested_stop_lock: self.requested_stop = True return def is_stop_requested(self): with self.requested_stop_lock: return self.requested_stop def _do_mount_command(self, source_path, destination_path): try: if not os.path.exists(destination_path) and not os.path.isdir( destination_path): os.mkdir(destination_path, 0o755) if self.is_stop_requested(): GLib.idle_add(self.callback, False, _("Operation cancelled by user.")) return is_unmounted, message = Utility.umount_warn_on_busy( destination_path) if not is_unmounted: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, message) return if self.is_stop_requested(): GLib.idle_add(self.callback, False, _("Operation cancelled by user.")) return is_unmounted, message = Utility.umount_warn_on_busy(source_path) if not is_unmounted: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, message) return if self.is_stop_requested(): GLib.idle_add(self.callback, False, _("Operation cancelled by user.")) return mount_cmd_list = ['mount', source_path, destination_path] process, flat_command_string, failed_message = Utility.interruptable_run( "Mounting selected partition: ", mount_cmd_list, use_c_locale=False, is_shutdown_fn=self.is_stop_requested) if process.returncode != 0: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, failed_message) return else: GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, True, "", destination_path) except Exception as e: tb = traceback.format_exc() print(tb) GLib.idle_add(self.please_wait_popup.destroy) GLib.idle_add(self.callback, False, "Error mounting folder: " + tb)