Esempio n. 1
0
    def _mount_system(
        self, root_device, boot_device, efi_device=None, volumes=None
    ):
        self.root_mount = MountManager(
            device=root_device
        )
        self.boot_mount = MountManager(
            device=boot_device,
            mountpoint=self.root_mount.mountpoint + '/boot'
        )
        if efi_device:
            self.efi_mount = MountManager(
                device=efi_device,
                mountpoint=self.root_mount.mountpoint + '/boot/efi'
            )

        self.root_mount.mount()

        if not self.root_mount.device == self.boot_mount.device:
            self.boot_mount.mount()

        if efi_device:
            self.efi_mount.mount()

        if volumes:
            for volume_path in Path.sort_by_hierarchy(
                sorted(volumes.keys())
            ):
                volume_mount = MountManager(
                    device=volumes[volume_path]['volume_device'],
                    mountpoint=self.root_mount.mountpoint + '/' + volume_path
                )
                self.volumes_mount.append(volume_mount)
                volume_mount.mount(
                    options=[volumes[volume_path]['volume_options']]
                )

        if self.root_filesystem_is_overlay:
            # In case of an overlay root system all parts of the rootfs
            # are read-only by squashfs except for the extra boot partition.
            # However tools like grub's mkconfig creates temporary files
            # at call time and therefore /tmp needs to be writable during
            # the call time of the tools
            self.tmp_mount = MountManager(
                device='/tmp',
                mountpoint=self.root_mount.mountpoint + '/tmp'
            )
            self.tmp_mount.bind_mount()

        self.device_mount = MountManager(
            device='/dev',
            mountpoint=self.root_mount.mountpoint + '/dev'
        )
        self.proc_mount = MountManager(
            device='/proc',
            mountpoint=self.root_mount.mountpoint + '/proc'
        )
        self.device_mount.bind_mount()
        self.proc_mount.bind_mount()
Esempio n. 2
0
 def _mount_volumes(self, mountpoint) -> None:
     for volume_path in Path.sort_by_hierarchy(sorted(self.volumes.keys())):
         volume_mount = MountManager(
             device=self.volumes[volume_path]['volume_device'],
             mountpoint=os.path.join(mountpoint, volume_path))
         self.mount_list.append(volume_mount)
         volume_mount.mount(
             options=[self.volumes[volume_path]['volume_options']])
Esempio n. 3
0
 def _iso_mount_path(self, path):
     # The prefix name 'kiwi_iso_mount' has a meaning here because the
     # zypper repository manager looks up iso mount paths by its repo
     # source name
     iso_mount_path = mkdtemp(prefix='kiwi_iso_mount.')
     iso_mount = MountManager(device=path, mountpoint=iso_mount_path)
     self.mount_stack.append(iso_mount)
     iso_mount.mount()
     return iso_mount.mountpoint
Esempio n. 4
0
 def _iso_mount_path(self, path):
     # The prefix name 'kiwi_iso_mount' has a meaning here because the
     # zypper repository manager looks up iso mount paths by its repo
     # source name
     iso_mount_path = mkdtemp(prefix='kiwi_iso_mount.')
     iso_mount = MountManager(
         device=path, mountpoint=iso_mount_path
     )
     self.mount_stack.append(iso_mount)
     iso_mount.mount()
     return iso_mount.mountpoint
Esempio n. 5
0
class BootLoaderInstallZipl(BootLoaderInstallBase):
    """
    **zipl bootloader installation**
    """
    def post_init(self, custom_args):
        """
        zipl post initialization method

        :param dict custom_args:
            Contains custom zipl bootloader arguments

            .. code:: python

                {'boot_device': string}

        """
        self.custom_args = custom_args
        if not custom_args or 'boot_device' not in custom_args:
            raise KiwiBootLoaderZiplInstallError(
                'boot device node name required for zipl installation')

        self.boot_mount = MountManager(custom_args['boot_device'])

    def install_required(self):
        """
        Check if zipl has to be installed

        Always required

        :return: True

        :rtype: bool
        """
        return True

    def install(self):
        """
        Install bootloader on self.device
        """
        log.info('Installing zipl on disk %s', self.device)

        self.boot_mount.mount()

        bash_command = ' '.join([
            'cd',
            os.sep.join([self.root_dir, 'boot']), '&&', 'zipl', '-V', '-c',
            'zipl/config', '-m', 'menu'
        ])
        zipl_call = Command.run(['bash', '-c', bash_command])
        log.debug('zipl install succeeds with: %s', zipl_call.output)

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        self.boot_mount.umount()
Esempio n. 6
0
class BootLoaderInstallZipl(BootLoaderInstallBase):
    """
    zipl bootloader installation

    Attributes

    * :attr:`boot_mount`
        Instance of MountManager for boot device
    """
    def post_init(self, custom_args):
        self.custom_args = custom_args
        if not custom_args or 'boot_device' not in custom_args:
            raise KiwiBootLoaderZiplInstallError(
                'boot device node name required for zipl installation'
            )

        self.boot_mount = MountManager(
            custom_args['boot_device']
        )

    def install_required(self):
        """
        Check if zipl has to be installed

        Always required

        :rtype: True
        """
        return True

    def install(self):
        """
        Install bootloader on self.device
        """
        log.info('Installing zipl on disk %s', self.device)

        self.boot_mount.mount()

        bash_command = ' '.join(
            [
                'cd', self.boot_mount.mountpoint, '&&',
                'zipl', '-V', '-c', self.boot_mount.mountpoint + '/config',
                '-m', 'menu'
            ]
        )
        zipl_call = Command.run(
            ['bash', '-c', bash_command]
        )
        log.debug('zipl install succeeds with: %s', zipl_call.output)

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        self.boot_mount.umount()
Esempio n. 7
0
	def finalize(self, image):
		# mount fs
		mount = MountManager(device=self._device)
		mount.mount(options=None)

		# run post-image script
		setup = SystemSetup(xml_state=self._state, root_dir=mount.mountpoint)
		setup.import_description()
		setup.call_disk_script()

		# clean-up
		mount.umount()
		del setup
		return True
Esempio n. 8
0
    def _mount_device_and_volumes(self):
        if self.root_mount is None:
            self.root_mount = MountManager(
                device=self.custom_args['root_device'])
            self.root_mount.mount()

        if self.boot_mount is None:
            if 's390' in self.arch:
                self.boot_mount = MountManager(
                    device=self.custom_args['boot_device'],
                    mountpoint=self.root_mount.mountpoint + '/boot/zipl')
            else:
                self.boot_mount = MountManager(
                    device=self.custom_args['boot_device'],
                    mountpoint=self.root_mount.mountpoint + '/boot')
            if not self.root_mount.device == self.boot_mount.device:
                self.boot_mount.mount()

        if self.efi_mount is None and self.custom_args.get('efi_device'):
            self.efi_mount = MountManager(
                device=self.custom_args['efi_device'],
                mountpoint=self.root_mount.mountpoint + '/boot/efi')
            self.efi_mount.mount()

        if self.volumes and not self.volumes_mount:
            for volume_path in Path.sort_by_hierarchy(
                    sorted(self.volumes.keys())):
                volume_mount = MountManager(
                    device=self.volumes[volume_path]['volume_device'],
                    mountpoint=self.root_mount.mountpoint + '/' + volume_path)
                self.volumes_mount.append(volume_mount)
                volume_mount.mount(
                    options=[self.volumes[volume_path]['volume_options']])
        if self.device_mount is None:
            self.device_mount = MountManager(
                device='/dev', mountpoint=self.root_mount.mountpoint + '/dev')
            self.device_mount.bind_mount()
        if self.proc_mount is None:
            self.proc_mount = MountManager(
                device='/proc',
                mountpoint=self.root_mount.mountpoint + '/proc')
            self.proc_mount.bind_mount()
        if self.sysfs_mount is None:
            self.sysfs_mount = MountManager(
                device='/sys', mountpoint=self.root_mount.mountpoint + '/sys')
            self.sysfs_mount.bind_mount()
Esempio n. 9
0
class BootLoaderInstallZipl(BootLoaderInstallBase):
    """
    zipl bootloader installation

    Attributes

    * :attr:`boot_mount`
        Instance of MountManager for boot device
    """
    def post_init(self, custom_args):
        self.custom_args = custom_args
        if not custom_args or 'boot_device' not in custom_args:
            raise KiwiBootLoaderZiplInstallError(
                'boot device node name required for zipl installation')

        self.boot_mount = MountManager(custom_args['boot_device'])

    def install_required(self):
        """
        Check if zipl has to be installed

        Always required

        :rtype: True
        """
        return True

    def install(self):
        """
        Install bootloader on self.device
        """
        log.info('Installing zipl on disk %s', self.device)

        self.boot_mount.mount()

        bash_command = ' '.join([
            'cd', self.boot_mount.mountpoint, '&&', 'zipl', '-V', '-c',
            self.boot_mount.mountpoint + '/config', '-m', 'menu'
        ])
        zipl_call = Command.run(['bash', '-c', bash_command])
        log.debug('zipl install succeeds with: %s', zipl_call.output)

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        self.boot_mount.umount()
Esempio n. 10
0
class TestMountManager:
    def setup(self):
        self.mount_manager = MountManager(
            '/dev/some-device', '/some/mountpoint'
        )

    @patch('kiwi.mount_manager.mkdtemp')
    def test_setup_empty_mountpoint(self, mock_mkdtemp):
        mock_mkdtemp.return_value = 'tmpdir'
        mount_manager = MountManager('/dev/some-device')
        assert mount_manager.mountpoint == 'tmpdir'

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_bind_mount(self, mock_mounted, mock_command):
        mock_mounted.return_value = False
        self.mount_manager.bind_mount()
        mock_command.assert_called_once_with(
            ['mount', '-n', '--bind', '/dev/some-device', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_mount(self, mock_mounted, mock_command):
        mock_mounted.return_value = False
        self.mount_manager.mount(['options'])
        mock_command.assert_called_once_with(
            ['mount', '-o', 'options', '/dev/some-device', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_umount_lazy(self, mock_mounted, mock_command):
        mock_mounted.return_value = True
        self.mount_manager.umount_lazy()
        mock_command.assert_called_once_with(
            ['umount', '-l', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    @patch('time.sleep')
    @patch('kiwi.logger.log.warning')
    def test_umount_with_errors(
        self, mock_warn, mock_sleep, mock_mounted, mock_command
    ):
        mock_command.side_effect = Exception
        mock_mounted.return_value = True
        assert self.mount_manager.umount() is False
        assert mock_command.call_args_list == [
            call(['umount', '/some/mountpoint']),
            call(['umount', '/some/mountpoint']),
            call(['umount', '/some/mountpoint'])
        ]
        assert mock_warn.called

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_umount_success(self, mock_mounted, mock_command):
        mock_mounted.return_value = True
        assert self.mount_manager.umount() is True
        mock_command.assert_called_once_with(
            ['umount', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    def test_is_mounted_true(self, mock_command):
        command = mock.Mock()
        command.returncode = 0
        mock_command.return_value = command
        assert self.mount_manager.is_mounted() is True
        mock_command.assert_called_once_with(
            command=['mountpoint', '-q', '/some/mountpoint'],
            raise_on_error=False
        )

    @patch('kiwi.mount_manager.Command.run')
    def test_is_mounted_false(self, mock_command):
        command = mock.Mock()
        command.returncode = 1
        mock_command.return_value = command
        assert self.mount_manager.is_mounted() is False
        mock_command.assert_called_once_with(
            command=['mountpoint', '-q', '/some/mountpoint'],
            raise_on_error=False
        )

    @patch('kiwi.mount_manager.Path.wipe')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_destructor(self, mock_mounted, mock_wipe):
        self.mount_manager.mountpoint_created_by_mount_manager = True
        mock_mounted.return_value = False
        self.mount_manager.__del__()
        mock_wipe.assert_called_once_with('/some/mountpoint')
Esempio n. 11
0
    def install(self):  # noqa: C901
        """
        Install bootloader on disk device
        """
        log.info('Installing grub2 on disk %s', self.device)

        if self.target_removable:
            self.install_arguments.append('--removable')

        if Defaults.is_x86_arch(self.arch):
            self.target = 'i386-pc'
            self.install_device = self.device
            self.modules = ' '.join(
                Defaults.get_grub_bios_modules(multiboot=True)
            )
            self.install_arguments.append('--skip-fs-probe')
        elif self.arch.startswith('ppc64'):
            if not self.custom_args or 'prep_device' not in self.custom_args:
                raise KiwiBootLoaderGrubInstallError(
                    'prep device name required for grub2 installation on ppc'
                )
            self.target = 'powerpc-ieee1275'
            self.install_device = self.custom_args['prep_device']
            self.modules = ' '.join(Defaults.get_grub_ofw_modules())
            self.install_arguments.append('--skip-fs-probe')
            self.install_arguments.append('--no-nvram')
        elif self.arch.startswith('s390'):
            self.target = 's390x-emu'
            self.install_device = self.device
            self.modules = ' '.join(Defaults.get_grub_s390_modules())
            self.install_arguments.append('--skip-fs-probe')
            self.install_arguments.append('--no-nvram')
        else:
            raise KiwiBootLoaderGrubPlatformError(
                'host architecture %s not supported for grub2 installation' %
                self.arch
            )

        self.root_mount = MountManager(
            device=self.custom_args['root_device']
        )
        if 's390' in self.arch:
            self.boot_mount = MountManager(
                device=self.custom_args['boot_device'],
                mountpoint=self.root_mount.mountpoint + '/boot/zipl'
            )
        else:
            self.boot_mount = MountManager(
                device=self.custom_args['boot_device'],
                mountpoint=self.root_mount.mountpoint + '/boot'
            )
        if self.custom_args.get('efi_device'):
            self.efi_mount = MountManager(
                device=self.custom_args['efi_device'],
                mountpoint=self.root_mount.mountpoint + '/boot/efi'
            )

        self.root_mount.mount()

        if not self.root_mount.device == self.boot_mount.device:
            self.boot_mount.mount()

        if self.efi_mount:
            self.efi_mount.mount()

        if self.volumes:
            for volume_path in Path.sort_by_hierarchy(
                sorted(self.volumes.keys())
            ):
                volume_mount = MountManager(
                    device=self.volumes[volume_path]['volume_device'],
                    mountpoint=self.root_mount.mountpoint + '/' + volume_path
                )
                self.volumes_mount.append(volume_mount)
                volume_mount.mount(
                    options=[self.volumes[volume_path]['volume_options']]
                )

        self.device_mount = MountManager(
            device='/dev',
            mountpoint=self.root_mount.mountpoint + '/dev'
        )
        self.proc_mount = MountManager(
            device='/proc',
            mountpoint=self.root_mount.mountpoint + '/proc'
        )
        self.sysfs_mount = MountManager(
            device='/sys',
            mountpoint=self.root_mount.mountpoint + '/sys'
        )
        self.device_mount.bind_mount()
        self.proc_mount.bind_mount()
        self.sysfs_mount.bind_mount()

        # check if a grub installation could be found in the image system
        module_directory = Defaults.get_grub_path(
            self.root_mount.mountpoint, self.target, raise_on_error=False
        )
        if not module_directory:
            raise KiwiBootLoaderGrubDataError(
                'No grub2 installation found in {0} for target {1}'.format(
                    self.root_mount.mountpoint, self.target
                )
            )
        module_directory = module_directory.replace(
            self.root_mount.mountpoint, ''
        )
        boot_directory = '/boot'

        # wipe existing grubenv to allow the grub installer to create a new one
        grubenv_glob = os.sep.join(
            [self.root_mount.mountpoint, 'boot', '*', 'grubenv']
        )
        for grubenv in glob.glob(grubenv_glob):
            Path.wipe(grubenv)

        # install grub2 boot code
        if self.firmware.get_partition_table_type() == 'dasd':
            # On s390 and in CDL mode (4k DASD) the call of grub2-install
            # does not work because grub2-install is not able to identify
            # a 4k fdasd partitioned device as a grub supported device
            # and fails. As grub2-install is only used to invoke
            # grub2-zipl-setup and has no other job to do we can
            # circumvent this problem by directly calling grub2-zipl-setup
            # instead.
            Command.run(
                [
                    'chroot', self.root_mount.mountpoint,
                    'grub2-zipl-setup', '--keep'
                ]
            )
            zipl_config_file = ''.join(
                [
                    self.root_mount.mountpoint, '/boot/zipl/config'
                ]
            )
            zipl2grub_config_file_orig = ''.join(
                [
                    self.root_mount.mountpoint,
                    '/etc/default/zipl2grub.conf.in.orig'
                ]
            )
            if os.path.exists(zipl2grub_config_file_orig):
                Command.run(
                    [
                        'mv', zipl2grub_config_file_orig,
                        zipl2grub_config_file_orig.replace('.orig', '')
                    ]
                )
            if os.path.exists(zipl_config_file):
                Command.run(
                    ['mv', zipl_config_file, zipl_config_file + '.kiwi']
                )
        else:
            Command.run(
                [
                    'chroot', self.root_mount.mountpoint,
                    self._get_grub2_install_tool_name(
                        self.root_mount.mountpoint
                    )
                ] + self.install_arguments + [
                    '--directory', module_directory,
                    '--boot-directory', boot_directory,
                    '--target', self.target,
                    '--modules', self.modules,
                    self.install_device
                ]
            )

        if self.firmware and self.firmware.efi_mode() == 'uefi':
            shim_install = self._get_shim_install_tool_name(
                self.root_mount.mountpoint
            )
            # if shim-install does _not_ exist the fallback mechanism
            # has applied at the bootloader/config level and we expect
            # no further tool calls to be required
            if shim_install:
                # Before we call shim-install, the grub installer binary is
                # replaced by a noop. Actually there is no reason for
                # shim-install to call the grub installer because it should
                # only setup the system for EFI secure boot which does not
                # require any bootloader code in the master boot record.
                # In addition kiwi has called the grub installer right
                # before
                self._disable_grub2_install(self.root_mount.mountpoint)
                Command.run(
                    [
                        'chroot', self.root_mount.mountpoint,
                        'shim-install', '--removable',
                        self.install_device
                    ]
                )
                # restore the grub installer noop
                self._enable_grub2_install(self.root_mount.mountpoint)
Esempio n. 12
0
class BootLoaderInstallGrub2(BootLoaderInstallBase):
    """
    **grub2 bootloader installation**
    """
    def post_init(self, custom_args):
        """
        grub2 post initialization method

        :param dict custom_args:
            Contains custom grub2 bootloader arguments

            .. code:: python

                {
                    'target_removable': bool,
                    'system_volumes': list_of_volumes,
                    'firmware': FirmWare_instance,
                    'efi_device': string,
                    'boot_device': string,
                    'root_device': string
                }

        """
        self.arch = Defaults.get_platform_name()
        self.custom_args = custom_args
        self.install_arguments = []
        self.firmware = None
        self.efi_mount = None
        self.root_mount = None
        self.boot_mount = None
        self.device_mount = None
        self.proc_mount = None
        self.sysfs_mount = None
        self.volumes = None
        self.volumes_mount = []
        self.target_removable = None
        if custom_args and 'target_removable' in custom_args:
            self.target_removable = custom_args['target_removable']
        if custom_args and 'system_volumes' in custom_args:
            self.volumes = custom_args['system_volumes']
        if custom_args and 'firmware' in custom_args:
            self.firmware = custom_args['firmware']

        if self.firmware and self.firmware.efi_mode():
            if not custom_args or 'efi_device' not in custom_args:
                raise KiwiBootLoaderGrubInstallError(
                    'EFI device name required for shim installation')
        if not custom_args or 'boot_device' not in custom_args:
            raise KiwiBootLoaderGrubInstallError(
                'boot device name required for grub2 installation')
        if not custom_args or 'root_device' not in custom_args:
            raise KiwiBootLoaderGrubInstallError(
                'root device name required for grub2 installation')

    def install_required(self):
        """
        Check if grub2 has to be installed

        Take architecture and firmware setup into account to check if
        bootloader code in a boot record is required

        :return: True or False

        :rtype: bool
        """
        if 'ppc64' in self.arch and self.firmware.opal_mode():
            # OPAL doesn't need a grub2 stage1, just a config file.
            # The machine will be setup to kexec grub2 in user space
            log.info('No grub boot code installation in opal mode on %s',
                     self.arch)
            return False
        elif 'arm' in self.arch or self.arch == 'aarch64':
            # On arm grub2 is used for EFI setup only, no install
            # of grub2 boot code makes sense
            log.info('No grub boot code installation on %s', self.arch)
            return False
        elif self.arch == 'riscv64':
            # On riscv grub2 is used for EFI setup only, no install
            # of grub2 boot code makes sense
            log.info('No grub boot code installation on %s', self.arch)
            return False
        return True

    def install(self):
        """
        Install bootloader on disk device
        """
        log.info('Installing grub2 on disk %s', self.device)

        if self.target_removable:
            self.install_arguments.append('--removable')

        if Defaults.is_x86_arch(self.arch):
            self.target = 'i386-pc'
            self.install_device = self.device
            self.modules = ' '.join(
                Defaults.get_grub_bios_modules(multiboot=True))
            self.install_arguments.append('--skip-fs-probe')
        elif self.arch.startswith('ppc64'):
            if not self.custom_args or 'prep_device' not in self.custom_args:
                raise KiwiBootLoaderGrubInstallError(
                    'prep device name required for grub2 installation on ppc')
            self.target = 'powerpc-ieee1275'
            self.install_device = self.custom_args['prep_device']
            self.modules = ' '.join(Defaults.get_grub_ofw_modules())
            self.install_arguments.append('--skip-fs-probe')
            self.install_arguments.append('--no-nvram')
        elif self.arch.startswith('s390'):
            self.target = 's390x-emu'
            self.install_device = self.device
            self.modules = ' '.join(Defaults.get_grub_s390_modules())
            self.install_arguments.append('--skip-fs-probe')
            self.install_arguments.append('--no-nvram')
        else:
            raise KiwiBootLoaderGrubPlatformError(
                'host architecture %s not supported for grub2 installation' %
                self.arch)

        self._mount_device_and_volumes()

        # check if a grub installation could be found in the image system
        module_directory = Defaults.get_grub_path(self.root_mount.mountpoint,
                                                  self.target,
                                                  raise_on_error=False)
        if not module_directory:
            raise KiwiBootLoaderGrubDataError(
                'No grub2 installation found in {0} for target {1}'.format(
                    self.root_mount.mountpoint, self.target))
        module_directory = module_directory.replace(self.root_mount.mountpoint,
                                                    '')
        boot_directory = '/boot'

        # wipe existing grubenv to allow the grub installer to create a new one
        grubenv_glob = os.sep.join(
            [self.root_mount.mountpoint, 'boot', '*', 'grubenv'])
        for grubenv in glob.glob(grubenv_glob):
            Path.wipe(grubenv)

        # install grub2 boot code
        if self.firmware.get_partition_table_type() == 'dasd':
            # On s390 and in CDL mode (4k DASD) the call of grub2-install
            # does not work because grub2-install is not able to identify
            # a 4k fdasd partitioned device as a grub supported device
            # and fails. As grub2-install is only used to invoke
            # grub2-zipl-setup and has no other job to do we can
            # circumvent this problem by directly calling grub2-zipl-setup
            # instead.
            Command.run([
                'chroot', self.root_mount.mountpoint, 'grub2-zipl-setup',
                '--keep'
            ])
            zipl_config_file = ''.join(
                [self.root_mount.mountpoint, '/boot/zipl/config'])
            zipl2grub_config_file_orig = ''.join([
                self.root_mount.mountpoint,
                '/etc/default/zipl2grub.conf.in.orig'
            ])
            if os.path.exists(zipl2grub_config_file_orig):
                Command.run([
                    'mv', zipl2grub_config_file_orig,
                    zipl2grub_config_file_orig.replace('.orig', '')
                ])
            if os.path.exists(zipl_config_file):
                Command.run(
                    ['mv', zipl_config_file, zipl_config_file + '.kiwi'])
        else:
            Command.run([
                'chroot', self.root_mount.mountpoint,
                self._get_grub2_install_tool_name(self.root_mount.mountpoint)
            ] + self.install_arguments + [
                '--directory', module_directory, '--boot-directory',
                boot_directory, '--target', self.target, '--modules',
                self.modules, self.install_device
            ])

    def secure_boot_install(self):
        if self.firmware and self.firmware.efi_mode() == 'uefi' and (
                Defaults.is_x86_arch(self.arch) or 'arm' in self.arch
                or self.arch == 'aarch64'  # noqa: W503
        ):
            self._mount_device_and_volumes()
            shim_install = self._get_shim_install_tool_name(
                self.root_mount.mountpoint)
            # if shim-install does _not_ exist the fallback mechanism
            # has applied at the bootloader/config level and we expect
            # no further tool calls to be required
            if shim_install:
                # Before we call shim-install, the grub installer binary is
                # replaced by a noop. Actually there is no reason for
                # shim-install to call the grub installer because it should
                # only setup the system for EFI secure boot which does not
                # require any bootloader code in the master boot record.
                # In addition kiwi has called the grub installer right
                # before
                self._disable_grub2_install(self.root_mount.mountpoint)
                Command.run([
                    'chroot', self.root_mount.mountpoint, 'shim-install',
                    '--removable', self.device
                ])
                # restore the grub installer noop
                self._enable_grub2_install(self.root_mount.mountpoint)

    def _mount_device_and_volumes(self):
        if self.root_mount is None:
            self.root_mount = MountManager(
                device=self.custom_args['root_device'])
            self.root_mount.mount()

        if self.boot_mount is None:
            if 's390' in self.arch:
                self.boot_mount = MountManager(
                    device=self.custom_args['boot_device'],
                    mountpoint=self.root_mount.mountpoint + '/boot/zipl')
            else:
                self.boot_mount = MountManager(
                    device=self.custom_args['boot_device'],
                    mountpoint=self.root_mount.mountpoint + '/boot')
            if not self.root_mount.device == self.boot_mount.device:
                self.boot_mount.mount()

        if self.efi_mount is None and self.custom_args.get('efi_device'):
            self.efi_mount = MountManager(
                device=self.custom_args['efi_device'],
                mountpoint=self.root_mount.mountpoint + '/boot/efi')
            self.efi_mount.mount()

        if self.volumes and not self.volumes_mount:
            for volume_path in Path.sort_by_hierarchy(
                    sorted(self.volumes.keys())):
                volume_mount = MountManager(
                    device=self.volumes[volume_path]['volume_device'],
                    mountpoint=self.root_mount.mountpoint + '/' + volume_path)
                self.volumes_mount.append(volume_mount)
                volume_mount.mount(
                    options=[self.volumes[volume_path]['volume_options']])
        if self.device_mount is None:
            self.device_mount = MountManager(
                device='/dev', mountpoint=self.root_mount.mountpoint + '/dev')
            self.device_mount.bind_mount()
        if self.proc_mount is None:
            self.proc_mount = MountManager(
                device='/proc',
                mountpoint=self.root_mount.mountpoint + '/proc')
            self.proc_mount.bind_mount()
        if self.sysfs_mount is None:
            self.sysfs_mount = MountManager(
                device='/sys', mountpoint=self.root_mount.mountpoint + '/sys')
            self.sysfs_mount.bind_mount()

    def _disable_grub2_install(self, root_path):
        if os.access(root_path, os.W_OK):
            grub2_install = ''.join([
                root_path, '/usr/sbin/',
                self._get_grub2_install_tool_name(root_path)
            ])
            grub2_install_backup = ''.join([grub2_install, '.orig'])
            grub2_install_noop = ''.join([root_path, '/bin/true'])
            Command.run(['cp', '-p', grub2_install, grub2_install_backup])
            Command.run(['cp', grub2_install_noop, grub2_install])

    def _enable_grub2_install(self, root_path):
        if os.access(root_path, os.W_OK):
            grub2_install = ''.join([
                root_path, '/usr/sbin/',
                self._get_grub2_install_tool_name(root_path)
            ])
            grub2_install_backup = ''.join([grub2_install, '.orig'])
            if os.path.exists(grub2_install_backup):
                Command.run(['mv', grub2_install_backup, grub2_install])

    def _get_grub2_install_tool_name(self, root_path):
        return self._get_tool_name(
            root_path, lookup_list=['grub2-install', 'grub-install'])

    def _get_shim_install_tool_name(self, root_path):
        return self._get_tool_name(root_path,
                                   lookup_list=['shim-install'],
                                   fallback_on_not_found=False)

    def _get_tool_name(self,
                       root_path,
                       lookup_list,
                       fallback_on_not_found=True):
        for tool in lookup_list:
            if Path.which(filename=tool, root_dir=root_path):
                return tool

        if fallback_on_not_found:
            # no tool from the list was found, we intentionally don't
            # raise here but return the default tool name and raise
            # an exception at invocation time in order to log the
            # expected call and its arguments
            return lookup_list[0]

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        for volume_mount in reversed(self.volumes_mount):
            volume_mount.umount()
        if self.device_mount:
            self.device_mount.umount()
        if self.proc_mount:
            self.proc_mount.umount()
        if self.sysfs_mount:
            self.sysfs_mount.umount()
        if self.efi_mount:
            self.efi_mount.umount()
        if self.boot_mount:
            self.boot_mount.umount()
        if self.root_mount:
            self._enable_grub2_install(self.root_mount.mountpoint)
            self.root_mount.umount()
Esempio n. 13
0
    def install(self):
        """
        Install bootloader on disk device
        """
        log.info('Installing grub2 on disk %s', self.device)

        if self.target_removable:
            self.install_arguments.append('--removable')

        if self.arch == 'x86_64' or self.arch == 'i686' or self.arch == 'i586':
            self.target = 'i386-pc'
            self.install_device = self.device
            self.modules = ' '.join(
                Defaults.get_grub_bios_modules(multiboot=True)
            )
            self.install_arguments.append('--skip-fs-probe')
        elif self.arch.startswith('ppc64'):
            if not self.custom_args or 'prep_device' not in self.custom_args:
                raise KiwiBootLoaderGrubInstallError(
                    'prep device name required for grub2 installation on ppc'
                )
            self.target = 'powerpc-ieee1275'
            self.install_device = self.custom_args['prep_device']
            self.modules = ' '.join(Defaults.get_grub_ofw_modules())
            self.install_arguments.append('--skip-fs-probe')
            self.install_arguments.append('--no-nvram')
        else:
            raise KiwiBootLoaderGrubPlatformError(
                'host architecture %s not supported for grub2 installation' %
                self.arch
            )

        self.root_mount = MountManager(
            device=self.custom_args['root_device']
        )
        self.boot_mount = MountManager(
            device=self.custom_args['boot_device'],
            mountpoint=self.root_mount.mountpoint + '/boot'
        )

        self.root_mount.mount()

        if not self.root_mount.device == self.boot_mount.device:
            self.boot_mount.mount()

        if self.volumes:
            for volume_path in Path.sort_by_hierarchy(
                sorted(self.volumes.keys())
            ):
                volume_mount = MountManager(
                    device=self.volumes[volume_path]['volume_device'],
                    mountpoint=self.root_mount.mountpoint + '/' + volume_path
                )
                self.volumes_mount.append(volume_mount)
                volume_mount.mount(
                    options=[self.volumes[volume_path]['volume_options']]
                )

        self.device_mount = MountManager(
            device='/dev',
            mountpoint=self.root_mount.mountpoint + '/dev'
        )
        self.proc_mount = MountManager(
            device='/proc',
            mountpoint=self.root_mount.mountpoint + '/proc'
        )
        self.sysfs_mount = MountManager(
            device='/sys',
            mountpoint=self.root_mount.mountpoint + '/sys'
        )
        self.device_mount.bind_mount()
        self.proc_mount.bind_mount()
        self.sysfs_mount.bind_mount()

        # check if a grub installation could be found in the image system
        grub_directory = Defaults.get_grub_path(
            self.root_mount.mountpoint + '/usr/lib'
        )
        if not grub_directory:
            raise KiwiBootLoaderGrubDataError(
                'No grub2 installation found in %s' % self.root_mount.mountpoint
            )
        grub_directory = grub_directory.replace(
            self.root_mount.mountpoint, ''
        )
        module_directory = grub_directory + '/' + self.target
        boot_directory = '/boot'

        # wipe existing grubenv to allow the grub installer to create a new one
        grubenv_glob = os.sep.join(
            [self.root_mount.mountpoint, 'boot', '*', 'grubenv']
        )
        for grubenv in glob.glob(grubenv_glob):
            Path.wipe(grubenv)

        # install grub2 boot code
        Command.run(
            [
                'chroot', self.root_mount.mountpoint,
                self._get_grub2_install_tool_name(self.root_mount.mountpoint)
            ] + self.install_arguments + [
                '--directory', module_directory,
                '--boot-directory', boot_directory,
                '--target', self.target,
                '--modules', self.modules,
                self.install_device
            ]
        )

        if self.firmware and self.firmware.efi_mode() == 'uefi':
            shim_install = self._get_shim_install_tool_name(
                self.root_mount.mountpoint
            )
            # if shim-install does _not_ exist the fallback mechanism
            # has applied at the bootloader/config level and we expect
            # no further tool calls to be required
            if shim_install:
                self.efi_mount = MountManager(
                    device=self.custom_args['efi_device'],
                    mountpoint=self.root_mount.mountpoint + '/boot/efi'
                )
                self.efi_mount.mount()

                # Before we call shim-install, the grub installer binary is
                # replaced by a noop. Actually there is no reason for
                # shim-install to call the grub installer because it should
                # only setup the system for EFI secure boot which does not
                # require any bootloader code in the master boot record.
                # In addition kiwi has called the grub installer right
                # before
                self._disable_grub2_install(self.root_mount.mountpoint)
                Command.run(
                    [
                        'chroot', self.root_mount.mountpoint,
                        'shim-install', '--removable',
                        self.install_device
                    ]
                )
                # restore the grub installer noop
                self._enable_grub2_install(self.root_mount.mountpoint)
Esempio n. 14
0
    def mount(self) -> None:
        """
        Mount image system from current block layers
        """
        # mount root boot and efi devices as they are present
        (root_device, boot_device, efi_device) = self._setup_device_names()
        root_mount = MountManager(device=root_device)
        if 's390' in self.arch:
            boot_mount = MountManager(device=boot_device,
                                      mountpoint=os.path.join(
                                          root_mount.mountpoint, 'boot',
                                          'zipl'))
        else:
            boot_mount = MountManager(device=boot_device,
                                      mountpoint=os.path.join(
                                          root_mount.mountpoint, 'boot'))
        if efi_device:
            efi_mount = MountManager(device=efi_device,
                                     mountpoint=os.path.join(
                                         root_mount.mountpoint, 'boot', 'efi'))

        self.mount_list.append(root_mount)
        root_mount.mount()

        if not root_mount.device == boot_mount.device:
            self.mount_list.append(boot_mount)
            boot_mount.mount()

        if efi_device:
            self.mount_list.append(efi_mount)
            efi_mount.mount()

        if self.volumes:
            self._mount_volumes(root_mount.mountpoint)

        # bind mount /image from unpacked root to get access to e.g scripts
        image_mount = MountManager(device=os.path.join(self.root_dir, 'image'),
                                   mountpoint=os.path.join(
                                       root_mount.mountpoint, 'image'))
        self.mount_list.append(image_mount)
        image_mount.bind_mount()

        # mount tmp as tmpfs
        tmp_mount = MountManager(device='tmpfs',
                                 mountpoint=os.path.join(
                                     root_mount.mountpoint, 'tmp'))
        self.mount_list.append(tmp_mount)
        tmp_mount.tmpfs_mount()

        # mount var/tmp as tmpfs
        var_tmp_mount = MountManager(device='tmpfs',
                                     mountpoint=os.path.join(
                                         root_mount.mountpoint, 'var', 'tmp'))
        self.mount_list.append(var_tmp_mount)
        var_tmp_mount.tmpfs_mount()

        # mount dev as bind
        device_mount = MountManager(device='/dev',
                                    mountpoint=os.path.join(
                                        root_mount.mountpoint, 'dev'))
        self.mount_list.append(device_mount)
        device_mount.bind_mount()

        # mount proc as bind
        proc_mount = MountManager(device='/proc',
                                  mountpoint=os.path.join(
                                      root_mount.mountpoint, 'proc'))
        self.mount_list.append(proc_mount)
        proc_mount.bind_mount()
Esempio n. 15
0
class TestMountManager:
    @fixture(autouse=True)
    def inject_fixtures(self, caplog):
        self._caplog = caplog

    def setup(self):
        self.mount_manager = MountManager('/dev/some-device',
                                          '/some/mountpoint')

    @patch('kiwi.mount_manager.Temporary')
    def test_setup_empty_mountpoint(self, mock_Temporary):
        mock_Temporary.return_value.new_dir.return_value.name = 'tmpdir'
        mount_manager = MountManager('/dev/some-device')
        assert mount_manager.mountpoint == 'tmpdir'

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_bind_mount(self, mock_mounted, mock_command):
        mock_mounted.return_value = False
        self.mount_manager.bind_mount()
        mock_command.assert_called_once_with(
            ['mount', '-n', '--bind', '/dev/some-device', '/some/mountpoint'])

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_mount(self, mock_mounted, mock_command):
        mock_mounted.return_value = False
        self.mount_manager.mount(['options'])
        mock_command.assert_called_once_with(
            ['mount', '-o', 'options', '/dev/some-device', '/some/mountpoint'])

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_umount_lazy(self, mock_mounted, mock_command):
        mock_mounted.return_value = True
        self.mount_manager.umount_lazy()
        mock_command.assert_called_once_with(
            ['umount', '-l', '/some/mountpoint'])

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    @patch('time.sleep')
    def test_umount_with_errors(self, mock_sleep, mock_mounted, mock_command):
        mock_command.side_effect = Exception
        mock_mounted.return_value = True
        with self._caplog.at_level(logging.WARNING):
            assert self.mount_manager.umount(raise_on_busy=False) is False
        assert mock_command.call_args_list == [
            call(['umount', '/some/mountpoint']),  # 1
            call(['umount', '/some/mountpoint']),  # 2
            call(['umount', '/some/mountpoint']),  # 3
            call(['umount', '/some/mountpoint']),  # 4
            call(['umount', '/some/mountpoint']),  # 5
            call(['umount', '/some/mountpoint']),  # 6
            call(['umount', '/some/mountpoint']),  # 7
            call(['umount', '/some/mountpoint']),  # 8
            call(['umount', '/some/mountpoint']),  # 9
            call(['umount', '/some/mountpoint'])  # 10
        ]

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    @patch('time.sleep')
    @patch('kiwi.mount_manager.Path.which')
    def test_umount_with_errors_raises_no_lsof_present(self, mock_Path_which,
                                                       mock_sleep,
                                                       mock_mounted,
                                                       mock_command):
        def command_call(args):
            if 'umount' in args:
                raise Exception

        mock_Path_which.return_value = None
        mock_command.side_effect = command_call
        mock_mounted.return_value = True
        with raises(KiwiUmountBusyError):
            self.mount_manager.umount()

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    @patch('time.sleep')
    @patch('kiwi.mount_manager.Path.which')
    def test_umount_with_errors_raises_lsof_present(self, mock_Path_which,
                                                    mock_sleep, mock_mounted,
                                                    mock_command):
        def command_call(args, raise_on_error=None):
            if 'umount' in args:
                raise Exception
            else:
                call_return = Mock()
                call_return.output = 'HEADLINE\ndata'
                return call_return

        mock_Path_which.return_value = 'lsof'
        mock_command.side_effect = command_call
        mock_mounted.return_value = True
        with raises(KiwiUmountBusyError) as issue:
            self.mount_manager.umount()
        assert 'HEADLINE' in issue.value.message

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_umount_success(self, mock_mounted, mock_command):
        mock_mounted.return_value = True
        assert self.mount_manager.umount() is True
        mock_command.assert_called_once_with(['umount', '/some/mountpoint'])

    @patch('kiwi.mount_manager.Command.run')
    def test_is_mounted_true(self, mock_command):
        command = Mock()
        command.returncode = 0
        mock_command.return_value = command
        assert self.mount_manager.is_mounted() is True
        mock_command.assert_called_once_with(
            command=['mountpoint', '-q', '/some/mountpoint'],
            raise_on_error=False)

    @patch('kiwi.mount_manager.Command.run')
    def test_is_mounted_false(self, mock_command):
        command = Mock()
        command.returncode = 1
        mock_command.return_value = command
        assert self.mount_manager.is_mounted() is False
        mock_command.assert_called_once_with(
            command=['mountpoint', '-q', '/some/mountpoint'],
            raise_on_error=False)
Esempio n. 16
0
class BootLoaderInstallGrub2(BootLoaderInstallBase):
    """
    grub2 bootloader installation

    Attributes

    * :attr:`arch`
        platform.machine

    * :attr:`firmware`
        Instance of FirmWare

    * :attr:`efi_mount`
        Instance of MountManager for EFI device

    * :attr:`root_mount`
        Instance of MountManager for root device

    * :attr:`boot_mount`
        Instance of MountManager for boot device

    * :attr:`device_mount`
        Instance of MountManager for /dev tree

    * :attr:`proc_mount`
        Instance of MountManager for proc

    * :attr:`sysfs_mount`
        Instance of MountManager for sysfs
    """
    def post_init(self, custom_args):
        """
        grub2 post initialization method

        Setup class attributes
        """
        self.arch = platform.machine()
        self.custom_args = custom_args
        self.install_arguments = []
        self.firmware = None
        self.efi_mount = None
        self.root_mount = None
        self.boot_mount = None
        self.device_mount = None
        self.proc_mount = None
        self.sysfs_mount = None
        self.volumes = None
        self.volumes_mount = []
        self.target_removable = None
        if custom_args and 'target_removable' in custom_args:
            self.target_removable = custom_args['target_removable']
        if custom_args and 'system_volumes' in custom_args:
            self.volumes = custom_args['system_volumes']
        if custom_args and 'firmware' in custom_args:
            self.firmware = custom_args['firmware']

        if self.firmware and self.firmware.efi_mode():
            if not custom_args or 'efi_device' not in custom_args:
                raise KiwiBootLoaderGrubInstallError(
                    'EFI device name required for shim installation'
                )
        if not custom_args or 'boot_device' not in custom_args:
            raise KiwiBootLoaderGrubInstallError(
                'boot device name required for grub2 installation'
            )
        if not custom_args or 'root_device' not in custom_args:
            raise KiwiBootLoaderGrubInstallError(
                'root device name required for grub2 installation'
            )

    def install_required(self):
        """
        Check if grub2 has to be installed

        Take architecture and firmware setup into account to check if
        bootloader code in a boot record is required

        :rtype: bool
        """
        if 'ppc64' in self.arch and self.firmware.opal_mode():
            # OPAL doesn't need a grub2 stage1, just a config file.
            # The machine will be setup to kexec grub2 in user space
            log.info(
                'No grub boot code installation in opal mode on %s', self.arch
            )
            return False
        elif 'arm' in self.arch or self.arch == 'aarch64':
            # On arm grub2 is used for EFI setup only, no install
            # of grub2 boot code makes sense
            log.info(
                'No grub boot code installation on %s', self.arch
            )
            return False
        return True

    def install(self):
        """
        Install bootloader on disk device
        """
        log.info('Installing grub2 on disk %s', self.device)

        if self.target_removable:
            self.install_arguments.append('--removable')

        if self.arch == 'x86_64' or self.arch == 'i686' or self.arch == 'i586':
            self.target = 'i386-pc'
            self.install_device = self.device
            self.modules = ' '.join(
                Defaults.get_grub_bios_modules(multiboot=True)
            )
            self.install_arguments.append('--skip-fs-probe')
        elif self.arch.startswith('ppc64'):
            if not self.custom_args or 'prep_device' not in self.custom_args:
                raise KiwiBootLoaderGrubInstallError(
                    'prep device name required for grub2 installation on ppc'
                )
            self.target = 'powerpc-ieee1275'
            self.install_device = self.custom_args['prep_device']
            self.modules = ' '.join(Defaults.get_grub_ofw_modules())
            self.install_arguments.append('--skip-fs-probe')
            self.install_arguments.append('--no-nvram')
        else:
            raise KiwiBootLoaderGrubPlatformError(
                'host architecture %s not supported for grub2 installation' %
                self.arch
            )

        self.root_mount = MountManager(
            device=self.custom_args['root_device']
        )
        self.boot_mount = MountManager(
            device=self.custom_args['boot_device'],
            mountpoint=self.root_mount.mountpoint + '/boot'
        )

        self.root_mount.mount()

        if not self.root_mount.device == self.boot_mount.device:
            self.boot_mount.mount()

        if self.volumes:
            for volume_path in Path.sort_by_hierarchy(
                sorted(self.volumes.keys())
            ):
                volume_mount = MountManager(
                    device=self.volumes[volume_path]['volume_device'],
                    mountpoint=self.root_mount.mountpoint + '/' + volume_path
                )
                self.volumes_mount.append(volume_mount)
                volume_mount.mount(
                    options=[self.volumes[volume_path]['volume_options']]
                )

        self.device_mount = MountManager(
            device='/dev',
            mountpoint=self.root_mount.mountpoint + '/dev'
        )
        self.proc_mount = MountManager(
            device='/proc',
            mountpoint=self.root_mount.mountpoint + '/proc'
        )
        self.sysfs_mount = MountManager(
            device='/sys',
            mountpoint=self.root_mount.mountpoint + '/sys'
        )
        self.device_mount.bind_mount()
        self.proc_mount.bind_mount()
        self.sysfs_mount.bind_mount()

        # check if a grub installation could be found in the image system
        grub_directory = Defaults.get_grub_path(
            self.root_mount.mountpoint + '/usr/lib'
        )
        if not grub_directory:
            raise KiwiBootLoaderGrubDataError(
                'No grub2 installation found in %s' % self.root_mount.mountpoint
            )
        grub_directory = grub_directory.replace(
            self.root_mount.mountpoint, ''
        )
        module_directory = grub_directory + '/' + self.target
        boot_directory = '/boot'

        # wipe existing grubenv to allow the grub installer to create a new one
        grubenv_glob = os.sep.join(
            [self.root_mount.mountpoint, 'boot', '*', 'grubenv']
        )
        for grubenv in glob.glob(grubenv_glob):
            Path.wipe(grubenv)

        # install grub2 boot code
        Command.run(
            [
                'chroot', self.root_mount.mountpoint,
                self._get_grub2_install_tool_name(self.root_mount.mountpoint)
            ] + self.install_arguments + [
                '--directory', module_directory,
                '--boot-directory', boot_directory,
                '--target', self.target,
                '--modules', self.modules,
                self.install_device
            ]
        )

        if self.firmware and self.firmware.efi_mode() == 'uefi':
            shim_install = self._get_shim_install_tool_name(
                self.root_mount.mountpoint
            )
            # if shim-install does _not_ exist the fallback mechanism
            # has applied at the bootloader/config level and we expect
            # no further tool calls to be required
            if shim_install:
                self.efi_mount = MountManager(
                    device=self.custom_args['efi_device'],
                    mountpoint=self.root_mount.mountpoint + '/boot/efi'
                )
                self.efi_mount.mount()

                # Before we call shim-install, the grub installer binary is
                # replaced by a noop. Actually there is no reason for
                # shim-install to call the grub installer because it should
                # only setup the system for EFI secure boot which does not
                # require any bootloader code in the master boot record.
                # In addition kiwi has called the grub installer right
                # before
                self._disable_grub2_install(self.root_mount.mountpoint)
                Command.run(
                    [
                        'chroot', self.root_mount.mountpoint,
                        'shim-install', '--removable',
                        self.install_device
                    ]
                )
                # restore the grub installer noop
                self._enable_grub2_install(self.root_mount.mountpoint)

    def _disable_grub2_install(self, root_path):
        if os.access(root_path, os.W_OK):
            grub2_install = ''.join(
                [
                    root_path, '/usr/sbin/',
                    self._get_grub2_install_tool_name(root_path)
                ]
            )
            grub2_install_backup = ''.join(
                [grub2_install, '.orig']
            )
            grub2_install_noop = ''.join(
                [root_path, '/bin/true']
            )
            Command.run(
                ['cp', '-p', grub2_install, grub2_install_backup]
            )
            Command.run(
                ['cp', grub2_install_noop, grub2_install]
            )

    def _enable_grub2_install(self, root_path):
        if os.access(root_path, os.W_OK):
            grub2_install = ''.join(
                [
                    root_path, '/usr/sbin/',
                    self._get_grub2_install_tool_name(root_path)
                ]
            )
            grub2_install_backup = ''.join(
                [grub2_install, '.orig']
            )
            if os.path.exists(grub2_install_backup):
                Command.run(
                    ['cp', '-p', grub2_install_backup, grub2_install]
                )

    def _get_grub2_install_tool_name(self, root_path):
        return self._get_tool_name(
            root_path, lookup_list=['grub2-install', 'grub-install']
        )

    def _get_shim_install_tool_name(self, root_path):
        return self._get_tool_name(
            root_path, lookup_list=['shim-install'], fallback_on_not_found=False
        )

    def _get_tool_name(
        self, root_path, lookup_list, fallback_on_not_found=True
    ):
        chroot_env = {'PATH': os.sep.join([root_path, 'usr', 'sbin'])}
        for tool in lookup_list:
            if Path.which(filename=tool, custom_env=chroot_env):
                return tool

        if fallback_on_not_found:
            # no tool from the list was found, we intentionally don't
            # raise here but return the default tool name and raise
            # an exception at invocation time in order to log the
            # expected call and its arguments
            return lookup_list[0]

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        for volume_mount in reversed(self.volumes_mount):
            volume_mount.umount()
        if self.device_mount:
            self.device_mount.umount()
        if self.proc_mount:
            self.proc_mount.umount()
        if self.sysfs_mount:
            self.sysfs_mount.umount()
        if self.efi_mount:
            self.efi_mount.umount()
        if self.boot_mount:
            self.boot_mount.umount()
        if self.root_mount:
            self._enable_grub2_install(self.root_mount.mountpoint)
            self.root_mount.umount()
Esempio n. 17
0
class VolumeManagerBtrfs(VolumeManagerBase):
    """
    Implements btrfs sub-volume management

    :param list subvol_mount_list: list of mounted btrfs subvolumes
    :param object toplevel_mount: :class:`MountManager` for root mountpoint
    """
    def post_init(self, custom_args):
        """
        Post initialization method

        Store custom btrfs initialization arguments

        :param list custom_args: custom btrfs volume manager arguments
        """
        if custom_args:
            self.custom_args = custom_args
        else:
            self.custom_args = {}
        if 'root_label' not in self.custom_args:
            self.custom_args['root_label'] = 'ROOT'
        if 'root_is_snapshot' not in self.custom_args:
            self.custom_args['root_is_snapshot'] = False
        if 'root_is_readonly_snapshot' not in self.custom_args:
            self.custom_args['root_is_readonly_snapshot'] = False

        self.subvol_mount_list = []
        self.toplevel_mount = None
        self.toplevel_volume = None

    def setup(self, name=None):
        """
        Setup btrfs volume management

        In case of btrfs a toplevel(@) subvolume is created and marked
        as default volume. If snapshots are activated via the custom_args
        the setup method also created the @/.snapshots/1/snapshot
        subvolumes. There is no concept of a volume manager name, thus
        the name argument is not used for btrfs

        :param string name: unused
        """
        self.setup_mountpoint()

        filesystem = FileSystem(
            name='btrfs',
            device_provider=MappedDevice(
                device=self.device, device_provider=self
            ),
            custom_args=self.custom_filesystem_args
        )
        filesystem.create_on_device(
            label=self.custom_args['root_label']
        )
        self.toplevel_mount = MountManager(
            device=self.device, mountpoint=self.mountpoint
        )
        self.toplevel_mount.mount(
            self.custom_filesystem_args['mount_options']
        )
        root_volume = self.mountpoint + '/@'
        Command.run(
            ['btrfs', 'subvolume', 'create', root_volume]
        )
        if self.custom_args['root_is_snapshot']:
            snapshot_volume = self.mountpoint + '/@/.snapshots'
            Command.run(
                ['btrfs', 'subvolume', 'create', snapshot_volume]
            )
            Path.create(snapshot_volume + '/1')
            snapshot = self.mountpoint + '/@/.snapshots/1/snapshot'
            Command.run(
                ['btrfs', 'subvolume', 'snapshot', root_volume, snapshot]
            )
            self._set_default_volume('@/.snapshots/1/snapshot')
        else:
            self._set_default_volume('@')

    def create_volumes(self, filesystem_name):
        """
        Create configured btrfs subvolumes

        Any btrfs subvolume is of the same btrfs filesystem. There is no
        way to have different filesystems per btrfs subvolume. Thus
        the filesystem_name has no effect for btrfs

        :param string filesystem_name: unused
        """
        log.info(
            'Creating %s sub volumes', filesystem_name
        )
        self.create_volume_paths_in_root_dir()

        canonical_volume_list = self.get_canonical_volume_list()
        if canonical_volume_list.full_size_volume:
            # put an eventual fullsize volume to the volume list
            # because there is no extra handling required for it on btrfs
            canonical_volume_list.volumes.append(
                canonical_volume_list.full_size_volume
            )

        for volume in canonical_volume_list.volumes:
            if volume.name == 'LVRoot':
                # the btrfs root volume named '@' has been created as
                # part of the setup procedure
                pass
            else:
                log.info('--> sub volume %s', volume.realpath)
                toplevel = self.mountpoint + '/@/'
                volume_parent_path = os.path.normpath(
                    toplevel + os.path.dirname(volume.realpath)
                )
                if not os.path.exists(volume_parent_path):
                    Path.create(volume_parent_path)
                Command.run(
                    [
                        'btrfs', 'subvolume', 'create',
                        os.path.normpath(toplevel + volume.realpath)
                    ]
                )
                self.apply_attributes_on_volume(
                    toplevel, volume
                )
                if self.custom_args['root_is_snapshot']:
                    snapshot = self.mountpoint + '/@/.snapshots/1/snapshot/'
                    volume_mount = MountManager(
                        device=self.device,
                        mountpoint=os.path.normpath(snapshot + volume.realpath)
                    )
                    self.subvol_mount_list.append(
                        volume_mount
                    )

    def get_fstab(self, persistency_type='by-label', filesystem_name=None):
        """
        Implements creation of the fstab entries. The method
        returns a list of fstab compatible entries

        :param string persistency_type: by-label | by-uuid
        :param string filesystem_name: unused

        :return: list of fstab entries

        :rtype: list
        """
        fstab_entries = []
        mount_options = \
            self.custom_filesystem_args['mount_options'] or ['defaults']
        block_operation = BlockID(self.device)
        blkid_type = 'LABEL' if persistency_type == 'by-label' else 'UUID'
        device_id = block_operation.get_blkid(blkid_type)
        if self.custom_args['root_is_snapshot']:
            mount_entry_options = mount_options + ['subvol=@/.snapshots']
            fstab_entry = ' '.join(
                [
                    blkid_type + '=' + device_id, '/.snapshots',
                    'btrfs', ','.join(mount_entry_options), '0 0'
                ]
            )
            fstab_entries.append(fstab_entry)
        for volume_mount in self.subvol_mount_list:
            subvol_name = self._get_subvol_name_from_mountpoint(volume_mount)
            mount_entry_options = mount_options + ['subvol=' + subvol_name]
            fstab_entry = ' '.join(
                [
                    blkid_type + '=' + device_id, subvol_name.replace('@', ''),
                    'btrfs', ','.join(mount_entry_options), '0 0'
                ]
            )
            fstab_entries.append(fstab_entry)
        return fstab_entries

    def get_volumes(self):
        """
        Return dict of volumes

        :return: volumes dictionary

        :rtype: dict
        """
        volumes = {}
        for volume_mount in self.subvol_mount_list:
            subvol_name = self._get_subvol_name_from_mountpoint(volume_mount)
            subvol_options = ','.join(
                [
                    'subvol=' + subvol_name
                ] + self.custom_filesystem_args['mount_options']
            )
            volumes[subvol_name.replace('@', '')] = {
                'volume_options': subvol_options,
                'volume_device': volume_mount.device
            }
        return volumes

    def mount_volumes(self):
        """
        Mount btrfs subvolumes
        """

        self.toplevel_mount.mount(
            self.custom_filesystem_args['mount_options']
        )

        for volume_mount in self.subvol_mount_list:
            if self.volumes_mounted_initially:
                volume_mount.mountpoint = os.path.normpath(
                    volume_mount.mountpoint.replace(self.toplevel_volume, '', 1)
                )
            if not os.path.exists(volume_mount.mountpoint):
                Path.create(volume_mount.mountpoint)
            subvol_name = self._get_subvol_name_from_mountpoint(volume_mount)
            subvol_options = ','.join(
                [
                    'subvol=' + subvol_name
                ] + self.custom_filesystem_args['mount_options']
            )
            volume_mount.mount(
                options=[subvol_options]
            )

        self.volumes_mounted_initially = True

    def umount_volumes(self):
        """
        Umount btrfs subvolumes

        :return: True if all subvolumes are successfully unmounted

        :rtype: bool
        """
        all_volumes_umounted = True
        for volume_mount in reversed(self.subvol_mount_list):
            if volume_mount.is_mounted():
                if not volume_mount.umount():
                    all_volumes_umounted = False

        if all_volumes_umounted:
            if self.toplevel_mount.is_mounted():
                if not self.toplevel_mount.umount():
                    all_volumes_umounted = False

        return all_volumes_umounted

    def sync_data(self, exclude=None):
        """
        Sync data into btrfs filesystem

        If snapshots are activated the root filesystem is synced
        into the first snapshot

        :param list exclude: files to exclude from sync
        """
        if self.toplevel_mount:
            sync_target = self.mountpoint + '/@'
            if self.custom_args['root_is_snapshot']:
                sync_target = self.mountpoint + '/@/.snapshots/1/snapshot'
                self._create_snapshot_info(
                    ''.join([self.mountpoint, '/@/.snapshots/1/info.xml'])
                )
            data = DataSync(self.root_dir, sync_target)
            data.sync_data(
                options=['-a', '-H', '-X', '-A', '--one-file-system'],
                exclude=exclude
            )

    def set_property_readonly_root(self):
        """
        Sets the root volume to be a readonly filesystem
        """
        root_is_snapshot = \
            self.custom_args['root_is_snapshot']
        root_is_readonly_snapshot = \
            self.custom_args['root_is_readonly_snapshot']
        if root_is_snapshot and root_is_readonly_snapshot:
            sync_target = self.mountpoint
            Command.run(
                ['btrfs', 'property', 'set', sync_target, 'ro', 'true']
            )

    def _set_default_volume(self, default_volume):
        subvolume_list_call = Command.run(
            ['btrfs', 'subvolume', 'list', self.mountpoint]
        )
        for subvolume in subvolume_list_call.output.split('\n'):
            id_search = re.search('ID (\d+) .*path (.*)', subvolume)
            if id_search:
                volume_id = id_search.group(1)
                volume_path = id_search.group(2)
                if volume_path == default_volume:
                    Command.run(
                        [
                            'btrfs', 'subvolume', 'set-default',
                            volume_id, self.mountpoint
                        ]
                    )
                    self.toplevel_volume = default_volume
                    return

        raise KiwiVolumeRootIDError(
            'Failed to find btrfs volume: %s' % default_volume
        )

    def _xml_pretty(self, toplevel_element):
        xml_data_unformatted = ElementTree.tostring(
            toplevel_element, 'utf-8'
        )
        xml_data_domtree = minidom.parseString(xml_data_unformatted)
        return xml_data_domtree.toprettyxml(indent="    ")

    def _create_snapshot_info(self, filename):
        date_info = datetime.datetime.now()
        snapshot = ElementTree.Element('snapshot')

        snapshot_type = ElementTree.SubElement(snapshot, 'type')
        snapshot_type.text = 'single'

        snapshot_number = ElementTree.SubElement(snapshot, 'num')
        snapshot_number.text = '1'

        snapshot_description = ElementTree.SubElement(snapshot, 'description')
        snapshot_description.text = 'first root filesystem'

        snapshot_date = ElementTree.SubElement(snapshot, 'date')
        snapshot_date.text = date_info.strftime("%Y-%m-%d %H:%M:%S")

        with open(filename, 'w') as snapshot_info_file:
            snapshot_info_file.write(self._xml_pretty(snapshot))

    def _get_subvol_name_from_mountpoint(self, volume_mount):
        subvol_name = '/'.join(volume_mount.mountpoint.split('/')[3:])
        if self.toplevel_volume and self.toplevel_volume in subvol_name:
            subvol_name = subvol_name.replace(self.toplevel_volume, '')
        return os.path.normpath(os.sep.join(['@', subvol_name]))

    def __del__(self):
        if self.toplevel_mount:
            log.info('Cleaning up %s instance', type(self).__name__)
            if self.umount_volumes():
                Path.wipe(self.mountpoint)
Esempio n. 18
0
class BootLoaderInstallGrub2(BootLoaderInstallBase):
    """
    grub2 bootloader installation

    Attributes

    * :attr:`arch`
        platform.machine

    * :attr:`firmware`
        Instance of FirmWare

    * :attr:`efi_mount`
        Instance of MountManager for EFI device

    * :attr:`root_mount`
        Instance of MountManager for root device

    * :attr:`boot_mount`
        Instance of MountManager for boot device

    * :attr:`device_mount`
        Instance of MountManager for /dev tree

    * :attr:`proc_mount`
        Instance of MountManager for proc

    * :attr:`sysfs_mount`
        Instance of MountManager for sysfs
    """
    def post_init(self, custom_args):
        """
        grub2 post initialization method

        Setup class attributes
        """
        self.arch = platform.machine()
        self.custom_args = custom_args
        self.install_arguments = []
        self.firmware = None
        self.efi_mount = None
        self.root_mount = None
        self.boot_mount = None
        self.device_mount = None
        self.proc_mount = None
        self.sysfs_mount = None
        self.volumes = None
        self.volumes_mount = []
        self.target_removable = None
        if custom_args and 'target_removable' in custom_args:
            self.target_removable = custom_args['target_removable']
        if custom_args and 'system_volumes' in custom_args:
            self.volumes = custom_args['system_volumes']
        if custom_args and 'firmware' in custom_args:
            self.firmware = custom_args['firmware']

        if self.firmware and self.firmware.efi_mode():
            if not custom_args or 'efi_device' not in custom_args:
                raise KiwiBootLoaderGrubInstallError(
                    'EFI device name required for shim installation')
        if not custom_args or 'boot_device' not in custom_args:
            raise KiwiBootLoaderGrubInstallError(
                'boot device name required for grub2 installation')
        if not custom_args or 'root_device' not in custom_args:
            raise KiwiBootLoaderGrubInstallError(
                'root device name required for grub2 installation')

    def install_required(self):
        """
        Check if grub2 has to be installed

        Take architecture and firmware setup into account to check if
        bootloader code in a boot record is required

        :rtype: bool
        """
        if 'ppc64' in self.arch and self.firmware.opal_mode():
            # OPAL doesn't need a grub2 stage1, just a config file.
            # The machine will be setup to kexec grub2 in user space
            log.info('No grub boot code installation in opal mode on %s',
                     self.arch)
            return False
        elif 'arm' in self.arch or self.arch == 'aarch64':
            # On arm grub2 is used for EFI setup only, no install
            # of grub2 boot code makes sense
            log.info('No grub boot code installation on %s', self.arch)
            return False
        return True

    def install(self):
        """
        Install bootloader on disk device
        """
        log.info('Installing grub2 on disk %s', self.device)

        if self.target_removable:
            self.install_arguments.append('--removable')

        if self.arch == 'x86_64' or self.arch == 'i686' or self.arch == 'i586':
            self.target = 'i386-pc'
            self.install_device = self.device
            self.modules = ' '.join(
                Defaults.get_grub_bios_modules(multiboot=True))
            self.install_arguments.append('--skip-fs-probe')
        elif self.arch.startswith('ppc64'):
            if not self.custom_args or 'prep_device' not in self.custom_args:
                raise KiwiBootLoaderGrubInstallError(
                    'prep device name required for grub2 installation on ppc')
            self.target = 'powerpc-ieee1275'
            self.install_device = self.custom_args['prep_device']
            self.modules = ' '.join(Defaults.get_grub_ofw_modules())
            self.install_arguments.append('--skip-fs-probe')
            self.install_arguments.append('--no-nvram')
        else:
            raise KiwiBootLoaderGrubPlatformError(
                'host architecture %s not supported for grub2 installation' %
                self.arch)

        self.root_mount = MountManager(device=self.custom_args['root_device'])
        self.boot_mount = MountManager(device=self.custom_args['boot_device'],
                                       mountpoint=self.root_mount.mountpoint +
                                       '/boot')

        self.root_mount.mount()

        if not self.root_mount.device == self.boot_mount.device:
            self.boot_mount.mount()

        if self.volumes:
            for volume_path in Path.sort_by_hierarchy(
                    sorted(self.volumes.keys())):
                volume_mount = MountManager(
                    device=self.volumes[volume_path]['volume_device'],
                    mountpoint=self.root_mount.mountpoint + '/' + volume_path)
                self.volumes_mount.append(volume_mount)
                volume_mount.mount(
                    options=[self.volumes[volume_path]['volume_options']])

        self.device_mount = MountManager(
            device='/dev', mountpoint=self.root_mount.mountpoint + '/dev')
        self.proc_mount = MountManager(device='/proc',
                                       mountpoint=self.root_mount.mountpoint +
                                       '/proc')
        self.sysfs_mount = MountManager(device='/sys',
                                        mountpoint=self.root_mount.mountpoint +
                                        '/sys')
        self.device_mount.bind_mount()
        self.proc_mount.bind_mount()
        self.sysfs_mount.bind_mount()

        # check if a grub installation could be found in the image system
        grub_directory = Defaults.get_grub_path(self.root_mount.mountpoint +
                                                '/usr/lib')
        if not grub_directory:
            raise KiwiBootLoaderGrubDataError(
                'No grub2 installation found in %s' %
                self.root_mount.mountpoint)
        grub_directory = grub_directory.replace(self.root_mount.mountpoint, '')
        module_directory = grub_directory + '/' + self.target
        boot_directory = '/boot'

        # wipe existing grubenv to allow the grub installer to create a new one
        grubenv_glob = os.sep.join(
            [self.root_mount.mountpoint, 'boot', '*', 'grubenv'])
        for grubenv in glob.glob(grubenv_glob):
            Path.wipe(grubenv)

        # install grub2 boot code
        Command.run([
            'chroot', self.root_mount.mountpoint,
            self._get_grub2_install_tool_name(self.root_mount.mountpoint)
        ] + self.install_arguments + [
            '--directory', module_directory, '--boot-directory',
            boot_directory, '--target', self.target, '--modules', self.modules,
            self.install_device
        ])

        if self.firmware and self.firmware.efi_mode() == 'uefi':
            shim_install = self._get_shim_install_tool_name(
                self.root_mount.mountpoint)
            # if shim-install does _not_ exist the fallback mechanism
            # has applied at the bootloader/config level and we expect
            # no further tool calls to be required
            if shim_install:
                self.efi_mount = MountManager(
                    device=self.custom_args['efi_device'],
                    mountpoint=self.root_mount.mountpoint + '/boot/efi')
                self.efi_mount.mount()

                # Before we call shim-install, the grub installer binary is
                # replaced by a noop. Actually there is no reason for
                # shim-install to call the grub installer because it should
                # only setup the system for EFI secure boot which does not
                # require any bootloader code in the master boot record.
                # In addition kiwi has called the grub installer right
                # before
                self._disable_grub2_install(self.root_mount.mountpoint)
                Command.run([
                    'chroot', self.root_mount.mountpoint, 'shim-install',
                    '--removable', self.install_device
                ])
                # restore the grub installer noop
                self._enable_grub2_install(self.root_mount.mountpoint)

    def _disable_grub2_install(self, root_path):
        if os.access(root_path, os.W_OK):
            grub2_install = ''.join([
                root_path, '/usr/sbin/',
                self._get_grub2_install_tool_name(root_path)
            ])
            grub2_install_backup = ''.join([grub2_install, '.orig'])
            grub2_install_noop = ''.join([root_path, '/bin/true'])
            Command.run(['cp', '-p', grub2_install, grub2_install_backup])
            Command.run(['cp', grub2_install_noop, grub2_install])

    def _enable_grub2_install(self, root_path):
        if os.access(root_path, os.W_OK):
            grub2_install = ''.join([
                root_path, '/usr/sbin/',
                self._get_grub2_install_tool_name(root_path)
            ])
            grub2_install_backup = ''.join([grub2_install, '.orig'])
            if os.path.exists(grub2_install_backup):
                Command.run(['cp', '-p', grub2_install_backup, grub2_install])

    def _get_grub2_install_tool_name(self, root_path):
        return self._get_tool_name(
            root_path, lookup_list=['grub2-install', 'grub-install'])

    def _get_shim_install_tool_name(self, root_path):
        return self._get_tool_name(root_path,
                                   lookup_list=['shim-install'],
                                   fallback_on_not_found=False)

    def _get_tool_name(self,
                       root_path,
                       lookup_list,
                       fallback_on_not_found=True):
        chroot_env = {'PATH': os.sep.join([root_path, 'usr', 'sbin'])}
        for tool in lookup_list:
            if Path.which(filename=tool, custom_env=chroot_env):
                return tool

        if fallback_on_not_found:
            # no tool from the list was found, we intentionally don't
            # raise here but return the default tool name and raise
            # an exception at invocation time in order to log the
            # expected call and its arguments
            return lookup_list[0]

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        for volume_mount in reversed(self.volumes_mount):
            volume_mount.umount()
        if self.device_mount:
            self.device_mount.umount()
        if self.proc_mount:
            self.proc_mount.umount()
        if self.sysfs_mount:
            self.sysfs_mount.umount()
        if self.efi_mount:
            self.efi_mount.umount()
        if self.boot_mount:
            self.boot_mount.umount()
        if self.root_mount:
            self._enable_grub2_install(self.root_mount.mountpoint)
            self.root_mount.umount()
Esempio n. 19
0
class BootLoaderInstallZipl(BootLoaderInstallBase):
    """
    **zipl bootloader installation**
    """
    def post_init(self, custom_args):
        """
        zipl post initialization method

        :param dict custom_args:
            Contains custom zipl bootloader arguments

            .. code:: python

                {'boot_device': string}

        """
        self.custom_args = custom_args
        if not custom_args or 'boot_device' not in custom_args:
            raise KiwiBootLoaderZiplInstallError(
                'boot device node name required for zipl installation'
            )

        self.boot_mount = MountManager(
            custom_args['boot_device']
        )

    def install_required(self):
        """
        Check if zipl has to be installed

        Always required

        :return: True

        :rtype: bool
        """
        return True

    def install(self):
        """
        Install bootloader on self.device
        """
        log.info('Installing zipl on disk %s', self.device)

        self.boot_mount.mount()

        bash_command = ' '.join(
            [
                'cd', self.boot_mount.mountpoint, '&&',
                'zipl', '-V', '-c', self.boot_mount.mountpoint + '/config',
                '-m', 'menu'
            ]
        )
        zipl_call = Command.run(
            ['bash', '-c', bash_command]
        )
        log.debug('zipl install succeeds with: %s', zipl_call.output)

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        self.boot_mount.umount()
Esempio n. 20
0
class FileSystemBase:
    """
    **Implements base class for filesystem interface**

    :param object device_provider:
        Instance of a class based on DeviceProvider
        required for filesystems which needs a block device for
        creation. In most cases the DeviceProvider is a LoopDevice
    :param string root_dir: root directory path name
    :param dict custom_args: custom filesystem arguments
    """
    def __init__(
        self, device_provider: DeviceProvider,
        root_dir: str = '', custom_args: Dict = {}
    ):
        # filesystems created with a block device stores the mountpoint
        # here. The file name of the file containing the filesystem is
        # stored in the device_provider if the filesystem is represented
        # as a file there
        self.filesystem_mount: Optional[MountManager] = None

        # bind the block device providing class instance to this object.
        # This is done to guarantee the correct destructor order when
        # the device should be released. This is only required if the
        # filesystem required a block device to become created
        self.device_provider = device_provider

        self.root_dir = root_dir

        # filesystems created without a block device stores the result
        # filesystem file name here
        self.filename = ''

        self.custom_args: Dict = {}
        self.post_init(custom_args)
        self.veritysetup: Optional[VeritySetup] = None

    def post_init(self, custom_args: Dict):
        """
        Post initialization method

        Store dictionary of custom arguments if not empty. This
        overrides the default custom argument hash

        :param dict custom_args: custom arguments

            .. code:: python

                {
                    'create_options': ['option'],
                    'mount_options': ['option'],
                    'meta_data': {
                        'key': 'value'
                    }
                }
        """
        if custom_args:
            self.custom_args = copy.deepcopy(custom_args)

        if not self.custom_args.get('create_options'):
            self.custom_args['create_options'] = []

        if not self.custom_args.get('meta_data'):
            self.custom_args['meta_data'] = {}

        if not self.custom_args.get('mount_options'):
            self.custom_args['mount_options'] = []

        if not self.custom_args.get('fs_attributes'):
            self.custom_args['fs_attributes'] = []

    def set_uuid(self):
        """
        Create new random filesystem UUID

        Implement in specialized filesystem class for filesystems which
        supports the concept of an UUID and allows to change it
        """
        log.warning(
            'Instance {0} has no support for setting a new UUID label'.format(
                type(self).__name__
            )
        )

    def create_on_device(
        self, label: str = None, size: int = 0, unit: str = defaults.UNIT.kb,
        uuid: str = None
    ):
        """
        Create filesystem on block device

        Implement in specialized filesystem class for filesystems which
        requires a block device for creation, e.g ext4.

        :param str label: label name
        :param int size:
            size value, can also be counted from the end via -X
            The value is interpreted in units of: unit
        :param str unit:
            unit name. Default unit is set to: defaults.UNIT.kb
        :param str uuid: UUID name
        """
        raise NotImplementedError

    def create_on_file(
        self, filename: str, label: str = None, exclude: List[str] = None
    ):
        """
        Create filesystem from root data tree

        Implement in specialized filesystem class for filesystems which
        requires a data tree for creation, e.g squashfs.

        :param string filename: result file path name
        :param string label: label name
        :param list exclude: list of exclude dirs/files
        """
        raise NotImplementedError

    def get_mountpoint(self) -> Optional[str]:
        """
        Provides mount point directory

        Effective use of the directory is guaranteed only after sync_data

        :return: directory path name

        :rtype: string
        """
        if self.filesystem_mount:
            return self.filesystem_mount.mountpoint
        return None

    def sync_data(self, exclude: List[str] = []):
        """
        Copy root data tree into filesystem

        :param list exclude: list of exclude dirs/files
        """
        if not self.root_dir:
            raise KiwiFileSystemSyncError(
                'no root directory specified'
            )
        if not os.path.exists(self.root_dir):
            raise KiwiFileSystemSyncError(
                'given root directory %s does not exist' % self.root_dir
            )
        self.filesystem_mount = MountManager(
            device=self.device_provider.get_device()
        )
        self.filesystem_mount.mount(
            self.custom_args['mount_options']
        )
        self._apply_attributes()
        data = DataSync(
            self.root_dir, self.filesystem_mount.mountpoint
        )
        data.sync_data(
            exclude=exclude, options=Defaults.get_sync_options()
        )

    def create_verity_layer(
        self, blocks: Optional[int] = None, filename: str = None
    ):
        """
        Create veritysetup on device

        :param int block:
            Number of blocks to use for veritysetup.
            If not specified the entire root device is used
        :param str filename:
            Target filename to use for VeritySetup.
            If not specified the filename or block special
            provided at object construction time is used
        """
        on_file_name = filename or self.filename
        self.veritysetup = VeritySetup(
            on_file_name or self.device_provider.get_device(),
            blocks
        )
        log.info(
            '--> Creating dm verity hash ({0} blocks)...'.format(
                blocks or 'all'
            )
        )
        log.debug(
            '--> dm verity metadata: {0}'.format(self.veritysetup.format())
        )

    def create_verification_metadata(self, device_node: str = '') -> None:
        """
        Write verification block at the end of the device

        :param str device_node:
            Target device node, if not specified the root device
            from this instance is used
        """
        if self.veritysetup:
            log.info('--> Creating verification metadata...')
            self.veritysetup.create_verity_verification_metadata()
            log.info('--> Signing verification metadata...')
            self.veritysetup.sign_verification_metadata()
            self.veritysetup.write_verification_metadata(
                device_node or self.device_provider.get_device()
            )

    def umount(self):
        """
        Umounts the filesystem in case it is mounted, does nothing otherwise
        """
        if self.filesystem_mount:
            log.info('umount %s instance', type(self).__name__)
            self.filesystem_mount.umount()
            self.filesystem_mount = None

    def _map_size(self, size: float, from_unit: str, to_unit: str) -> float:
        """
        Return byte size value for given size and unit

        :param float size:
            requested filesystem size. The value is interpreted
            by the given from_unit.

        :param str from_unit: source unit
        :param str to_unit: target unit

        :return: size value in unit: to_unit

        :rtype: float
        """
        unit_map = {
            defaults.UNIT.byte: 1,
            defaults.UNIT.kb: 1024,
            defaults.UNIT.mb: 1048576,
            defaults.UNIT.gb: 1073741824
        }
        byte_size = size * unit_map[from_unit]
        return byte_size / unit_map[to_unit]

    def _fs_size(
        self, size: float, blocksize: int = 1, unit: str = defaults.UNIT.kb
    ) -> str:
        """
        Calculate filesystem size parameter in number of blocksize
        blocks. If the given size is <= 0 the calculation is done from
        the actual size of the block device reduced by the given size

        :param float size:
            requested filesystem size. The value is interpreted
            by the given unit.

        :param int blocksize:
            blocksize as requested from the filesystem creation tool
            for specifying the filesystem size. The value is interpreted
            by the given unit. By default set to: 1

        :param str unit:
            Unit to use for calculations and return value
            Default unit is set to: defaults.UNIT.kb

        :return: an int block count of the specified unit as str

        :rtype: str
        """
        if size > 0:
            result_size = size / blocksize
        else:
            device_name = self.device_provider.get_device()
            device_byte_size = self.device_provider.get_byte_size(device_name)
            requested_byte_size = self._map_size(size, unit, defaults.UNIT.byte)
            result_size = self._map_size(
                (device_byte_size + requested_byte_size) / blocksize,
                from_unit=defaults.UNIT.byte, to_unit=unit
            )
        return format(int(result_size))

    def _apply_attributes(self):
        """
        Apply filesystem attributes
        """
        attribute_map = {
            'synchronous-updates': '+S',
            'no-copy-on-write': '+C'
        }
        for attribute in self.custom_args['fs_attributes']:
            if attribute_map.get(attribute):
                log.info(
                    '--> setting {0} for {1}'.format(
                        attribute, self.filesystem_mount.mountpoint
                    )
                )
                Command.run(
                    [
                        'chattr', attribute_map.get(attribute),
                        self.filesystem_mount.mountpoint
                    ]
                )

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        self.umount()
Esempio n. 21
0
class TestMountManager:
    @fixture(autouse=True)
    def inject_fixtures(self, caplog):
        self._caplog = caplog

    def setup(self):
        self.mount_manager = MountManager(
            '/dev/some-device', '/some/mountpoint'
        )

    @patch('kiwi.mount_manager.Temporary')
    def test_setup_empty_mountpoint(self, mock_Temporary):
        mock_Temporary.return_value.new_dir.return_value.name = 'tmpdir'
        mount_manager = MountManager('/dev/some-device')
        assert mount_manager.mountpoint == 'tmpdir'

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_bind_mount(self, mock_mounted, mock_command):
        mock_mounted.return_value = False
        self.mount_manager.bind_mount()
        mock_command.assert_called_once_with(
            ['mount', '-n', '--bind', '/dev/some-device', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_mount(self, mock_mounted, mock_command):
        mock_mounted.return_value = False
        self.mount_manager.mount(['options'])
        mock_command.assert_called_once_with(
            ['mount', '-o', 'options', '/dev/some-device', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_umount_lazy(self, mock_mounted, mock_command):
        mock_mounted.return_value = True
        self.mount_manager.umount_lazy()
        mock_command.assert_called_once_with(
            ['umount', '-l', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    @patch('time.sleep')
    def test_umount_with_errors(
        self, mock_sleep, mock_mounted, mock_command
    ):
        mock_command.side_effect = Exception
        mock_mounted.return_value = True
        with self._caplog.at_level(logging.WARNING):
            assert self.mount_manager.umount() is False
        assert mock_command.call_args_list == [
            call(['umount', '/some/mountpoint']),
            call(['umount', '/some/mountpoint']),
            call(['umount', '/some/mountpoint'])
        ]

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_umount_success(self, mock_mounted, mock_command):
        mock_mounted.return_value = True
        assert self.mount_manager.umount() is True
        mock_command.assert_called_once_with(
            ['umount', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    def test_is_mounted_true(self, mock_command):
        command = Mock()
        command.returncode = 0
        mock_command.return_value = command
        assert self.mount_manager.is_mounted() is True
        mock_command.assert_called_once_with(
            command=['mountpoint', '-q', '/some/mountpoint'],
            raise_on_error=False
        )

    @patch('kiwi.mount_manager.Command.run')
    def test_is_mounted_false(self, mock_command):
        command = Mock()
        command.returncode = 1
        mock_command.return_value = command
        assert self.mount_manager.is_mounted() is False
        mock_command.assert_called_once_with(
            command=['mountpoint', '-q', '/some/mountpoint'],
            raise_on_error=False
        )
Esempio n. 22
0
class FileSystemBase:
    """
    **Implements base class for filesystem interface**

    :param object device_provider:
        Instance of a class based on DeviceProvider
        required for filesystems which needs a block device for
        creation. In most cases the DeviceProvider is a LoopDevice
    :param string root_dir: root directory path name
    :param dict custom_args: custom filesystem arguments
    """
    def __init__(self,
                 device_provider: DeviceProvider,
                 root_dir: str = None,
                 custom_args: Dict = None):
        # filesystems created with a block device stores the mountpoint
        # here. The file name of the file containing the filesystem is
        # stored in the device_provider if the filesystem is represented
        # as a file there
        self.filesystem_mount = None

        # bind the block device providing class instance to this object.
        # This is done to guarantee the correct destructor order when
        # the device should be released. This is only required if the
        # filesystem required a block device to become created
        self.device_provider = device_provider

        self.root_dir = root_dir

        # filesystems created without a block device stores the result
        # filesystem file name here
        self.filename = None

        self.custom_args = {}
        self.post_init(custom_args)

    def post_init(self, custom_args: Dict):
        """
        Post initialization method

        Store dictionary of custom arguments if not empty. This
        overrides the default custom argument hash

        :param dict custom_args: custom arguments

            .. code:: python

                {
                    'create_options': ['option'],
                    'mount_options': ['option'],
                    'meta_data': {
                        'key': 'value'
                    }
                }
        """
        if custom_args:
            self.custom_args = copy.deepcopy(custom_args)

        if not self.custom_args.get('create_options'):
            self.custom_args['create_options'] = []

        if not self.custom_args.get('meta_data'):
            self.custom_args['meta_data'] = {}

        if not self.custom_args.get('mount_options'):
            self.custom_args['mount_options'] = []

        if not self.custom_args.get('fs_attributes'):
            self.custom_args['fs_attributes'] = []

    def create_on_device(self, label: str = None):
        """
        Create filesystem on block device

        Implement in specialized filesystem class for filesystems which
        requires a block device for creation, e.g ext4.

        :param string label: label name
        """
        raise NotImplementedError

    def create_on_file(self,
                       filename: str,
                       label: str = None,
                       exclude: List[str] = None):
        """
        Create filesystem from root data tree

        Implement in specialized filesystem class for filesystems which
        requires a data tree for creation, e.g squashfs.

        :param string filename: result file path name
        :param string label: label name
        :param list exclude: list of exclude dirs/files
        """
        raise NotImplementedError

    def get_mountpoint(self) -> Optional[str]:
        """
        Provides mount point directory

        Effective use of the directory is guaranteed only after sync_data

        :return: directory path name

        :rtype: string
        """
        if self.filesystem_mount:
            return self.filesystem_mount.mountpoint

    def sync_data(self, exclude: List[str] = None):
        """
        Copy root data tree into filesystem

        :param list exclude: list of exclude dirs/files
        """
        if not self.root_dir:
            raise KiwiFileSystemSyncError('no root directory specified')
        if not os.path.exists(self.root_dir):
            raise KiwiFileSystemSyncError(
                'given root directory %s does not exist' % self.root_dir)
        self.filesystem_mount = MountManager(
            device=self.device_provider.get_device())
        self.filesystem_mount.mount(self.custom_args['mount_options'])
        self._apply_attributes()
        data = DataSync(self.root_dir, self.filesystem_mount.mountpoint)
        data.sync_data(exclude=exclude, options=Defaults.get_sync_options())

    def umount(self):
        """
        Umounts the filesystem in case it is mounted, does nothing otherwise
        """
        if self.filesystem_mount:
            log.info('umount %s instance', type(self).__name__)
            self.filesystem_mount.umount()
            self.filesystem_mount = None

    def _apply_attributes(self):
        """
        Apply filesystem attributes
        """
        attribute_map = {'synchronous-updates': '+S', 'no-copy-on-write': '+C'}
        for attribute in self.custom_args['fs_attributes']:
            if attribute_map.get(attribute):
                log.info('--> setting {0} for {1}'.format(
                    attribute, self.filesystem_mount.mountpoint))
                Command.run([
                    'chattr',
                    attribute_map.get(attribute),
                    self.filesystem_mount.mountpoint
                ])

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        self.umount()
Esempio n. 23
0
class FileSystemBase(object):
    """
    Implements base class for filesystem interface

    Attributes

    * :attr:`filesystem_mount`
        mount point when the filesystem is mounted for data sync

    * :attr:`device_provider`
        Instance of a class based on DeviceProvider
        required for filesystems which needs a block device for
        creation. In most cases the DeviceProvider is a LoopDevice

    * :attr:`root_dir`
        root directory path name

    * :attr:`filename`
        filesystem file if no block device is needed to create
        it, e.g squashfs

    * :attr:`custom_args`
        custom filesystem arguments
    """
    def __init__(self, device_provider, root_dir=None, custom_args=None):
        # filesystems created with a block device stores the mountpoint
        # here. The file name of the file containing the filesystem is
        # stored in the device_provider if the filesystem is represented
        # as a file there
        self.filesystem_mount = None

        # bind the block device providing class instance to this object.
        # This is done to guarantee the correct destructor order when
        # the device should be released. This is only required if the
        # filesystem required a block device to become created
        self.device_provider = device_provider

        self.root_dir = root_dir

        # filesystems created without a block device stores the result
        # filesystem file name here
        self.filename = None

        self.custom_args = {}
        self.post_init(custom_args)

    def post_init(self, custom_args):
        """
        Post initialization method

        Store dictionary of custom arguments if not empty. This
        overrides the default custom argument hash

        :param dict custom_args: custom arguments
        """
        if custom_args:
            self.custom_args = custom_args

        if 'create_options' not in self.custom_args:
            self.custom_args['create_options'] = []

        if 'mount_options' not in self.custom_args:
            self.custom_args['mount_options'] = []

    def create_on_device(self, label=None):
        """
        Create filesystem on block device

        Implement in specialized filesystem class for filesystems which
        requires a block device for creation, e.g ext4.

        :param string label: label name
        """
        raise NotImplementedError

    def create_on_file(self, filename, label=None, exclude=None):
        """
        Create filesystem from root data tree

        Implement in specialized filesystem class for filesystems which
        requires a data tree for creation, e.g squashfs.

        :param string filename: result file path name
        :param string label: label name
        :param list exclude: list of exclude dirs/files
        """
        raise NotImplementedError

    def sync_data(self, exclude=None):
        """
        Copy root data tree into filesystem

        :param list exclude: list of exclude dirs/files
        """
        if not self.root_dir:
            raise KiwiFileSystemSyncError(
                'no root directory specified'
            )
        if not os.path.exists(self.root_dir):
            raise KiwiFileSystemSyncError(
                'given root directory %s does not exist' % self.root_dir
            )
        self.filesystem_mount = MountManager(
            device=self.device_provider.get_device()
        )
        self.filesystem_mount.mount(
            self.custom_args['mount_options']
        )
        data = DataSync(
            self.root_dir, self.filesystem_mount.mountpoint
        )
        data.sync_data(
            options=['-a', '-H', '-X', '-A', '--one-file-system'],
            exclude=exclude
        )
        self.filesystem_mount.umount()

    def __del__(self):
        if self.filesystem_mount:
            log.info('Cleaning up %s instance', type(self).__name__)
            self.filesystem_mount.umount()
Esempio n. 24
0
    def install(self):
        """
        Install bootloader on disk device
        """
        log.info('Installing grub2 on disk %s', self.device)

        if self.target_removable:
            self.install_arguments.append('--removable')

        if self.arch == 'x86_64' or self.arch == 'i686' or self.arch == 'i586':
            self.target = 'i386-pc'
            self.install_device = self.device
            self.modules = ' '.join(
                Defaults.get_grub_bios_modules(multiboot=True))
            self.install_arguments.append('--skip-fs-probe')
        elif self.arch.startswith('ppc64'):
            if not self.custom_args or 'prep_device' not in self.custom_args:
                raise KiwiBootLoaderGrubInstallError(
                    'prep device name required for grub2 installation on ppc')
            self.target = 'powerpc-ieee1275'
            self.install_device = self.custom_args['prep_device']
            self.modules = ' '.join(Defaults.get_grub_ofw_modules())
            self.install_arguments.append('--skip-fs-probe')
            self.install_arguments.append('--no-nvram')
        else:
            raise KiwiBootLoaderGrubPlatformError(
                'host architecture %s not supported for grub2 installation' %
                self.arch)

        self.root_mount = MountManager(device=self.custom_args['root_device'])
        self.boot_mount = MountManager(device=self.custom_args['boot_device'],
                                       mountpoint=self.root_mount.mountpoint +
                                       '/boot')

        self.root_mount.mount()

        if not self.root_mount.device == self.boot_mount.device:
            self.boot_mount.mount()

        if self.volumes:
            for volume_path in Path.sort_by_hierarchy(
                    sorted(self.volumes.keys())):
                volume_mount = MountManager(
                    device=self.volumes[volume_path]['volume_device'],
                    mountpoint=self.root_mount.mountpoint + '/' + volume_path)
                self.volumes_mount.append(volume_mount)
                volume_mount.mount(
                    options=[self.volumes[volume_path]['volume_options']])

        self.device_mount = MountManager(
            device='/dev', mountpoint=self.root_mount.mountpoint + '/dev')
        self.proc_mount = MountManager(device='/proc',
                                       mountpoint=self.root_mount.mountpoint +
                                       '/proc')
        self.sysfs_mount = MountManager(device='/sys',
                                        mountpoint=self.root_mount.mountpoint +
                                        '/sys')
        self.device_mount.bind_mount()
        self.proc_mount.bind_mount()
        self.sysfs_mount.bind_mount()

        # check if a grub installation could be found in the image system
        grub_directory = Defaults.get_grub_path(self.root_mount.mountpoint +
                                                '/usr/lib')
        if not grub_directory:
            raise KiwiBootLoaderGrubDataError(
                'No grub2 installation found in %s' %
                self.root_mount.mountpoint)
        grub_directory = grub_directory.replace(self.root_mount.mountpoint, '')
        module_directory = grub_directory + '/' + self.target
        boot_directory = '/boot'

        # wipe existing grubenv to allow the grub installer to create a new one
        grubenv_glob = os.sep.join(
            [self.root_mount.mountpoint, 'boot', '*', 'grubenv'])
        for grubenv in glob.glob(grubenv_glob):
            Path.wipe(grubenv)

        # install grub2 boot code
        Command.run([
            'chroot', self.root_mount.mountpoint,
            self._get_grub2_install_tool_name(self.root_mount.mountpoint)
        ] + self.install_arguments + [
            '--directory', module_directory, '--boot-directory',
            boot_directory, '--target', self.target, '--modules', self.modules,
            self.install_device
        ])

        if self.firmware and self.firmware.efi_mode() == 'uefi':
            shim_install = self._get_shim_install_tool_name(
                self.root_mount.mountpoint)
            # if shim-install does _not_ exist the fallback mechanism
            # has applied at the bootloader/config level and we expect
            # no further tool calls to be required
            if shim_install:
                self.efi_mount = MountManager(
                    device=self.custom_args['efi_device'],
                    mountpoint=self.root_mount.mountpoint + '/boot/efi')
                self.efi_mount.mount()

                # Before we call shim-install, the grub installer binary is
                # replaced by a noop. Actually there is no reason for
                # shim-install to call the grub installer because it should
                # only setup the system for EFI secure boot which does not
                # require any bootloader code in the master boot record.
                # In addition kiwi has called the grub installer right
                # before
                self._disable_grub2_install(self.root_mount.mountpoint)
                Command.run([
                    'chroot', self.root_mount.mountpoint, 'shim-install',
                    '--removable', self.install_device
                ])
                # restore the grub installer noop
                self._enable_grub2_install(self.root_mount.mountpoint)
Esempio n. 25
0
class FileSystemBase:
    """
    **Implements base class for filesystem interface**

    :param object device_provider:
        Instance of a class based on DeviceProvider
        required for filesystems which needs a block device for
        creation. In most cases the DeviceProvider is a LoopDevice
    :param string root_dir: root directory path name
    :param dict custom_args: custom filesystem arguments
    """
    def __init__(self, device_provider, root_dir=None, custom_args=None):
        # filesystems created with a block device stores the mountpoint
        # here. The file name of the file containing the filesystem is
        # stored in the device_provider if the filesystem is represented
        # as a file there
        self.filesystem_mount = None

        # bind the block device providing class instance to this object.
        # This is done to guarantee the correct destructor order when
        # the device should be released. This is only required if the
        # filesystem required a block device to become created
        self.device_provider = device_provider

        self.root_dir = root_dir

        # filesystems created without a block device stores the result
        # filesystem file name here
        self.filename = None

        self.custom_args = {}
        self.post_init(custom_args)

    def post_init(self, custom_args):
        """
        Post initialization method

        Store dictionary of custom arguments if not empty. This
        overrides the default custom argument hash

        :param dict custom_args: custom arguments

            .. code:: python

                {
                    'create_options': ['option'],
                    'mount_options': ['option'],
                    'meta_data': {
                        'key': 'value'
                    }
                }
        """
        if custom_args:
            self.custom_args = copy.deepcopy(custom_args)

        if 'create_options' not in self.custom_args:
            self.custom_args['create_options'] = []

        if 'meta_data' not in self.custom_args:
            self.custom_args['meta_data'] = {}

        if 'mount_options' not in self.custom_args:
            self.custom_args['mount_options'] = []

    def create_on_device(self, label=None):
        """
        Create filesystem on block device

        Implement in specialized filesystem class for filesystems which
        requires a block device for creation, e.g ext4.

        :param string label: label name
        """
        raise NotImplementedError

    def create_on_file(self, filename, label=None, exclude=None):
        """
        Create filesystem from root data tree

        Implement in specialized filesystem class for filesystems which
        requires a data tree for creation, e.g squashfs.

        :param string filename: result file path name
        :param string label: label name
        :param list exclude: list of exclude dirs/files
        """
        raise NotImplementedError

    def sync_data(self, exclude=None):
        """
        Copy root data tree into filesystem

        :param list exclude: list of exclude dirs/files
        """
        if not self.root_dir:
            raise KiwiFileSystemSyncError('no root directory specified')
        if not os.path.exists(self.root_dir):
            raise KiwiFileSystemSyncError(
                'given root directory %s does not exist' % self.root_dir)
        self.filesystem_mount = MountManager(
            device=self.device_provider.get_device())
        self.filesystem_mount.mount(self.custom_args['mount_options'])
        data = DataSync(self.root_dir, self.filesystem_mount.mountpoint)
        data.sync_data(options=['-a', '-H', '-X', '-A', '--one-file-system'],
                       exclude=exclude)
        self.filesystem_mount.umount()

    def __del__(self):
        if self.filesystem_mount:
            log.info('Cleaning up %s instance', type(self).__name__)
            self.filesystem_mount.umount()
Esempio n. 26
0
class BootLoaderConfigBase:
    """
    **Base class for bootloader configuration**

    :param object xml_state: instance of :class:`XMLState`
    :param string root_dir: root directory path name
    :param dict custom_args: custom bootloader arguments dictionary
    """
    def __init__(self, xml_state, root_dir, boot_dir=None, custom_args=None):
        self.root_dir = root_dir
        self.boot_dir = boot_dir or root_dir
        self.xml_state = xml_state
        self.arch = Defaults.get_platform_name()

        self.volumes_mount = []
        self.root_mount = None
        self.boot_mount = None
        self.efi_mount = None
        self.device_mount = None
        self.proc_mount = None
        self.tmp_mount = None

        self.root_filesystem_is_overlay = xml_state.build_type.get_overlayroot(
        )
        self.post_init(custom_args)

    def post_init(self, custom_args):
        """
        Post initialization method

        Store custom arguments by default

        :param dict custom_args: custom bootloader arguments
        """
        self.custom_args = custom_args

    def write(self):
        """
        Write config data to config file.

        Implementation in specialized bootloader class required
        """
        raise NotImplementedError

    def write_meta_data(self, root_uuid=None, boot_options=''):
        """
        Write bootloader setup meta data files

        :param string root_uuid: root device UUID
        :param string boot_options: kernel options as string

        Implementation in specialized bootloader class optional
        """
        pass

    def setup_disk_image_config(self,
                                boot_uuid,
                                root_uuid,
                                hypervisor,
                                kernel,
                                initrd,
                                boot_options={}):
        """
        Create boot config file to boot from disk.

        :param string boot_uuid: boot device UUID
        :param string root_uuid: root device UUID
        :param string hypervisor: hypervisor name
        :param string kernel: kernel name
        :param string initrd: initrd name
        :param dict boot_options:
            custom options dictionary required to setup the bootloader.
            The scope of the options covers all information needed
            to setup and configure the bootloader and gets effective
            in the individual implementation. boot_options should
            not be mixed up with commandline options used at boot time.
            This information is provided from the get_*_cmdline
            methods. The contents of the dictionary can vary between
            bootloaders or even not be needed

        Implementation in specialized bootloader class required
        """
        raise NotImplementedError

    def setup_install_image_config(self, mbrid, hypervisor, kernel, initrd):
        """
        Create boot config file to boot from install media in EFI mode.

        :param string mbrid: mbrid file name on boot device
        :param string hypervisor: hypervisor name
        :param string kernel: kernel name
        :param string initrd: initrd name

        Implementation in specialized bootloader class required
        """
        raise NotImplementedError

    def setup_live_image_config(self, mbrid, hypervisor, kernel, initrd):
        """
        Create boot config file to boot live ISO image in EFI mode.

        :param string mbrid: mbrid file name on boot device
        :param string hypervisor: hypervisor name
        :param string kernel: kernel name
        :param string initrd: initrd name

        Implementation in specialized bootloader class required
        """
        raise NotImplementedError

    def setup_disk_boot_images(self, boot_uuid, lookup_path=None):
        """
        Create bootloader images for disk boot

        Some bootloaders requires to build a boot image the bootloader
        can load from a specific offset address or from a standardized
        path on a filesystem.

        :param string boot_uuid: boot device UUID
        :param string lookup_path: custom module lookup path

        Implementation in specialized bootloader class required
        """
        raise NotImplementedError

    def setup_install_boot_images(self, mbrid, lookup_path=None):
        """
        Create bootloader images for ISO boot an install media

        :param string mbrid: mbrid file name on boot device
        :param string lookup_path: custom module lookup path

        Implementation in specialized bootloader class required
        """
        raise NotImplementedError

    def setup_live_boot_images(self, mbrid, lookup_path=None):
        """
        Create bootloader images for ISO boot a live ISO image

        :param string mbrid: mbrid file name on boot device
        :param string lookup_path: custom module lookup path

        Implementation in specialized bootloader class required
        """
        raise NotImplementedError

    def setup_sysconfig_bootloader(self):
        """
        Create or update etc/sysconfig/bootloader by parameters
        required according to the bootloader setup

        Implementation in specialized bootloader class required
        """
        raise NotImplementedError

    def create_efi_path(self, in_sub_dir='boot/efi'):
        """
        Create standard EFI boot directory structure

        :param string in_sub_dir: toplevel directory

        :return: Full qualified EFI boot path

        :rtype: str
        """
        efi_boot_path = os.path.normpath(
            os.sep.join([self.boot_dir, in_sub_dir, 'EFI/BOOT']))
        Path.create(efi_boot_path)
        return efi_boot_path

    def get_boot_theme(self):
        """
        Bootloader Theme name

        :return: theme name

        :rtype: str
        """
        theme = None
        for preferences in self.xml_state.get_preferences_sections():
            section_content = preferences.get_bootloader_theme()
            if section_content:
                theme = section_content[0]
        return theme

    def get_boot_timeout_seconds(self):
        """
        Bootloader timeout in seconds

        If no timeout is specified the default timeout applies

        :return: timeout seconds

        :rtype: int
        """
        timeout_seconds = self.xml_state.get_build_type_bootloader_timeout()
        if timeout_seconds is None:
            timeout_seconds = Defaults.get_default_boot_timeout_seconds()
        return timeout_seconds

    def get_continue_on_timeout(self):
        """
        Check if the boot should continue after boot timeout or not

        :return: True or False

        :rtype: bool
        """
        continue_on_timeout = \
            self.xml_state.build_type.get_install_continue_on_timeout()
        if continue_on_timeout is None:
            continue_on_timeout = True
        return continue_on_timeout

    def failsafe_boot_entry_requested(self):
        """
        Check if a failsafe boot entry is requested

        :return: True or False

        :rtype: bool
        """
        if self.xml_state.build_type.get_installprovidefailsafe() is False:
            return False
        return True

    def get_boot_cmdline(self, uuid=None):
        """
        Boot commandline arguments passed to the kernel

        :param string uuid: boot device UUID

        :return: kernel boot arguments

        :rtype: str
        """
        cmdline = ''
        custom_cmdline = self.xml_state.build_type.get_kernelcmdline()
        if custom_cmdline:
            cmdline += ' ' + custom_cmdline
        custom_root = self._get_root_cmdline_parameter(uuid)
        if custom_root and custom_root not in cmdline:
            cmdline += ' ' + custom_root
        return cmdline.strip()

    def get_install_image_boot_default(self, loader=None):
        """
        Provide the default boot menu entry identifier for install images

        The install image can be configured to provide more than
        one boot menu entry. Menu entries configured are:

        * [0] Boot From Hard Disk
        * [1] Install
        * [2] Failsafe Install

        The installboot attribute controlls which of these are used
        by default. If not specified the boot from hard disk entry
        will be the default. Depending on the specified loader type
        either an entry number or name will be returned.

        :param string loader: bootloader name

        :return: menu name or id

        :rtype: str
        """
        menu_entry_title = self.get_menu_entry_title(plain=True)
        menu_type = namedtuple('menu_type', ['name', 'menu_id'])
        menu_list = [
            menu_type(name='Boot_from_Hard_Disk', menu_id='0'),
            menu_type(name='Install_' + menu_entry_title, menu_id='1'),
            menu_type(name='Failsafe_--_Install_' + menu_entry_title,
                      menu_id='2')
        ]
        boot_id = 0
        install_boot_name = self.xml_state.build_type.get_installboot()
        if install_boot_name == 'failsafe-install':
            boot_id = 2
        elif install_boot_name == 'install':
            boot_id = 1

        if not self.failsafe_boot_entry_requested() and boot_id == 2:
            log.warning(
                'Failsafe install requested but failsafe menu entry is disabled'
            )
            log.warning('Switching to standard install')
            boot_id = 1

        if loader and loader == 'isolinux':
            return menu_list[boot_id].name
        else:
            return menu_list[boot_id].menu_id

    def get_boot_path(self, target='disk'):
        """
        Bootloader lookup path on boot device

        If the bootloader reads the data it needs to boot, it does
        that from the configured boot device. Depending if that
        device is an extra boot partition or the root partition or
        or based on a non standard filesystem like a btrfs snapshot,
        the path name varies

        :param string target: target name: disk|iso

        :return: path name

        :rtype: str
        """
        if target != 'disk' and target != 'iso':
            raise KiwiBootLoaderTargetError('Invalid boot loader target %s' %
                                            target)
        bootpath = '/boot'
        need_boot_partition = False
        if target == 'disk':
            disk_setup = DiskSetup(self.xml_state, self.boot_dir)
            need_boot_partition = disk_setup.need_boot_partition()
            if need_boot_partition:
                # if an extra boot partition is used we will find the
                # data directly in the root of this partition and not
                # below the boot/ directory
                bootpath = '/'

        if target == 'disk':
            if not need_boot_partition:
                filesystem = self.xml_state.build_type.get_filesystem()
                volumes = self.xml_state.get_volumes()
                if filesystem == 'btrfs' and volumes:
                    # grub boot data paths must not be in a subvolume
                    # otherwise grub won't be able to find its config file
                    grub2_boot_data_paths = ['boot', 'boot/grub', 'boot/grub2']
                    for volume in volumes:
                        if volume.name in grub2_boot_data_paths:
                            raise KiwiBootLoaderTargetError(
                                '{0} must not be a subvolume'.format(
                                    volume.name))

        if target == 'iso':
            bootpath = '/boot/' + self.arch + '/loader'

        return bootpath

    def quote_title(self, name):
        """
        Quote special characters in the title name

        Not all characters can be displayed correctly in the bootloader
        environment. Therefore a quoting is required

        :param string name: title name

        :return: quoted text

        :rtype: str
        """
        name = name.replace(' ', '_')
        name = name.replace('[', '(')
        name = name.replace(']', ')')
        return name

    def get_menu_entry_title(self, plain=False):
        """
        Prefixed menu entry title

        If no displayname is specified in the image description,
        the menu title is constructed from the image name and
        build type

        :param bool plain: indicate to add built type into title text

        :return: title text

        :rtype: str
        """
        title = self.xml_state.xml_data.get_displayname()
        if not title:
            title = self.xml_state.xml_data.get_name()
        else:
            # if the title is set via the displayname attribute no custom
            # kiwi prefix or other style changes to that text should
            # be made
            plain = True
        type_name = self.xml_state.build_type.get_image()
        if plain:
            return title
        return title + ' [ ' + type_name.upper() + ' ]'

    def get_menu_entry_install_title(self):
        """
        Prefixed menu entry title for install images

        If no displayname is specified in the image description,
        the menu title is constructed from the image name

        :return: title text

        :rtype: str
        """
        title = self.xml_state.xml_data.get_displayname()
        if not title:
            title = self.xml_state.xml_data.get_name()
        return title

    def get_gfxmode(self, target):
        """
        Graphics mode according to bootloader target

        Bootloaders which support a graphics mode can be configured
        to run graphics in a specific resolution and colors. There
        is no standard for this setup which causes kiwi to create
        a mapping from the kernel vesa mode number to the corresponding
        bootloader graphics mode setup

        :param string target: bootloader name

        :return: boot graphics mode

        :rtype: str
        """
        gfxmode_map = Defaults.get_video_mode_map()

        default_mode = Defaults.get_default_video_mode()
        requested_gfxmode = self.xml_state.build_type.get_vga()

        if requested_gfxmode in gfxmode_map:
            gfxmode = requested_gfxmode
        else:
            gfxmode = default_mode

        if target == 'grub2':
            return gfxmode_map[gfxmode].grub2
        elif target == 'isolinux':
            return gfxmode_map[gfxmode].isolinux
        else:
            return gfxmode

    def _mount_system(self,
                      root_device,
                      boot_device,
                      efi_device=None,
                      volumes=None):
        self.root_mount = MountManager(device=root_device)
        self.boot_mount = MountManager(device=boot_device,
                                       mountpoint=self.root_mount.mountpoint +
                                       '/boot')
        if efi_device:
            self.efi_mount = MountManager(
                device=efi_device,
                mountpoint=self.root_mount.mountpoint + '/boot/efi')

        self.root_mount.mount()

        if not self.root_mount.device == self.boot_mount.device:
            self.boot_mount.mount()

        if efi_device:
            self.efi_mount.mount()

        if volumes:
            for volume_path in Path.sort_by_hierarchy(sorted(volumes.keys())):
                volume_mount = MountManager(
                    device=volumes[volume_path]['volume_device'],
                    mountpoint=self.root_mount.mountpoint + '/' + volume_path)
                self.volumes_mount.append(volume_mount)
                volume_mount.mount(
                    options=[volumes[volume_path]['volume_options']])

        if self.root_filesystem_is_overlay:
            # In case of an overlay root system all parts of the rootfs
            # are read-only by squashfs except for the extra boot partition.
            # However tools like grub's mkconfig creates temporary files
            # at call time and therefore /tmp needs to be writable during
            # the call time of the tools
            self.tmp_mount = MountManager(
                device='/tmp', mountpoint=self.root_mount.mountpoint + '/tmp')
            self.tmp_mount.bind_mount()

        self.device_mount = MountManager(
            device='/dev', mountpoint=self.root_mount.mountpoint + '/dev')
        self.proc_mount = MountManager(device='/proc',
                                       mountpoint=self.root_mount.mountpoint +
                                       '/proc')
        self.device_mount.bind_mount()
        self.proc_mount.bind_mount()

    def _get_root_cmdline_parameter(self, uuid):
        firmware = self.xml_state.build_type.get_firmware()
        initrd_system = self.xml_state.get_initrd_system()
        cmdline = self.xml_state.build_type.get_kernelcmdline()
        if cmdline and 'root=' in cmdline:
            log.info('Kernel root device explicitly set via kernelcmdline')
            root_search = re.search(r'(root=(.*)[ ]+|root=(.*)$)', cmdline)
            if root_search:
                return root_search.group(1)

        want_root_cmdline_parameter = False
        if firmware and 'ec2' in firmware:
            # EC2 requires to specifiy the root device in the bootloader
            # configuration. This is because the used pvgrub or hvmloader
            # reads this information and passes it to the guest configuration
            # which has an impact on the devices attached to the guest.
            want_root_cmdline_parameter = True

        if initrd_system == 'dracut':
            # When using a dracut initrd we have to specify the location
            # of the root device
            want_root_cmdline_parameter = True

        if want_root_cmdline_parameter:
            if uuid and self.xml_state.build_type.get_overlayroot():
                return 'root=overlay:UUID={0}'.format(uuid)
            elif uuid:
                return 'root=UUID={0} rw'.format(uuid)
            else:
                log.warning(
                    'root=UUID=<uuid> setup requested, but uuid is not provided'
                )

    def __del__(self):
        log.info('Cleaning up %s instance', type(self).__name__)
        for volume_mount in reversed(self.volumes_mount):
            volume_mount.umount()
        if self.device_mount:
            self.device_mount.umount()
        if self.proc_mount:
            self.proc_mount.umount()
        if self.efi_mount:
            self.efi_mount.umount()
        if self.tmp_mount:
            self.tmp_mount.umount()
        if self.boot_mount:
            self.boot_mount.umount()
        if self.root_mount:
            self.root_mount.umount()
Esempio n. 27
0
class VolumeManagerBtrfs(VolumeManagerBase):
    """
    Implements btrfs sub-volume management

    :param list subvol_mount_list: list of mounted btrfs subvolumes
    :param object toplevel_mount: :class:`MountManager` for root mountpoint
    """
    def post_init(self, custom_args):
        """
        Post initialization method

        Store custom btrfs initialization arguments

        :param list custom_args: custom btrfs volume manager arguments
        """
        if custom_args:
            self.custom_args = custom_args
        else:
            self.custom_args = {}
        if 'root_label' not in self.custom_args:
            self.custom_args['root_label'] = 'ROOT'
        if 'root_is_snapshot' not in self.custom_args:
            self.custom_args['root_is_snapshot'] = False
        if 'root_is_readonly_snapshot' not in self.custom_args:
            self.custom_args['root_is_readonly_snapshot'] = False

        self.subvol_mount_list = []
        self.toplevel_mount = None
        self.toplevel_volume = None

    def setup(self, name=None):
        """
        Setup btrfs volume management

        In case of btrfs a toplevel(@) subvolume is created and marked
        as default volume. If snapshots are activated via the custom_args
        the setup method also created the @/.snapshots/1/snapshot
        subvolumes. There is no concept of a volume manager name, thus
        the name argument is not used for btrfs

        :param string name: unused
        """
        self.setup_mountpoint()

        filesystem = FileSystem(name='btrfs',
                                device_provider=MappedDevice(
                                    device=self.device, device_provider=self),
                                custom_args=self.custom_filesystem_args)
        filesystem.create_on_device(label=self.custom_args['root_label'])
        self.toplevel_mount = MountManager(device=self.device,
                                           mountpoint=self.mountpoint)
        self.toplevel_mount.mount(self.custom_filesystem_args['mount_options'])
        root_volume = self.mountpoint + '/@'
        Command.run(['btrfs', 'subvolume', 'create', root_volume])
        if self.custom_args['root_is_snapshot']:
            snapshot_volume = self.mountpoint + '/@/.snapshots'
            Command.run(['btrfs', 'subvolume', 'create', snapshot_volume])
            Path.create(snapshot_volume + '/1')
            snapshot = self.mountpoint + '/@/.snapshots/1/snapshot'
            Command.run(
                ['btrfs', 'subvolume', 'snapshot', root_volume, snapshot])
            self._set_default_volume('@/.snapshots/1/snapshot')
        else:
            self._set_default_volume('@')

    def create_volumes(self, filesystem_name):
        """
        Create configured btrfs subvolumes

        Any btrfs subvolume is of the same btrfs filesystem. There is no
        way to have different filesystems per btrfs subvolume. Thus
        the filesystem_name has no effect for btrfs

        :param string filesystem_name: unused
        """
        log.info('Creating %s sub volumes', filesystem_name)
        self.create_volume_paths_in_root_dir()

        canonical_volume_list = self.get_canonical_volume_list()
        if canonical_volume_list.full_size_volume:
            # put an eventual fullsize volume to the volume list
            # because there is no extra handling required for it on btrfs
            canonical_volume_list.volumes.append(
                canonical_volume_list.full_size_volume)

        for volume in canonical_volume_list.volumes:
            if volume.name == 'LVRoot':
                # the btrfs root volume named '@' has been created as
                # part of the setup procedure
                pass
            else:
                log.info('--> sub volume %s', volume.realpath)
                toplevel = self.mountpoint + '/@/'
                volume_parent_path = os.path.normpath(
                    toplevel + os.path.dirname(volume.realpath))
                if not os.path.exists(volume_parent_path):
                    Path.create(volume_parent_path)
                Command.run([
                    'btrfs', 'subvolume', 'create',
                    os.path.normpath(toplevel + volume.realpath)
                ])
                self.apply_attributes_on_volume(toplevel, volume)
                if self.custom_args['root_is_snapshot']:
                    snapshot = self.mountpoint + '/@/.snapshots/1/snapshot/'
                    volume_mount = MountManager(
                        device=self.device,
                        mountpoint=os.path.normpath(snapshot +
                                                    volume.realpath))
                    self.subvol_mount_list.append(volume_mount)

    def get_fstab(self, persistency_type='by-label', filesystem_name=None):
        """
        Implements creation of the fstab entries. The method
        returns a list of fstab compatible entries

        :param string persistency_type: by-label | by-uuid
        :param string filesystem_name: unused

        :return: list of fstab entries

        :rtype: list
        """
        fstab_entries = []
        mount_options = \
            self.custom_filesystem_args['mount_options'] or ['defaults']
        block_operation = BlockID(self.device)
        blkid_type = 'LABEL' if persistency_type == 'by-label' else 'UUID'
        device_id = block_operation.get_blkid(blkid_type)
        if self.custom_args['root_is_snapshot']:
            mount_entry_options = mount_options + ['subvol=@/.snapshots']
            fstab_entry = ' '.join([
                blkid_type + '=' + device_id, '/.snapshots', 'btrfs',
                ','.join(mount_entry_options), '0 0'
            ])
            fstab_entries.append(fstab_entry)
        for volume_mount in self.subvol_mount_list:
            subvol_name = self._get_subvol_name_from_mountpoint(volume_mount)
            mount_entry_options = mount_options + ['subvol=' + subvol_name]
            fstab_entry = ' '.join([
                blkid_type + '=' + device_id,
                subvol_name.replace('@', ''), 'btrfs',
                ','.join(mount_entry_options), '0 0'
            ])
            fstab_entries.append(fstab_entry)
        return fstab_entries

    def get_volumes(self):
        """
        Return dict of volumes

        :return: volumes dictionary

        :rtype: dict
        """
        volumes = {}
        for volume_mount in self.subvol_mount_list:
            subvol_name = self._get_subvol_name_from_mountpoint(volume_mount)
            subvol_options = ','.join(
                ['subvol=' + subvol_name] +
                self.custom_filesystem_args['mount_options'])
            volumes[subvol_name.replace('@', '')] = {
                'volume_options': subvol_options,
                'volume_device': volume_mount.device
            }
        return volumes

    def mount_volumes(self):
        """
        Mount btrfs subvolumes
        """

        self.toplevel_mount.mount(self.custom_filesystem_args['mount_options'])

        for volume_mount in self.subvol_mount_list:
            if self.volumes_mounted_initially:
                volume_mount.mountpoint = os.path.normpath(
                    volume_mount.mountpoint.replace(self.toplevel_volume, '',
                                                    1))
            if not os.path.exists(volume_mount.mountpoint):
                Path.create(volume_mount.mountpoint)
            subvol_name = self._get_subvol_name_from_mountpoint(volume_mount)
            subvol_options = ','.join(
                ['subvol=' + subvol_name] +
                self.custom_filesystem_args['mount_options'])
            volume_mount.mount(options=[subvol_options])

        self.volumes_mounted_initially = True

    def umount_volumes(self):
        """
        Umount btrfs subvolumes

        :return: True if all subvolumes are successfully unmounted

        :rtype: bool
        """
        all_volumes_umounted = True
        for volume_mount in reversed(self.subvol_mount_list):
            if volume_mount.is_mounted():
                if not volume_mount.umount():
                    all_volumes_umounted = False

        if all_volumes_umounted:
            if self.toplevel_mount.is_mounted():
                if not self.toplevel_mount.umount():
                    all_volumes_umounted = False

        return all_volumes_umounted

    def sync_data(self, exclude=None):
        """
        Sync data into btrfs filesystem

        If snapshots are activated the root filesystem is synced
        into the first snapshot

        :param list exclude: files to exclude from sync
        """
        if self.toplevel_mount:
            sync_target = self.mountpoint + '/@'
            if self.custom_args['root_is_snapshot']:
                sync_target = self.mountpoint + '/@/.snapshots/1/snapshot'
                self._create_snapshot_info(''.join(
                    [self.mountpoint, '/@/.snapshots/1/info.xml']))
            data = DataSync(self.root_dir, sync_target)
            data.sync_data(
                options=['-a', '-H', '-X', '-A', '--one-file-system'],
                exclude=exclude)

    def set_property_readonly_root(self):
        """
        Sets the root volume to be a readonly filesystem
        """
        root_is_snapshot = \
            self.custom_args['root_is_snapshot']
        root_is_readonly_snapshot = \
            self.custom_args['root_is_readonly_snapshot']
        if root_is_snapshot and root_is_readonly_snapshot:
            sync_target = self.mountpoint
            Command.run(
                ['btrfs', 'property', 'set', sync_target, 'ro', 'true'])

    def _set_default_volume(self, default_volume):
        subvolume_list_call = Command.run(
            ['btrfs', 'subvolume', 'list', self.mountpoint])
        for subvolume in subvolume_list_call.output.split('\n'):
            id_search = re.search('ID (\d+) .*path (.*)', subvolume)
            if id_search:
                volume_id = id_search.group(1)
                volume_path = id_search.group(2)
                if volume_path == default_volume:
                    Command.run([
                        'btrfs', 'subvolume', 'set-default', volume_id,
                        self.mountpoint
                    ])
                    self.toplevel_volume = default_volume
                    return

        raise KiwiVolumeRootIDError('Failed to find btrfs volume: %s' %
                                    default_volume)

    def _xml_pretty(self, toplevel_element):
        xml_data_unformatted = ElementTree.tostring(toplevel_element, 'utf-8')
        xml_data_domtree = minidom.parseString(xml_data_unformatted)
        return xml_data_domtree.toprettyxml(indent="    ")

    def _create_snapshot_info(self, filename):
        date_info = datetime.datetime.now()
        snapshot = ElementTree.Element('snapshot')

        snapshot_type = ElementTree.SubElement(snapshot, 'type')
        snapshot_type.text = 'single'

        snapshot_number = ElementTree.SubElement(snapshot, 'num')
        snapshot_number.text = '1'

        snapshot_description = ElementTree.SubElement(snapshot, 'description')
        snapshot_description.text = 'first root filesystem'

        snapshot_date = ElementTree.SubElement(snapshot, 'date')
        snapshot_date.text = date_info.strftime("%Y-%m-%d %H:%M:%S")

        with open(filename, 'w') as snapshot_info_file:
            snapshot_info_file.write(self._xml_pretty(snapshot))

    def _get_subvol_name_from_mountpoint(self, volume_mount):
        subvol_name = '/'.join(volume_mount.mountpoint.split('/')[3:])
        if self.toplevel_volume and self.toplevel_volume in subvol_name:
            subvol_name = subvol_name.replace(self.toplevel_volume, '')
        return os.path.normpath(os.sep.join(['@', subvol_name]))

    def __del__(self):
        if self.toplevel_mount:
            log.info('Cleaning up %s instance', type(self).__name__)
            if self.umount_volumes():
                Path.wipe(self.mountpoint)
Esempio n. 28
0
class VolumeManagerBtrfs(VolumeManagerBase):
    """
    Implements btrfs sub-volume management

    :param list subvol_mount_list: list of mounted btrfs subvolumes
    :param object toplevel_mount: :class:`MountManager` for root mountpoint
    """
    def post_init(self, custom_args):
        """
        Post initialization method

        Store custom btrfs initialization arguments

        :param list custom_args: custom btrfs volume manager arguments
        """
        if custom_args:
            self.custom_args = custom_args
        else:
            self.custom_args = {}
        if 'root_label' not in self.custom_args:
            self.custom_args['root_label'] = 'ROOT'
        if 'root_is_snapshot' not in self.custom_args:
            self.custom_args['root_is_snapshot'] = False
        if 'root_is_readonly_snapshot' not in self.custom_args:
            self.custom_args['root_is_readonly_snapshot'] = False
        if 'quota_groups' not in self.custom_args:
            self.custom_args['quota_groups'] = False

        self.subvol_mount_list = []
        self.toplevel_mount = None
        self.toplevel_volume = None

    def setup(self, name=None):
        """
        Setup btrfs volume management

        In case of btrfs a toplevel(@) subvolume is created and marked
        as default volume. If snapshots are activated via the custom_args
        the setup method also created the @/.snapshots/1/snapshot
        subvolumes. There is no concept of a volume manager name, thus
        the name argument is not used for btrfs

        :param string name: unused
        """
        self.setup_mountpoint()

        filesystem = FileSystem.new(
            name='btrfs',
            device_provider=MappedDevice(
                device=self.device, device_provider=self.device_provider_root),
            custom_args=self.custom_filesystem_args)
        filesystem.create_on_device(label=self.custom_args['root_label'])
        self.toplevel_mount = MountManager(device=self.device,
                                           mountpoint=self.mountpoint)
        self.toplevel_mount.mount(self.custom_filesystem_args['mount_options'])
        if self.custom_args['quota_groups']:
            Command.run(['btrfs', 'quota', 'enable', self.mountpoint])
        root_volume = self.mountpoint + '/@'
        Command.run(['btrfs', 'subvolume', 'create', root_volume])
        if self.custom_args['root_is_snapshot']:
            snapshot_volume = self.mountpoint + '/@/.snapshots'
            Command.run(['btrfs', 'subvolume', 'create', snapshot_volume])
            volume_mount = MountManager(device=self.device,
                                        mountpoint=self.mountpoint +
                                        '/.snapshots')
            self.subvol_mount_list.append(volume_mount)
            Path.create(snapshot_volume + '/1')
            snapshot = self.mountpoint + '/@/.snapshots/1/snapshot'
            Command.run(
                ['btrfs', 'subvolume', 'snapshot', root_volume, snapshot])
            self._set_default_volume('@/.snapshots/1/snapshot')
        else:
            self._set_default_volume('@')

    def create_volumes(self, filesystem_name):
        """
        Create configured btrfs subvolumes

        Any btrfs subvolume is of the same btrfs filesystem. There is no
        way to have different filesystems per btrfs subvolume. Thus
        the filesystem_name has no effect for btrfs

        :param string filesystem_name: unused
        """
        log.info('Creating %s sub volumes', filesystem_name)
        self.create_volume_paths_in_root_dir()

        canonical_volume_list = self.get_canonical_volume_list()
        if canonical_volume_list.full_size_volume:
            # put an eventual fullsize volume to the volume list
            # because there is no extra handling required for it on btrfs
            canonical_volume_list.volumes.append(
                canonical_volume_list.full_size_volume)

        for volume in canonical_volume_list.volumes:
            if volume.is_root_volume:
                # the btrfs root volume named '@' has been created as
                # part of the setup procedure
                pass
            else:
                log.info('--> sub volume %s', volume.realpath)
                toplevel = self.mountpoint + '/@/'
                volume_parent_path = os.path.normpath(
                    toplevel + os.path.dirname(volume.realpath))
                if not os.path.exists(volume_parent_path):
                    Path.create(volume_parent_path)
                Command.run([
                    'btrfs', 'subvolume', 'create',
                    os.path.normpath(toplevel + volume.realpath)
                ])
                self.apply_attributes_on_volume(toplevel, volume)
                if self.custom_args['root_is_snapshot']:
                    snapshot = self.mountpoint + '/@/.snapshots/1/snapshot/'
                    volume_mount = MountManager(
                        device=self.device,
                        mountpoint=os.path.normpath(snapshot +
                                                    volume.realpath))
                    self.subvol_mount_list.append(volume_mount)

    def get_fstab(self, persistency_type='by-label', filesystem_name=None):
        """
        Implements creation of the fstab entries. The method
        returns a list of fstab compatible entries

        :param string persistency_type: by-label | by-uuid
        :param string filesystem_name: unused

        :return: list of fstab entries

        :rtype: list
        """
        fstab_entries = []
        mount_options = \
            self.custom_filesystem_args['mount_options'] or ['defaults']
        block_operation = BlockID(self.device)
        blkid_type = 'LABEL' if persistency_type == 'by-label' else 'UUID'
        device_id = block_operation.get_blkid(blkid_type)
        for volume_mount in self.subvol_mount_list:
            subvol_name = self._get_subvol_name_from_mountpoint(volume_mount)
            mount_entry_options = mount_options + ['subvol=' + subvol_name]
            fs_check = self._is_volume_enabled_for_fs_check(
                volume_mount.mountpoint)
            fstab_entry = ' '.join([
                blkid_type + '=' + device_id,
                subvol_name.replace('@', ''), 'btrfs',
                ','.join(mount_entry_options),
                '0 {fs_passno}'.format(fs_passno='2' if fs_check else '0')
            ])
            fstab_entries.append(fstab_entry)
        return fstab_entries

    def get_volumes(self):
        """
        Return dict of volumes

        :return: volumes dictionary

        :rtype: dict
        """
        volumes = {}
        for volume_mount in self.subvol_mount_list:
            subvol_name = self._get_subvol_name_from_mountpoint(volume_mount)
            subvol_options = ','.join(
                ['subvol=' + subvol_name] +
                self.custom_filesystem_args['mount_options'])
            volumes[subvol_name.replace('@', '')] = {
                'volume_options': subvol_options,
                'volume_device': volume_mount.device
            }
        return volumes

    def mount_volumes(self):
        """
        Mount btrfs subvolumes
        """
        self.toplevel_mount.mount(self.custom_filesystem_args['mount_options'])

        for volume_mount in self.subvol_mount_list:
            if self.volumes_mounted_initially:
                volume_mount.mountpoint = os.path.normpath(
                    volume_mount.mountpoint.replace(self.toplevel_volume, '',
                                                    1))
            if not os.path.exists(volume_mount.mountpoint):
                Path.create(volume_mount.mountpoint)
            subvol_name = self._get_subvol_name_from_mountpoint(volume_mount)
            subvol_options = ','.join(
                ['subvol=' + subvol_name] +
                self.custom_filesystem_args['mount_options'])
            volume_mount.mount(options=[subvol_options])

        self.volumes_mounted_initially = True

    def umount_volumes(self):
        """
        Umount btrfs subvolumes

        :return: True if all subvolumes are successfully unmounted

        :rtype: bool
        """
        all_volumes_umounted = True
        for volume_mount in reversed(self.subvol_mount_list):
            if volume_mount.is_mounted():
                if not volume_mount.umount():
                    all_volumes_umounted = False

        if all_volumes_umounted:
            if self.toplevel_mount.is_mounted():
                if not self.toplevel_mount.umount():
                    all_volumes_umounted = False

        return all_volumes_umounted

    def get_mountpoint(self) -> str:
        """
        Provides btrfs root mount point directory

        Effective use of the directory is guaranteed only after sync_data

        :return: directory path name

        :rtype: string
        """
        sync_target: List[str] = [self.mountpoint, '@']
        if self.custom_args.get('root_is_snapshot'):
            sync_target.extend(['.snapshots', '1', 'snapshot'])
        return os.path.join(*sync_target)

    def sync_data(self, exclude=None):
        """
        Sync data into btrfs filesystem

        If snapshots are activated the root filesystem is synced
        into the first snapshot

        :param list exclude: files to exclude from sync
        """
        if self.toplevel_mount:
            sync_target = self.get_mountpoint()
            if self.custom_args['root_is_snapshot']:
                self._create_snapshot_info(''.join(
                    [self.mountpoint, '/@/.snapshots/1/info.xml']))
            data = DataSync(self.root_dir, sync_target)
            data.sync_data(options=Defaults.get_sync_options(),
                           exclude=exclude)
            if self.custom_args['quota_groups'] and \
               self.custom_args['root_is_snapshot']:
                self._create_snapper_quota_configuration()

    def set_property_readonly_root(self):
        """
        Sets the root volume to be a readonly filesystem
        """
        root_is_snapshot = \
            self.custom_args['root_is_snapshot']
        root_is_readonly_snapshot = \
            self.custom_args['root_is_readonly_snapshot']
        if root_is_snapshot and root_is_readonly_snapshot:
            sync_target = self.mountpoint
            Command.run(
                ['btrfs', 'property', 'set', sync_target, 'ro', 'true'])

    def _is_volume_enabled_for_fs_check(self, mountpoint):
        for volume in self.volumes:
            if volume.realpath in mountpoint:
                if 'enable-for-filesystem-check' in volume.attributes:
                    return True
        return False

    def _set_default_volume(self, default_volume):
        subvolume_list_call = Command.run(
            ['btrfs', 'subvolume', 'list', self.mountpoint])
        for subvolume in subvolume_list_call.output.split('\n'):
            id_search = re.search('ID (\d+) .*path (.*)', subvolume)
            if id_search:
                volume_id = id_search.group(1)
                volume_path = id_search.group(2)
                if volume_path == default_volume:
                    Command.run([
                        'btrfs', 'subvolume', 'set-default', volume_id,
                        self.mountpoint
                    ])
                    self.toplevel_volume = default_volume
                    return

        raise KiwiVolumeRootIDError('Failed to find btrfs volume: %s' %
                                    default_volume)

    def _xml_pretty(self, toplevel_element):
        xml_data_unformatted = ElementTree.tostring(toplevel_element, 'utf-8')
        xml_data_domtree = minidom.parseString(xml_data_unformatted)
        return xml_data_domtree.toprettyxml(indent="    ")

    def _create_snapper_quota_configuration(self):
        root_path = os.sep.join([self.mountpoint, '@/.snapshots/1/snapshot'])
        snapper_default_conf = os.path.normpath(
            os.sep.join(
                [root_path,
                 Defaults.get_snapper_config_template_file()]))
        if os.path.exists(snapper_default_conf):
            # snapper requires an extra parent qgroup to operate with quotas
            Command.run(['btrfs', 'qgroup', 'create', '1/0', self.mountpoint])
            config_file = self._set_snapper_sysconfig_file(root_path)
            if not os.path.exists(config_file):
                shutil.copyfile(snapper_default_conf, config_file)
            Command.run([
                'chroot', root_path, 'snapper', '--no-dbus', 'set-config',
                'QGROUP=1/0'
            ])

    @staticmethod
    def _set_snapper_sysconfig_file(root_path):
        sysconf_file = SysConfig(
            os.sep.join([root_path, 'etc/sysconfig/snapper']))
        if not sysconf_file.get('SNAPPER_CONFIGS') or \
           len(sysconf_file['SNAPPER_CONFIGS'].strip('\"')) == 0:

            sysconf_file['SNAPPER_CONFIGS'] = '"root"'
            sysconf_file.write()
        elif len(sysconf_file['SNAPPER_CONFIGS'].split()) > 1:
            raise KiwiVolumeManagerSetupError(
                'Unsupported SNAPPER_CONFIGS value: {0}'.format(
                    sysconf_file['SNAPPER_CONFIGS']))
        return os.sep.join([
            root_path, 'etc/snapper/configs',
            sysconf_file['SNAPPER_CONFIGS'].strip('\"')
        ])

    def _create_snapshot_info(self, filename):
        date_info = datetime.datetime.now()
        snapshot = ElementTree.Element('snapshot')

        snapshot_type = ElementTree.SubElement(snapshot, 'type')
        snapshot_type.text = 'single'

        snapshot_number = ElementTree.SubElement(snapshot, 'num')
        snapshot_number.text = '1'

        snapshot_description = ElementTree.SubElement(snapshot, 'description')
        snapshot_description.text = 'first root filesystem'

        snapshot_date = ElementTree.SubElement(snapshot, 'date')
        snapshot_date.text = date_info.strftime("%Y-%m-%d %H:%M:%S")

        with open(filename, 'w') as snapshot_info_file:
            snapshot_info_file.write(self._xml_pretty(snapshot))

    def _get_subvol_name_from_mountpoint(self, volume_mount):
        path_start_index = len(defaults.TEMP_DIR.split(os.sep)) + 1
        subvol_name = os.sep.join(
            volume_mount.mountpoint.split(os.sep)[path_start_index:])
        if self.toplevel_volume and self.toplevel_volume in subvol_name:
            subvol_name = subvol_name.replace(self.toplevel_volume, '')
        return os.path.normpath(os.sep.join(['@', subvol_name]))

    def __del__(self):
        if self.toplevel_mount:
            log.info('Cleaning up %s instance', type(self).__name__)
            if not self.umount_volumes():
                log.warning('Subvolumes still busy')
Esempio n. 29
0
class TestMountManager(object):
    def setup(self):
        self.mount_manager = MountManager(
            '/dev/some-device', '/some/mountpoint'
        )

    @patch('kiwi.mount_manager.mkdtemp')
    def test_setup_empty_mountpoint(self, mock_mkdtemp):
        mock_mkdtemp.return_value = 'tmpdir'
        mount_manager = MountManager('/dev/some-device')
        assert mount_manager.mountpoint == 'tmpdir'

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_bind_mount(self, mock_mounted, mock_command):
        mock_mounted.return_value = False
        self.mount_manager.bind_mount()
        mock_command.assert_called_once_with(
            ['mount', '-n', '--bind', '/dev/some-device', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_mount(self, mock_mounted, mock_command):
        mock_mounted.return_value = False
        self.mount_manager.mount(['options'])
        mock_command.assert_called_once_with(
            ['mount', '-o', 'options', '/dev/some-device', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_umount_lazy(self, mock_mounted, mock_command):
        mock_mounted.return_value = True
        self.mount_manager.umount_lazy()
        mock_command.assert_called_once_with(
            ['umount', '-l', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    @patch('time.sleep')
    @patch('kiwi.logger.log.warning')
    def test_umount_with_errors(
        self, mock_warn, mock_sleep, mock_mounted, mock_command
    ):
        mock_command.side_effect = Exception
        mock_mounted.return_value = True
        assert self.mount_manager.umount() is False
        assert mock_command.call_args_list == [
            call(['umount', '/some/mountpoint']),
            call(['umount', '/some/mountpoint']),
            call(['umount', '/some/mountpoint'])
        ]
        assert mock_warn.called

    @patch('kiwi.mount_manager.Command.run')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_umount_success(self, mock_mounted, mock_command):
        mock_mounted.return_value = True
        assert self.mount_manager.umount() is True
        mock_command.assert_called_once_with(
            ['umount', '/some/mountpoint']
        )

    @patch('kiwi.mount_manager.Command.run')
    def test_is_mounted_true(self, mock_command):
        command = mock.Mock()
        command.returncode = 0
        mock_command.return_value = command
        assert self.mount_manager.is_mounted() is True

    @patch('kiwi.mount_manager.Command.run')
    def test_is_mounted_false(self, mock_command):
        command = mock.Mock()
        command.returncode = 1
        mock_command.return_value = command
        assert self.mount_manager.is_mounted() is False

    @patch('kiwi.mount_manager.Path.wipe')
    @patch('kiwi.mount_manager.MountManager.is_mounted')
    def test_destructor(self, mock_mounted, mock_wipe):
        self.mount_manager.mountpoint_created_by_mount_manager = True
        mock_mounted.return_value = False
        self.mount_manager.__del__()
        mock_wipe.assert_called_once_with('/some/mountpoint')