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

Cedric Bosdonnat cbosdonnat at suse.com
Tue Aug 29 07:42:37 UTC 2017


On Sat, 2017-08-26 at 21:42 +0100, Radostin Stoyanov wrote:
> 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))

ACK
--
Cedric




More information about the virt-tools-list mailing list