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.qcow2 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 | 4 +- tests/docker_source.py | 23 ++++++++++ tests/file_source.py | 10 ++++ 6 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/virtBootstrap/sources/docker_source.py b/src/virtBootstrap/sources/docker_source.py index a6ea3e6..a2fc8b9 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) @@ -280,6 +287,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 5fb5d8c..a1b648e 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 @@ -532,6 +535,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 length. diff --git a/src/virtBootstrap/virt_bootstrap.py b/src/virtBootstrap/virt_bootstrap.py index f970838..f0abac4 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) @@ -138,7 +140,7 @@ def bootstrap(uri, dest, logger.info("Setting password of the root account") utils.set_root_password(fmt, dest, root_password) - if fmt == "dir" and uid_map or gid_map: + if fmt == "dir" and (uid_map or gid_map): logger.info("Mapping UID/GID") utils.mapping_uid_gid(dest, uid_map, gid_map) diff --git a/tests/docker_source.py b/tests/docker_source.py index 9dc25d9..eeea379 100644 --- a/tests/docker_source.py +++ b/tests/docker_source.py @@ -263,6 +263,29 @@ class TestQcow2DockerSource(Qcow2ImageAccessor): g.umount('/') g.shutdown() + def test_qcow2_ownership_mapping(self): + """ + Ensures that UID/GID mapping works correctly for qcow2 conversion. + """ + self.uid_map = [[1000, 2000, 10], [0, 1000, 10], [500, 500, 10]] + self.gid_map = [[1000, 2000, 10], [0, 1000, 10], [500, 500, 10]] + layers_rootfs = self.call_bootstrap() + + g = guestfs.GuestFS(python_return_dict=True) + g.add_drive_opts( + self.get_image_path(len(layers_rootfs)), + readonly=True + ) + + g.launch() + for rootfs in layers_rootfs[::-1]: + self.rootfs_tree = rootfs + self.apply_mapping() + g.mount('/dev/sda', '/') + self.check_image(g) + g.umount('/') + g.shutdown() + class TestDockerSource(unittest.TestCase): """ diff --git a/tests/file_source.py b/tests/file_source.py index 391ca48..9ffd4e3 100644 --- a/tests/file_source.py +++ b/tests/file_source.py @@ -105,3 +105,13 @@ class TestQcow2FileSource(Qcow2ImageAccessor): """ self.call_bootstrap() self.check_qcow2_images(self.get_image_path()) + + def test_qcow2_ownership_mapping(self): + """ + Ensures that UID/GID mapping works correctly for qcow2 conversion. + """ + self.uid_map = [[1000, 2000, 10], [0, 1000, 10], [500, 500, 10]] + self.gid_map = [[1000, 2000, 10], [0, 1000, 10], [500, 500, 10]] + self.call_bootstrap() + self.apply_mapping() + self.check_qcow2_images(self.get_image_path(1)) -- 2.13.5 _______________________________________________ virt-tools-list mailing list virt-tools-list@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/virt-tools-list