[virt-tools-list] [virt-bootstrap] [PATCH v4 17/26] Enable UID/GID mapping for qcow2
Radostin Stoyanov
rstoyanov1 at gmail.com
Thu Aug 3 13:13:15 UTC 2017
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
More information about the virt-tools-list
mailing list