[virt-tools-list] [virt-bootstrap] [PATCH v7 16/26] Set root password with guestfs-python

Radostin Stoyanov rstoyanov1 at gmail.com
Sat Aug 26 20:42:05 UTC 2017


Use the python bindings of libguestfs to create additional qcow2 image
which has as backing file the last layer (layer-0.qcow2 for FileSource)
and insert hashed value of given root password in the /etc/shadow file.

Note: This additional qcow2 image is also used to apply UID/GID map.
---
 src/virtBootstrap/sources/docker_source.py |  9 +++-
 src/virtBootstrap/sources/file_source.py   | 11 +++-
 src/virtBootstrap/utils.py                 | 83 ++++++++++++++++++------------
 src/virtBootstrap/virt_bootstrap.py        | 14 ++---
 tests/docker_source.py                     | 18 +++++++
 tests/file_source.py                       |  9 ++++
 6 files changed, 103 insertions(+), 41 deletions(-)

diff --git a/src/virtBootstrap/sources/docker_source.py b/src/virtBootstrap/sources/docker_source.py
index a2fc8b9..3500bf1 100644
--- a/src/virtBootstrap/sources/docker_source.py
+++ b/src/virtBootstrap/sources/docker_source.py
@@ -51,6 +51,7 @@ class DockerSource(object):
         @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 root_password: Root password to set 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
@@ -65,6 +66,7 @@ class DockerSource(object):
         self.password = kwargs.get('password', None)
         self.uid_map = kwargs.get('uid_map', [])
         self.gid_map = kwargs.get('gid_map', [])
+        self.root_password = kwargs.get('root_password', 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)
@@ -287,10 +289,15 @@ class DockerSource(object):
                 )
                 img.create_base_layer()
                 img.create_backing_chains()
+                img.set_root_password(self.root_password)
                 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
+                        len(self.layers),  # Number of layers
+                        dest,
+                        self.uid_map,
+                        self.gid_map,
+                        (self.root_password is None)  # Create new disk?
                     )
 
             else:
diff --git a/src/virtBootstrap/sources/file_source.py b/src/virtBootstrap/sources/file_source.py
index b4b29ce..63dd4d2 100644
--- a/src/virtBootstrap/sources/file_source.py
+++ b/src/virtBootstrap/sources/file_source.py
@@ -43,12 +43,14 @@ class FileSource(object):
         @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 root_password: Root password to set 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.root_password = kwargs.get('root_password', None)
         self.progress = kwargs['progress'].update_progress
 
     def unpack(self, dest):
@@ -77,9 +79,16 @@ class FileSource(object):
                 progress=self.progress
             )
             img.create_base_layer()
+            img.set_root_password(self.root_password)
             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)
+                utils.map_id_in_image(
+                    1,  # Number of layers
+                    dest,
+                    self.uid_map,
+                    self.gid_map,
+                    (self.root_password is None)  # Create new disk?
+                )
 
         else:
             raise Exception("Unknown format:" + self.output_format)
diff --git a/src/virtBootstrap/utils.py b/src/virtBootstrap/utils.py
index a1b648e..d5da5c1 100644
--- a/src/virtBootstrap/utils.py
+++ b/src/virtBootstrap/utils.py
@@ -31,7 +31,6 @@ import subprocess
 import sys
 import tempfile
 import logging
-import re
 
 import guestfs
 import passlib.hosts
@@ -138,6 +137,46 @@ class BuildImage(object):
                       xattrs=True, selinux=True, acls=True)
         self.g.umount('/')
 
+    def set_root_password(self, root_password):
+        """
+        Set root password within new layer
+        """
+        if not root_password:
+            return
+
+        self.progress("Setting root password", logger=logger)
+        img_file = os.path.join(self.dest, 'layer-%s.qcow2' % self.nlayers)
+        self.g.disk_create(
+            filename=img_file,
+            format='qcow2',
+            size=-1,
+            backingfile=self.qcow2_files[-1],
+            backingformat='qcow2'
+        )
+        self.g.add_drive(img_file, format='qcow2')
+        self.g.launch()
+        self.g.mount('/dev/sda', '/')
+        success = False
+        if self.g.is_file('/etc/shadow'):
+            shadow_content = self.g.read_file('/etc/shadow').decode('utf-8')
+            shadow_content = shadow_content.split('\n')
+            if shadow_content:
+                # Note: 'shadow_content' is a list, pass-by-reference is used
+                set_password_in_shadow_content(shadow_content, root_password)
+                self.g.write('/etc/shadow', '\n'.join(shadow_content))
+                success = True
+            else:
+                logger.error('shadow file is empty')
+        else:
+            logger.error('shadow file was not found')
+
+        self.g.umount('/')
+        self.g.shutdown()
+
+        if not success:
+            self.progress("Removing root password layer", logger=logger)
+            os.remove(img_file)
+
 
 def get_compression_type(tar_file):
     """
@@ -432,29 +471,6 @@ def set_root_password_in_rootfs(rootfs, password):
         os.chmod(shadow_file, shadow_file_permissions)
 
 
-def set_root_password_in_image(image, password):
-    """
-    Set password on the root user within image
-    """
-    password_hash = passlib.hosts.linux_context.hash(password)
-    execute(['virt-edit',
-             '-a', image, '/etc/shadow',
-             '-e', 's,^root:.*?:,root:%s:,' % re.escape(password_hash)])
-
-
-def set_root_password(fmt, dest, root_password):
-    """
-    Set root password
-    """
-    if fmt == "dir":
-        set_root_password_in_rootfs(dest, root_password)
-    elif fmt == "qcow2":
-        layers = [layer for layer in os.listdir(dest)
-                  if layer.startswith('layer-')]
-        set_root_password_in_image(os.path.join(dest, max(layers)),
-                                   root_password)
-
-
 def write_progress(prog):
     """
     Write progress output to console
@@ -569,7 +585,7 @@ def apply_mapping_in_image(uid, gid, rootfs_tree, g):
             g.lchown(new_uid, new_gid, os.path.join('/', member))
 
 
-def map_id_in_image(nlayers, dest, map_uid, map_gid):
+def map_id_in_image(nlayers, dest, map_uid, map_gid, new_disk=True):
     """
     Create additional layer in which UID/GID mipping is applied.
 
@@ -582,14 +598,15 @@ def map_id_in_image(nlayers, dest, map_uid, map_gid):
     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'
-    )
+    if new_disk:
+        # 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', '/')
diff --git a/src/virtBootstrap/virt_bootstrap.py b/src/virtBootstrap/virt_bootstrap.py
index f0abac4..e387842 100755
--- a/src/virtBootstrap/virt_bootstrap.py
+++ b/src/virtBootstrap/virt_bootstrap.py
@@ -132,17 +132,19 @@ def bootstrap(uri, dest,
            password=password,
            uid_map=uid_map,
            gid_map=gid_map,
+           root_password=root_password,
            not_secure=not_secure,
            no_cache=no_cache,
            progress=prog).unpack(dest)
 
-    if root_password is not None:
-        logger.info("Setting password of the root account")
-        utils.set_root_password(fmt, dest, root_password)
+    if fmt == "dir":
+        if root_password is not None:
+            logger.info("Setting password of the root account")
+            utils.set_root_password_in_rootfs(dest, root_password)
 
-    if fmt == "dir" and (uid_map or gid_map):
-        logger.info("Mapping UID/GID")
-        utils.mapping_uid_gid(dest, uid_map, gid_map)
+        if uid_map or gid_map:
+            logger.info("Mapping UID/GID")
+            utils.mapping_uid_gid(dest, uid_map, gid_map)
 
 
 def set_logging_conf(loglevel=None):
diff --git a/tests/docker_source.py b/tests/docker_source.py
index eeea379..9090988 100644
--- a/tests/docker_source.py
+++ b/tests/docker_source.py
@@ -286,6 +286,24 @@ class TestQcow2DockerSource(Qcow2ImageAccessor):
             g.umount('/')
         g.shutdown()
 
+    def test_qcow2_setting_root_password(self):
+        """
+        Ensures that the root password is set in the last qcow2 image.
+        """
+        self.root_password = "My secret password"
+        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()
+        g.mount('/dev/sda', '/')
+        self.validate_shadow_file_in_image(g)
+        g.umount('/')
+        g.shutdown()
+
 
 class TestDockerSource(unittest.TestCase):
     """
diff --git a/tests/file_source.py b/tests/file_source.py
index 9ffd4e3..4554ecd 100644
--- a/tests/file_source.py
+++ b/tests/file_source.py
@@ -115,3 +115,12 @@ class TestQcow2FileSource(Qcow2ImageAccessor):
         self.call_bootstrap()
         self.apply_mapping()
         self.check_qcow2_images(self.get_image_path(1))
+
+    def test_qcow2_setting_root_password(self):
+        """
+        Ensures that the root password is set in the last qcow2 image.
+        """
+        self.root_password = "My secret password"
+        self.call_bootstrap()
+        self.check_image = self.validate_shadow_file_in_image
+        self.check_qcow2_images(self.get_image_path(1))
-- 
2.13.5




More information about the virt-tools-list mailing list