def _get_rpm_database_location(self): shared_mount = MountManager(device='/dev', mountpoint=self.root_dir + '/dev') if not shared_mount.is_mounted(): shared_mount.bind_mount() rpmdb = RpmDataBase(self.root_dir) if rpmdb.has_rpm(): dbpath = rpmdb.rpmdb_image.expand_query('%_dbpath') else: dbpath = rpmdb.rpmdb_host.expand_query('%_dbpath') if shared_mount.is_mounted(): shared_mount.umount_lazy() return dbpath
def sync_data(self, exclude=None): """ Implements sync of root directory to mounted volumes :param list exclude: file patterns to exclude """ if self.mountpoint: root_mount = MountManager(device=None, mountpoint=self.mountpoint) if not root_mount.is_mounted(): self.mount_volumes() data = DataSync(self.root_dir, self.mountpoint) data.sync_data(options=Defaults.get_sync_options(), exclude=exclude)
def sync_data(self, exclude=None): """ Implements sync of root directory to mounted volumes :param list exclude: file patterns to exclude """ if self.mountpoint: root_mount = MountManager(device=None, mountpoint=self.mountpoint) if not root_mount.is_mounted(): self.mount_volumes() data = DataSync(self.root_dir, self.mountpoint) data.sync_data( options=['-a', '-H', '-X', '-A', '--one-file-system'], exclude=exclude)
def sync_data(self, exclude=None): """ Implements sync of root directory to mounted volumes :param list exclude: file patterns to exclude """ if self.mountpoint: root_mount = MountManager(device=None, mountpoint=self.mountpoint) if not root_mount.is_mounted(): self.mount_volumes() data = DataSync(self.root_dir, self.mountpoint) data.sync_data( options=['-a', '-H', '-X', '-A', '--one-file-system'], exclude=exclude ) self.umount_volumes()
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')
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)
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')
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')
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)
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 )
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)