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)
Example #2
0
    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()
Example #3
0
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)
Example #4
0
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)
Example #5
0
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)
Example #6
0
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)
Example #7
0
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)