def verify_filesystem(self): self.log.info(_('Verifying filesystem...')) if self.fstype not in self.valid_fstypes: if not self.fstype: raise TailsInstallerError( _('Unknown filesystem. Your device ' 'may need to be reformatted.')) else: raise TailsInstallerError( _("Unsupported filesystem: %s") % self.fstype) if self.drive['label'] != self.label: self.log.info('Setting %(device)s label to %(label)s' % { 'device': self.drive['device'], 'label': self.label }) try: if self.fstype in ('vfat', 'msdos'): try: self.popen('/sbin/dosfslabel %s %s' % (self.drive['device'], self.label)) except TailsInstallerError: # dosfslabel returns an error code even upon success pass else: self.popen('/sbin/e2label %s %s' % (self.drive['device'], self.label)) except TailsInstallerError as e: self.log.error( _("Unable to change volume label: %(message)s") % {'message': str(e)})
def install_bootloader(self): """ Run syslinux to install the bootloader on our devices """ self.log.info(_('Installing bootloader...')) # Don't prompt about overwriting files from mtools (#491234) for ldlinux in [ self.get_liveos_file_path(p, 'ldlinux.sys') for p in ('syslinux', '') ]: self.log.debug('Looking for %s' % ldlinux) if os.path.isfile(ldlinux): self.log.debug(_('Removing %(file)s') % {'file': ldlinux}) os.unlink(ldlinux) # FAT syslinux_executable = 'syslinux' self.log.debug('Will use %s as the syslinux binary' % syslinux_executable) iso_syslinux = self.get_liveos_file_path('utils', 'linux', syslinux_executable) tmpdir = tempfile.mkdtemp() tmp_syslinux = os.path.join(tmpdir, syslinux_executable) shutil.copy(iso_syslinux, tmp_syslinux) os.chmod( tmp_syslinux, os.stat(tmp_syslinux).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) self.flush_buffers() self.unmount_device() self.popen('%s %s -d %s %s' % (tmp_syslinux, ' '.join( self.syslinux_options()), 'syslinux', self.drive['device']), env={"LC_CTYPE": "C"}) shutil.rmtree(tmpdir)
def delete_liveos(self): """ Delete the files installed by the existing Live OS, after chmod'ing them since Python for Windows is unable to delete read-only files. """ self.log.info(_('Removing existing Tails system')) for path in self.get_liveos_toplevel_files(absolute=True): if not os.path.exists(path): continue self.log.debug('Considering ' + path) if os.path.isfile(path): try: os.unlink(path) except Exception as e: raise TailsInstallerError( _("Unable to remove file from" " previous Tails system: %(message)s") % {'message': str(e)}) elif os.path.isdir(path): try: _set_liberal_perms_recursive(path) except OSError as e: self.log.debug( _("Unable to chmod %(file)s: %(message)s") % { 'file': path, 'message': str(e) }) try: shutil.rmtree(path) except OSError as e: raise TailsInstallerError( _("Unable to remove directory from" " previous Tails system: %(message)s") % {'message': str(e)})
def on_target_changed(self, combobox_target): # get selected device drive = self.get_selected_drive() if drive is None: self.enable_widgets(False) return device = self.live.drives[drive] if self.live.device_can_be_upgraded(device): self.opts.partition = False self.force_reinstall = False self.__button_start.set_label(_('Upgrade')) self.__help_link.set_label(_('Manual Upgrade Instructions')) self.__help_link.set_uri('https://tails.boum.org/upgrade/') if device['is_device_big_enough_for_reinstall']: self.force_reinstall_button_available = True self.__button_force_reinstall.set_visible(True) else: self.force_reinstall_button_available = False self.__button_force_reinstall.set_visible(False) else: self.opts.partition = True self.force_reinstall = True self.__button_start.set_label(_('Install')) self.force_reinstall_button_available = False self.__button_force_reinstall.set_visible(False) self.__help_link.set_label(_('Installation Instructions')) self.__help_link.set_uri('https://tails.boum.org/install/')
def __init__(self, path): if not os.path.exists(path): raise SourceError(_('"%s" does not exist') % path) if not os.path.isdir(path): raise SourceError(_('"%s" is not a directory') % path) self.path = path self.size = _dir_size(self.path) self.dev = underlying_physical_device(self.path)
def read_extracted_mbr(self): mbr_path = self._get_mbr_bin() self.log.info(_('Reading extracted MBR from %s') % mbr_path) with open(mbr_path, 'rb') as mbr_fd: self.extracted_mbr_content = mbr_fd.read() if not len(self.extracted_mbr_content): raise TailsInstallerError( _("Could not read the extracted MBR from %(path)s") % {'path': mbr_path})
def extract_iso(self): """ Extract our ISO with 7-zip directly to the USB key """ self.log.info(_('Extracting live image to the target device...')) start = datetime.now() self.source.clone(self.dest) delta = datetime.now() - start if delta.seconds: self.mb_per_sec = (self.source.size / delta.seconds) / 1024**2 if self.mb_per_sec: self.log.info( _('Wrote to device at %(speed)d MB/sec') % {'speed': self.mb_per_sec})
def __init__(self, path): self.path = os.path.abspath(_to_unicode(path)) self.size = os.stat(self.path)[ST_SIZE] if not iso_is_live_system(self.path): raise SourceError(_("Unable to find Tails on ISO")) self.dev = None # This can fail for devices not supported by UDisks such as aufs mounts try: self.dev = underlying_physical_device(self.path) except Exception as e: print(_('Could not guess underlying block device: %s') % e.args[0], file=sys.stderr) pass
def switch_drive_to_system_partition(self): full_drive_name = self._full_drive['device'] append = False if full_drive_name.startswith('/dev/sd'): append = '1' elif full_drive_name.startswith('/dev/mmcblk'): append = 'p1' if not append: self.log.warning( _("Unsupported device '%(device)s', please report a bug.") % {'device': full_drive_name}) self.log.info(_('Trying to continue anyway.')) append = '1' self.drive = '%s%s' % (full_drive_name, append)
def bootable_partition(self): """ Ensure that the selected partition is flagged as bootable """ if self.opts.partition: # already done at partitioning step return if self.drive.get('parent') is None: self.log.debug('No partitions on device; not attempting to mark ' 'any partitions as bootable') return import parted try: disk, partition = self.get_disk_partition() except TailsInstallerError as e: self.log.exception(e) return if partition.isFlagAvailable(parted.PARTITION_BOOT): if partition.getFlag(parted.PARTITION_BOOT): self.log.debug(_('%s already bootable') % self._drive) else: partition.setFlag(parted.PARTITION_BOOT) try: disk.commit() self.log.info('Marked %s as bootable' % self._drive) except Exception as e: self.log.exception(e) else: self.log.warning('%s does not have boot flag' % self._drive)
def get_open_write_fd(block): """ @returns the file descriptor for a block """ (fd_index, fd_list) = block.call_open_for_restore_sync( arg_options=GLib.Variant('a{sv}', None) ) file_desc = fd_list.get(fd_index.get_handle()) if file_desc == -1: raise Exception(_('Could not open device for writing.')) return file_desc
def on_installation_complete(self, data=None): # FIXME: replace content by a specific page dialog = Gtk.MessageDialog(parent=self, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, message_type=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.CLOSE, message_format=_('Installation complete!')) dialog.run() self.close()
def reset_mbr(self): parent = parent = self.drive.get('parent', self._drive) if parent is None: parent = self._drive parent_udi = self.drive['udi'] else: parent_udi = self.drive['parent_udi'] parent_udi = str(parent_udi) parent = str(parent) if '/dev/loop' not in self.drive: self.log.info(_('Resetting Master Boot Record of %s') % parent) self.log.debug( _('Resetting Master Boot Record of %s') % parent_udi) obj = self._get_object(udi=parent_udi) block = obj.props.block write_to_block_device(block, self.extracted_mbr_content) else: self.log.info(_('Drive is a loopback, skipping MBR reset'))
def get_disk_partition(self): """ Return the PedDisk and partition of the selected device """ import parted parent = self.drives[self._drive]['parent'] dev = parted.Device(path=parent) disk = parted.Disk(device=dev) for part in disk.partitions: if self._drive == '/dev/%s' % (part.getDeviceNodeName(), ): return disk, part raise TailsInstallerError(_('Unable to find partition'))
def create_persistent_overlay(self): if self.overlay: self.log.info(_('Creating %sMB persistent overlay') % self.overlay) if self.fstype == 'vfat': # vfat apparently can't handle sparse files self.popen('dd if=/dev/zero of="%s" count=%d bs=1M' % (self.get_overlay(), self.overlay)) else: self.popen('dd if=/dev/zero of="%s" count=1 bs=1M seek=%d' % (self.get_overlay(), self.overlay))
def mount_device(self): """ Mount our device if it is not already mounted """ if not self.fstype: raise TailsInstallerError( _('Unknown filesystem. Your device ' 'may need to be reformatted.')) if self.fstype not in self.valid_fstypes: raise TailsInstallerError( _('Unsupported filesystem: %s') % self.fstype) self.dest = self.drive['mount'] if not self.dest: self.log.debug('Mounting %s' % self.drive['udi']) # XXX: this is racy and then it sometimes fails with: # 'NoneType' object has no attribute 'call_mount_sync' filesystem = self._get_object().props.filesystem mount = None try: mount = filesystem.call_mount_sync(arg_options=GLib.Variant( 'a{sv}', None), cancellable=None) except GLib.Error as e: if 'org.freedesktop.UDisks2.Error.AlreadyMounted' in e.message: self.log.debug('Device already mounted') else: raise TailsInstallerError( _('Unknown GLib exception while trying to ' 'mount device: %(message)s') % {'message': str(e)}) except Exception as e: raise TailsInstallerError( _('Unable to mount device: %(message)s') % {'message': str(e)}) # Get the new mount point if not mount: self.log.error(_('No mount points found')) else: self.dest = self.drive['mount'] = mount self.drive['free'] = self.get_free_bytes(self.dest) / 1024**2 self.log.debug('Mounted %s to %s ' % (self.drive['device'], self.dest)) else: self.log.debug('Using existing mount: %s' % self.dest)
def get_device_pretty_name(self, device): size = _format_bytes_in_gb(device['parent_size'] if device['parent_size'] else device['size']) pretty_name = _('%(size)s %(vendor)s %(model)s device (%(device)s)') % { 'size': size, 'vendor': device['vendor'], 'model': device['model'], 'device': device['device'] } return pretty_name
def select_source_iso(self, isofile): if not os.access(isofile, os.R_OK): self.status(_('The selected file is unreadable. ' 'Please fix its permissions or select another file.')) return False try: self.live.source = LocalIsoSource(path=isofile) except Exception as ex: self.status(_('Unable to use the selected file. ' 'You may have better luck if you move your ISO ' 'to the root of your drive (ie: C:\)')) self.live.log.exception(ex.args[0]) return False self.live.log.info(_('%(filename)s selected') % {'filename': str(os.path.basename(self.live.source.path))}) self.source_available = True self.live.log.debug('Calling populate_devices()' ' from select_source_iso') self.populate_devices()
def on_radio_button_source_iso_toggled(self, radio_button): self.live.log.debug('Entering on_radio_button_source_iso_toggled') active_radio = [r for r in radio_button.get_group() if r.get_active()][0] if active_radio.get_label() == _('Clone the current Tails'): self.live.log.debug('Mode: clone') self.opts.clone = True self.live.source = RunningLiveSystemSource( path=CONFIG['running_liveos_mountpoint']) self.source_available = True self.__filechooserbutton_source_file.set_sensitive(False) elif active_radio.get_label() == _('Use a downloaded Tails ISO image'): self.live.log.debug('Mode: from ISO') self.opts.clone = False self.live.source = None self.source_available = False self.__filechooserbutton_source_file.set_sensitive(True) # previous error messages may be invalid now self.clear_log() # some previously rejected devices may now be valid candidates # and vice-versa self.live.log.debug('Calling populate_devices()' ' from on_radio_button_source_iso_toggled') self.populate_devices()
def show_confirmation_dialog(self, title, message, warning, label_string=_('Install')): if warning: buttons = Gtk.ButtonsType.OK else: buttons = Gtk.ButtonsType.NONE dialog = Gtk.MessageDialog(parent=self, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, message_type=Gtk.MessageType.QUESTION, buttons=buttons, message_format=title) dialog.format_secondary_text(message) if not warning: dialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL) dialog.add_button(label_string, Gtk.ResponseType.YES) reply = dialog.run() dialog.hide() if reply == Gtk.ResponseType.YES: return True else: self.target_selected = None self.enable_widgets(True) return False
def unmount_device(self): """ Unmount our device """ self.log.debug( _('Entering unmount_device for "%(device)s"') % {'device': self.drive['device']}) self.log.debug(pformat(self.drive)) if self.drive['mount'] is None: udis = self.drive['mounted_partitions'] else: udis = [self.drive['udi']] if udis: self.log.info( _('Unmounting mounted filesystems on "%(device)s"') % {'device': self.drive['device']}) for udi in udis: self.log.debug( _('Unmounting "%(udi)s" on "%(device)s"') % { 'device': self.drive['device'], 'udi': udi }) filesystem = self._get_object(udi).props.filesystem filesystem.call_unmount_sync(arg_options=GLib.Variant( 'a{sv}', None), cancellable=None) self.drive['mount'] = None if not self.opts.partition and self.dest is not None \ and os.path.exists(self.dest): self.log.error(_('Mount %s exists after unmounting') % self.dest) self.dest = None # Sometimes the device is still considered as busy by the kernel # at this point, which prevents, when called by reset_mbr() -> # write_to_block_device() -> get_open_write_fd() # -> call_open_for_restore_sync() from opening it for writing. self.flush_buffers(silent=True) time.sleep(3)
def clone(self, destination): for f in CONFIG['liveos_toplevel_files']: src = os.path.join(self.path, f) dst = os.path.join(destination, f) if os.path.isfile(src): if src.lower().endswith('.iso'): print(_('Skipping "%(filename)s"') % {'filename': src}, file=sys.stderr) else: shutil.copy(src, dst) elif os.path.islink(src): linkto = os.readlink(src) os.symlink(linkto, dst) elif os.path.isdir(src): shutil.copytree(src, dst) _set_liberal_perms_recursive(destination)
def check_free_space(self): """ Make sure there is enough space for the LiveOS and overlay """ freebytes = self.get_free_bytes() self.log.debug('freebytes = %d' % freebytes) self.log.debug('source size = %d' % self.source.size) overlaysize = self.overlay * 1024**2 self.log.debug('overlaysize = %d' % overlaysize) self.totalsize = overlaysize + self.source.size if self.totalsize > freebytes: raise TailsInstallerError( _("Not enough free space on device." + "\n%(iso_size)dMB ISO + %(overlay_size)dMB overlay " + "> %(free_space)dMB free space") % { 'iso_size': self.source.size / 1024**2, 'overlay_size': self.overlay, 'free_space': freebytes / 1024**2 })
def _set_drive(self, drive): # XXX: sometimes fails with: # Traceback (most recent call last): # File "tails-installer/git/tails_installer.gui.py", line 200, in run # self.live.switch_drive_to_system_partition() # File "tails-installer/git/tails_installer.creator.py", line 967, in switch_drive_to_system_partition # self.drive = '%s%s' % (full_drive_name, append) # File "tails-installer/git/tails_installer.creator.py", line 88, in <lambda> # fset=lambda self, d: self._set_drive(d)) # File "tails-installer/git/tails_installer.creator.py", line 553, in _set_drive # raise TailsInstallerError(_('Cannot find device %s') % drive) if drive not in self.drives: raise TailsInstallerError(_('Cannot find device %s') % drive) self.log.debug('%s selected: %s' % (drive, self.drives[drive])) self._drive = drive self.uuid = self.drives[drive]['uuid'] self.fstype = self.drives[drive]['fstype']
def extract_file_content_from_iso(iso_path, path): """ Return the content of that file read from inside self.iso """ cmd = ['isoinfo', '-R', '-i', bytes_to_unicode(iso_path), '-x', bytes_to_unicode(path)] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = proc.communicate() out = bytes_to_unicode(out) err = bytes_to_unicode(err) if proc.returncode: raise Exception(_("There was a problem executing `%(cmd)s`." "%(out)s\n%(err)s") % { 'cmd': cmd, 'out': out, 'err': err }) return out
def clone(self, destination): cmd = ['7z', 'x', self.path, '-x![BOOT]', '-y', '-o%s' % (destination)] cmd_decoded = ' '.join(cmd) cmd_bytes = [unicode_to_filesystemencoding(el) for el in cmd] proc = subprocess.Popen(cmd_bytes, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = proc.communicate() out = out.decode('utf-8') err = err.decode('utf-8') if proc.returncode: raise SourceError( _("There was a problem executing `%(cmd)s`.\n" "%(out)s\n%(err)s") % { 'cmd': cmd_decoded, 'out': out, 'err': err }) _set_liberal_perms_recursive(destination)
def popen(self, cmd, passive=False, ret='proc', **user_kwargs): """ A wrapper method for running subprocesses. This method handles logging of the command and it's output, and keeps track of the pids in case we need to kill them. If something goes wrong, an error log is written out and a TailsInstallerError is thrown. @param cmd: The commandline to execute. Either a string or a list. @param passive: Enable passive process failure. @param kwargs: Extra arguments to pass to subprocess.Popen """ if isinstance(cmd, list): cmd_str = ' '.join(cmd) else: cmd_str = cmd self.log.debug(cmd_str) self.output.write(cmd_str) kwargs = {'shell': True, 'stdin': subprocess.PIPE} kwargs.update(user_kwargs) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) self.pids.append(proc.pid) out, err = proc.communicate() out = bytes_to_unicode(out) err = bytes_to_unicode(err) self.output.write(out + '\n' + err + '\n') if proc.returncode: if passive: self.log.debug(self.output.getvalue()) else: self.log.info(self.output.getvalue()) raise TailsInstallerError( _('There was a problem executing the following command: `%(command)s`.\nA more detailed error log has been written to "%(filename)s".' ) % { 'command': cmd, 'filename': self._error_log_filename }) if ret == 'stdout': return out return proc
def _build_ui(self): # Set windows properties self.set_deletable(True) self.connect('delete-event', Gtk.main_quit) self.set_title(_('Tails Installer')) # Import window content from UI file builder = Gtk.Builder.new_from_file(os.path.join(_get_datadir(), 'tails-installer.ui')) self.__box_installer = builder.get_object('box_installer') self.__image_header = builder.get_object('image_header') self.__infobar = builder.get_object('infobar') self.__label_infobar_title = builder.get_object('label_infobar_title') self.__label_infobar_details = builder.get_object('label_infobar_details') self.__box_source = builder.get_object('box_source') self.__box_source_select = builder.get_object('box_source_select') self.__box_source_file = builder.get_object('box_source_file') self.__filechooserbutton_source_file = builder.get_object('filechooserbutton_source_file') self.__box_target = builder.get_object('box_target') self.__combobox_target = builder.get_object('combobox_target') self.__liststore_target = builder.get_object('liststore_target') self.__textview_log = builder.get_object('textview_log') self.__progressbar = builder.get_object('progressbar_progress') self.__button_start = builder.get_object('button_start') self.__radio_button_source_iso = builder.get_object('radio_button_source_iso') self.__radio_button_source_device = builder.get_object('radio_button_source_device') self.__button_force_reinstall = builder.get_object('check_force_reinstall') self.__help_link = builder.get_object('help_link') self.add(self.__box_installer) builder.connect_signals(self) # Add a cell renderer to the comboboxes cell = Gtk.CellRendererText() self.__combobox_target.pack_start(cell, True) self.__combobox_target.add_attribute(cell, 'text', 0) # Add image header self.__image_header.set_from_file( os.path.join(_get_datadir(), CONFIG['branding']['header'])) rgba = Gdk.RGBA() rgba.parse(CONFIG['branding']['color']) self.__image_header.override_background_color(Gtk.StateFlags.NORMAL, rgba)
def format_device(self): """ Format the selected partition as FAT32 """ self.log.info( _('Formatting %(device)s as FAT32') % {'device': self._drive}) dev = self._get_object() block = dev.props.block try: block.call_format_sync( 'vfat', arg_options=GLib.Variant( 'a{sv}', { 'label': GLib.Variant('s', self.label), 'update-partition-type': GLib.Variant('s', 'FALSE') })) except GLib.Error as e: if ('GDBus.Error:org.freedesktop.UDisks2.Error.Failed' in e.message and ('Error synchronizing after formatting' in e.message or 'Error synchronizing after initial wipe' in e.message)): self.log.debug( 'Failed to synchronize. Trying again, which usually solves the issue. Error was: %s' % e.message) self.flush_buffers(silent=True) time.sleep(5) block.call_format_sync( 'vfat', arg_options=GLib.Variant( 'a{sv}', { 'label': GLib.Variant('s', self.label), 'update-partition-type': GLib.Variant( 's', 'FALSE') })) else: raise self.fstype = self.drive['fstype'] = 'vfat' self.flush_buffers(silent=True) time.sleep(3) self._get_object().props.block.call_rescan_sync( GLib.Variant('a{sv}', None))
def update_configs(self): """ Generate our syslinux.cfg and grub.conf files """ grubconf = self.get_liveos_file_path('EFI', 'BOOT', 'grub.conf') bootconf = self.get_liveos_file_path('EFI', 'BOOT', 'boot.conf') bootx64conf = self.get_liveos_file_path('EFI', 'BOOT', 'bootx64.conf') bootia32conf = self.get_liveos_file_path('EFI', 'BOOT', 'bootia32.conf') updates = [(self.get_liveos_file_path('isolinux', 'isolinux.cfg'), self.get_liveos_file_path('isolinux', 'syslinux.cfg')), (self.get_liveos_file_path('isolinux', 'stdmenu.cfg'), self.get_liveos_file_path('isolinux', 'stdmenu.cfg')), (self.get_liveos_file_path('isolinux', 'exithelp.cfg'), self.get_liveos_file_path('isolinux', 'exithelp.cfg')), (self.get_liveos_file_path('EFI', 'BOOT', 'isolinux.cfg'), self.get_liveos_file_path('EFI', 'BOOT', 'syslinux.cfg')), (grubconf, bootconf)] copies = [(bootconf, grubconf), (bootconf, bootx64conf), (bootconf, bootia32conf)] for (infile, outfile) in updates: if os.path.exists(infile): self._update_configs(infile, outfile) # only copy/overwrite files we had originally started with for (infile, outfile) in copies: if os.path.exists(outfile): try: shutil.copyfile(infile, outfile) except Exception as e: self.log.warning( _('Unable to copy %(infile)s to %(outfile)s: %(message)s' ) % { 'infile': infile, 'outfile': outfile, 'message': str(e) }) syslinux_path = self.get_liveos_file_path('syslinux') _move_if_exists(self.get_liveos_file_path('isolinux'), syslinux_path) _unlink_if_exists(os.path.join(syslinux_path, 'isolinux.cfg'))