On Thu, 2017-08-17 at 10:39 +0100, Radostin Stoyanov wrote: > Apply ownership mapping in qcow2 images using libguestfs python > bindings. To make this solution more general we introduce function > guestfs_walk() which will return the root file system tree of disk > image along with UID/GID values. > > These changes are applied in additional qcow2 disk image using the > last layer as backing file. For FileSource this is layer-1.qcow > with backing file layer-0.qcow2. > --- > src/virtBootstrap/sources/docker_source.py | 12 +++++ > src/virtBootstrap/sources/file_source.py | 7 +++ > src/virtBootstrap/utils.py | 74 ++++++++++++++++++++++++++++++ > src/virtBootstrap/virt_bootstrap.py | 2 + > tests/docker_source.py | 1 - > tests/file_source.py | 1 - > 6 files changed, 95 insertions(+), 2 deletions(-) > > diff --git a/src/virtBootstrap/sources/docker_source.py b/src/virtBootstrap/sources/docker_source.py > index 99247ab..ba70a44 100644 > --- a/src/virtBootstrap/sources/docker_source.py > +++ b/src/virtBootstrap/sources/docker_source.py > @@ -49,15 +49,22 @@ class DockerSource(object): > @param uri: Address of source registry > @param username: Username to access source registry > @param password: Password to access source registry > + @param uid_map: Mappings for UID of files in rootfs > + @param gid_map: Mappings for GID of files in rootfs > @param fmt: Format used to store image [dir, qcow2] > @param not_secure: Do not require HTTPS and certificate verification > @param no_cache: Whether to store downloaded images or not > @param progress: Instance of the progress module > + > + Note: uid_map and gid_map have the format: > + [[<start>, <target>, <count>], [<start>, <target>, <count>] ...] > """ > > self.url = self.gen_valid_uri(kwargs['uri']) > self.username = kwargs.get('username', None) > self.password = kwargs.get('password', None) > + self.uid_map = kwargs.get('uid_map', []) > + self.gid_map = kwargs.get('gid_map', []) > self.output_format = kwargs.get('fmt', utils.DEFAULT_OUTPUT_FORMAT) > self.insecure = kwargs.get('not_secure', False) > self.no_cache = kwargs.get('no_cache', False) > @@ -283,6 +290,11 @@ class DockerSource(object): > ) > img.create_base_layer() > img.create_backing_chains() > + if self.uid_map or self.gid_map: > + logger.info("Mapping UID/GID") > + utils.map_id_in_image( > + len(self.layers), dest, self.uid_map, self.gid_map > + ) > > else: > raise Exception("Unknown format:" + self.output_format) > diff --git a/src/virtBootstrap/sources/file_source.py b/src/virtBootstrap/sources/file_source.py > index 69f024c..b4b29ce 100644 > --- a/src/virtBootstrap/sources/file_source.py > +++ b/src/virtBootstrap/sources/file_source.py > @@ -41,10 +41,14 @@ class FileSource(object): > > @param uri: Path to tar archive file. > @param fmt: Format used to store image [dir, qcow2] > + @param uid_map: Mappings for UID of files in rootfs > + @param gid_map: Mappings for GID of files in rootfs > @param progress: Instance of the progress module > """ > self.path = kwargs['uri'].path > self.output_format = kwargs.get('fmt', utils.DEFAULT_OUTPUT_FORMAT) > + self.uid_map = kwargs.get('uid_map', []) > + self.gid_map = kwargs.get('gid_map', []) > self.progress = kwargs['progress'].update_progress > > def unpack(self, dest): > @@ -73,6 +77,9 @@ class FileSource(object): > progress=self.progress > ) > img.create_base_layer() > + if self.uid_map or self.gid_map: > + logger.info("Mapping UID/GID") > + utils.map_id_in_image(1, dest, self.uid_map, self.gid_map) > > else: > raise Exception("Unknown format:" + self.output_format) > diff --git a/src/virtBootstrap/utils.py b/src/virtBootstrap/utils.py > index 482e4aa..2e0bbf8 100644 > --- a/src/virtBootstrap/utils.py > +++ b/src/virtBootstrap/utils.py > @@ -64,6 +64,9 @@ class BuildImage(object): > @param tar_files: Tarballs to be converted to qcow2 images > @param dest: Directory where the qcow2 images will be created > @param progress: Instance of the progress module > + > + Note: uid_map and gid_map have the format: > + [[<start>, <target>, <count>], [<start>, <target>, <count>] ...] > """ > self.g = guestfs.GuestFS(python_return_dict=True) > self.layers = layers > @@ -529,6 +532,77 @@ def map_id(path, map_uid, map_gid): > os.lchown(file_path, new_uid, new_gid) > > > +def guestfs_walk(rootfs_tree, g, path='/'): > + """ > + File system walk for guestfs > + """ > + stat = g.lstat(path) > + rootfs_tree[path] = {'uid': stat['uid'], 'gid': stat['gid']} > + for member in g.ls(path): > + m_path = os.path.join(path, member) > + if g.is_dir(m_path): > + guestfs_walk(rootfs_tree, g, m_path) > + else: > + stat = g.lstat(m_path) > + rootfs_tree[m_path] = {'uid': stat['uid'], 'gid': stat['gid']} > + > + > +def apply_mapping_in_image(uid, gid, rootfs_tree, g): > + """ > + Apply mapping of new ownership > + """ > + if uid: > + uid_opts = get_mapping_opts(uid) > + if gid: > + gid_opts = get_mapping_opts(gid) > + > + for member in rootfs_tree: > + old_uid = rootfs_tree[member]['uid'] > + old_gid = rootfs_tree[member]['gid'] > + > + new_uid = get_map_id(old_uid, uid_opts) if uid else -1 > + new_gid = get_map_id(old_gid, gid_opts) if gid else -1 > + if new_uid != -1 or new_gid != -1: > + g.lchown(new_uid, new_gid, os.path.join('/', member)) > + > + > +def map_id_in_image(nlayers, dest, map_uid, map_gid): > + """ > + Create additional layer in which UID/GID mipping is applied. > + > + map_gid and map_uid have the format: > + [[<start>, <target>, <count>], [<start>, <target>, <count>], ...] > + """ > + > + g = guestfs.GuestFS(python_return_dict=True) > + last_layer = os.path.join(dest, "layer-%d.qcow2" % (nlayers - 1)) > + additional_layer = os.path.join(dest, "layer-%d.qcow2" % nlayers) > + # Add the last layer as readonly > + g.add_drive_opts(last_layer, format='qcow2', readonly=True) > + # Create the additional layer > + g.disk_create( > + filename=additional_layer, > + format='qcow2', > + size=-1, > + backingfile=last_layer, > + backingformat='qcow2' > + ) > + g.add_drive(additional_layer, format='qcow2') > + g.launch() > + g.mount('/dev/sda', '/') > + rootfs_tree = dict() > + guestfs_walk(rootfs_tree, g) > + g.umount('/') > + g.mount('/dev/sdb', '/') > + > + balance_uid_gid_maps(map_uid, map_gid) > + for uid, gid in zip(map_uid, map_gid): > + apply_mapping_in_image(uid, gid, rootfs_tree, g) > + > + g.umount('/') > + g.shutdown() > + > + > def balance_uid_gid_maps(uid_map, gid_map): > """ > Make sure the UID/GID list of mappings have the same lenght. > diff --git a/src/virtBootstrap/virt_bootstrap.py b/src/virtBootstrap/virt_bootstrap.py > index f970838..2d81998 100755 > --- a/src/virtBootstrap/virt_bootstrap.py > +++ b/src/virtBootstrap/virt_bootstrap.py > @@ -130,6 +130,8 @@ def bootstrap(uri, dest, > fmt=fmt, > username=username, > password=password, > + uid_map=uid_map, > + gid_map=gid_map, > not_secure=not_secure, > no_cache=no_cache, > progress=prog).unpack(dest) > diff --git a/tests/docker_source.py b/tests/docker_source.py > index ee294c1..93835de 100644 > --- a/tests/docker_source.py > +++ b/tests/docker_source.py > @@ -326,7 +326,6 @@ class Qcow2BuildImage(CreateLayers): > self.main() > > > -@unittest.skip("Need fix for this test to pass") > class Qcow2OwnershipMapping(Qcow2BuildImage): > """ > Ensures that UID/GID mapping works correctly for qcow2 conversion. > diff --git a/tests/file_source.py b/tests/file_source.py > index 2a0f964..a18b6f4 100644 > --- a/tests/file_source.py > +++ b/tests/file_source.py > @@ -72,7 +72,6 @@ class Qcow2BuildImage(BuildTarFiles): > self.check_qcow2_images(self.get_image_path()) > > > -@unittest.skip("Not implemented") > class Qcow2OwnershipMapping(Qcow2BuildImage): > """ > Ensures that UID/GID mapping works correctly for qcow2 conversion. I would add these tests only in this commit rather than skipping them before. ACK with the changes mentioned above -- Cedric _______________________________________________ virt-tools-list mailing list virt-tools-list@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/virt-tools-list