Extend and update unit tests. --- src/virtBootstrap/sources/docker_source.py | 11 +++- src/virtBootstrap/sources/file_source.py | 8 ++- src/virtBootstrap/utils.py | 37 +++++++++++ src/virtBootstrap/virt_bootstrap.py | 2 + tests/test_build_qcow2_image.py | 102 ++++++++++++++++++++++++++++- 5 files changed, 156 insertions(+), 4 deletions(-) diff --git a/src/virtBootstrap/sources/docker_source.py b/src/virtBootstrap/sources/docker_source.py index 9d7c187..45e6c1d 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', None) + self.gid_map = kwargs.get('gid_map', None) 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) @@ -270,7 +277,9 @@ class DockerSource(object): utils.Build_QCOW2_Image( tar_files=self.tar_files, dest=dest, - progress=self.progress + progress=self.progress, + uid_map=self.uid_map, + gid_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 760e50a..70ce8b8 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', None) + self.gid_map = kwargs.get('gid_map', None) self.progress = kwargs['progress'].update_progress def unpack(self, dest): @@ -68,7 +72,9 @@ class FileSource(object): utils.Build_QCOW2_Image( tar_files=[self.path], dest=dest, - progress=self.progress + progress=self.progress, + uid_map=self.uid_map, + gid_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 24209f1..99a66e2 100644 --- a/src/virtBootstrap/utils.py +++ b/src/virtBootstrap/utils.py @@ -29,6 +29,7 @@ import json import os import subprocess import sys +import tarfile import tempfile import logging import re @@ -63,8 +64,13 @@ class Build_QCOW2_Image(object): Initialize guestfs @param tar_files: List of paths to tar files from which to the rootfs + @param uid_map: Mappings for UID of files in rootfs + @param gid_map: Mappings for GID of files in rootfs @param dest: Destination directory where qcow2 images will be stored @param progress: Instance of the progress module + + Note: uid_map and gid_map have the format: + [[<start>, <target>, <count>], [<start>, <target>, <count>] ...] """ self.tar_files = kwargs['tar_files'] if not isinstance(self.tar_files, list): @@ -72,6 +78,8 @@ class Build_QCOW2_Image(object): 'tar_files must be list not %s' % type(self.tar_files) ) self.progress = kwargs['progress'] + self.uid_map = kwargs.get('uid_map', None) + self.gid_map = kwargs.get('gid_map', None) self.fmt = 'qcow2' self.qcow2_files = [os.path.join(kwargs['dest'], 'layer-%s.qcow2' % i) for i in range(len(self.tar_files))] @@ -106,6 +114,14 @@ class Build_QCOW2_Image(object): # from tar file. self.g.tar_in(tar_file, '/', get_compression_type(tar_file), xattrs=True, selinux=True, acls=True) + + # UID/GID Mapping + if self.uid_map or self.gid_map: + tar_members = tarfile.open(tar_file).getmembers() + balance_uid_gid_maps(self.uid_map, self.gid_map) + for uid, gid in zip(self.uid_map, self.gid_map): + self.map_id(tar_members, uid, gid) + # Shutdown guestfs instance to avoid hot-plugging of devices. self.g.umount('/') @@ -146,6 +162,27 @@ class Build_QCOW2_Image(object): logger=logger) self.tar_in(tar_file, devices[i]) + def map_id(self, tar_members, map_uid, map_gid): + """ + Remapping ownership of all files inside image. + + map_gid and map_uid: Contain integers in a list with format: + [<start>, <target>, <count>] + """ + if map_uid: + uid_opts = get_mapping_opts(map_uid) + if map_gid: + gid_opts = get_mapping_opts(map_gid) + + for member in tar_members: + old_uid = member.uid + old_gid = member.gid + + new_uid = get_map_id(old_uid, uid_opts) if map_uid else -1 + new_gid = get_map_id(old_gid, gid_opts) if map_gid else -1 + if new_uid != -1 or new_gid != -1: + self.g.lchown(new_uid, new_gid, os.path.join('/', member.name)) + def get_compression_type(tar_file): """ diff --git a/src/virtBootstrap/virt_bootstrap.py b/src/virtBootstrap/virt_bootstrap.py index 0bc2e2b..99aca24 100755 --- a/src/virtBootstrap/virt_bootstrap.py +++ b/src/virtBootstrap/virt_bootstrap.py @@ -124,6 +124,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/test_build_qcow2_image.py b/tests/test_build_qcow2_image.py index 09323c6..74d7883 100644 --- a/tests/test_build_qcow2_image.py +++ b/tests/test_build_qcow2_image.py @@ -43,7 +43,10 @@ class TestBuild_QCOW2_Image(unittest.TestCase): kwargs = { 'tar_files': ['foo', 'bar'], 'progress': mock.Mock(), - 'dest': 'dest' + 'dest': 'dest', + 'uid_map': [[0, 1000, 10], [500, 500, 10]], + 'gid_map': [[0, 1000, 10], [500, 500, 10]], + } m_guestfs = mock.Mock() @@ -66,6 +69,9 @@ class TestBuild_QCOW2_Image(unittest.TestCase): ) self.assertIs(src_instance.tar_files, kwargs['tar_files']) + self.assertIs(src_instance.uid_map, kwargs['uid_map']) + self.assertIs(src_instance.gid_map, kwargs['gid_map']) + self.assertIs(src_instance.progress, kwargs['progress']) self.assertIs(src_instance.g, m_guestfs) @@ -135,12 +141,14 @@ class TestBuild_QCOW2_Image(unittest.TestCase): def test_tar_in(self): """ Ensures that tar_in() calls mount(), tar_in() and unmount() - in this order and with correct parameters. + in this order and with correct parameters when UID/GID mapping + is not used. """ tar_file = 'foo.tar' dev = '/dev/sda' m_self = mock.Mock(spec=utils.Build_QCOW2_Image) + m_self.uid_map = m_self.gid_map = None m_self.g = mock.Mock() with mock.patch( @@ -164,6 +172,53 @@ class TestBuild_QCOW2_Image(unittest.TestCase): ] ) + def test_tar_in_calls_map_id(self): + """ + Ensures that tar_in() calls map_id() when UID/GID mapping is used. + """ + tar_file = 'foo.tar' + dev = '/dev/sda' + + m_self = mock.Mock(spec=utils.Build_QCOW2_Image) + m_self.uid_map = [[0, 1000, 10], [500, 500, 10]] + m_self.gid_map = [[0, 1000, 10], None] + m_self.g = mock.Mock() + + with mock.patch.multiple(utils, + tarfile=mock.DEFAULT, + balance_uid_gid_maps=mock.DEFAULT, + get_compression_type=mock.DEFAULT) as mocked: + utils.Build_QCOW2_Image.tar_in(m_self, tar_file, dev) + + # Check if getmembers() from tarfile module was called + mocked['tarfile'].open(tar_file).getmembers.assert_called_once() + + expected_calls = [ + mock.call.g.mount(dev, '/'), + mock.call.g.tar_in( + tar_file, + '/', + mocked['get_compression_type'](tar_file), + xattrs=True, + selinux=True, + acls=True + ) + ] + + # Append map_id calls + for uid, gid in zip(m_self.uid_map, m_self.gid_map): + expected_calls.append( + mock.call.map_id( + mocked['tarfile'].open(tar_file).getmembers(), + uid, + gid + ) + ) + + expected_calls.append(mock.call.g.umount('/')) + + self.assertEqual(m_self.method_calls, expected_calls) + ################################### # Tests for: create_base_qcow2_layer() ################################### @@ -267,3 +322,46 @@ class TestBuild_QCOW2_Image(unittest.TestCase): )) self.assertEqual(m_self.method_calls, expected_calls) + + ################################### + # Tests for: map_id() + ################################### + def test_map_id(self): + """ + Ensures that map_id() calls g.lchown() for all tar members with + the result from get_map_id() or -1 (when the map is None). + """ + members = 5 # Number of tar members + map_uid = [0, 1000, 10] + map_gid = None + + # Create dummy tar members + tar_members = [] + for i in range(members): + member = mock.Mock() + member.name = 'file%d' % i + member.uid = 0 + member.gid = 0 + tar_members.append(member) + + m_self = mock.Mock(spec=utils.Build_QCOW2_Image) + m_self.g = mock.Mock() + + with mock.patch.multiple(utils, + get_map_id=mock.DEFAULT, + get_mapping_opts=mock.DEFAULT) as mocked: + utils.Build_QCOW2_Image.map_id( + m_self, tar_members, map_uid, map_gid + ) + + expected_calls = [] + for member in tar_members: + expected_calls.append( + mock.call( + mocked['get_map_id'](), # new_uid + -1, # new_gid + '/%s' % member.name + ) + ) + + self.assertEqual(m_self.g.lchown.mock_calls, expected_calls) -- 2.13.3 _______________________________________________ virt-tools-list mailing list virt-tools-list@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/virt-tools-list