]> git.siccegge.de Git - forks/vmdebootstrap.git/commitdiff
Add an image extraction helper
authorNeil Williams <codehelp@debian.org>
Sat, 2 May 2015 17:57:16 +0000 (18:57 +0100)
committerNeil Williams <codehelp@debian.org>
Sat, 2 May 2015 17:57:16 +0000 (18:57 +0100)
vmextract can use python-guestfs to extract files and
directories from an image without needing root privileges.

README
vmextract.py [new file with mode: 0755]

diff --git a/README b/README
index 404db87512246c99dc8dbd2513d9a1fffb485e0a..ef9aa8b5d28529eedfa4cd84a4ec67580e35fbdd 100644 (file)
--- a/README
+++ b/README
@@ -59,6 +59,25 @@ In order to use vmdebootstrap, you'll need a few things:
 * kpartx
 * python-cliapp (see http://liw.fi/cliapp/)
 
+The vmextract helper
+--------------------
+
+Once the image is built, various files can be generated or modified
+during the install operations and some of these files can be useful
+when testing the image. One example is the initrd built by the process
+of installing a Debian kernel. Rather than having to mount the image
+and copy the files manually, the vmextract helper can do it for you,
+without needing root privileges.
+
+$ /usr/share/vmdebootstrap/vmextract.py --verbose \
+  --image bbb/bbb-debian-armmp.img --boot \
+  --path /boot/initrd.img-3.14-2-armmp \
+  --path /lib/arm-linux-gnueabihf/libresolv.so.2
+
+This uses python-guestfs (a Recommended package for vmdebootstrap) to
+prepare a read-only version of the image - in this case with the /boot
+partition also mounted - and copies files out into the current working
+directory.
 
 Legalese
 --------
diff --git a/vmextract.py b/vmextract.py
new file mode 100755 (executable)
index 0000000..f91908b
--- /dev/null
@@ -0,0 +1,162 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+#  Copyright 2015 Neil Williams <codehelp@debian.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import cliapp
+import guestfs
+import tarfile
+import logging
+
+__version__ = '0.1'
+__desc__ = "Helper to mount an image read-only and extract files" + \
+           " or directories."
+
+
+# pylint: disable=missing-docstring
+
+
+class VmExtract(cliapp.Application):  # pylint: disable=too-many-public-methods
+    """
+    Support for extracting useful content from VM images.
+    For example, to assist in validation.
+    """
+
+    def __init__(
+            self, progname=None,
+            version=__version__, description=__desc__, epilog=None):
+        super(VmExtract, self).__init__(
+            progname, version,
+            description, epilog)
+        self.guest_os = None
+        self.mps = []
+
+    def add_settings(self):
+        self.settings.boolean(
+            ['verbose'], 'report what is going on')
+        self.settings.string(
+            ['image'], 'image to read', metavar='FILE')
+        self.settings.string(
+            ['directory'], 'directory to extract as a tarball.')
+        self.settings.string_list(
+            ['path'], 'path to the filename to extract - can repeat.')
+        self.settings.boolean(
+            ['boot'], 'mount the boot partition as well as root')
+        self.settings.string(
+            ['filename'],
+            'name of tarball containing the extracted directory',
+            default='vmextract.tgz',
+            metavar='FILE')
+
+    # pylint: disable=too-many-branches,too-many-statements
+    def process_args(self, args):
+
+        if not self.settings['image']:
+            raise cliapp.AppException(
+                'You must give an image to read')
+        if not self.settings['directory'] and not self.settings['path']:
+            raise cliapp.AppException(
+                'You must provide either a filename or directory '
+                'to extract')
+
+        try:
+            self.prepare()
+            self.mount_root()
+            if self.settings['boot']:
+                self.mount_boot()
+            if self.settings['directory']:
+                self.extract_directory()
+            elif self.settings['path']:
+                for path in self.settings['path']:
+                    self.download(path)
+        except BaseException as exc:
+            self.message('EEEK! Something bad happened... %s' % exc)
+            sys.exit(1)
+
+    def message(self, msg):
+        logging.info(msg)
+        if self.settings['verbose']:
+            print msg
+
+    def prepare(self):
+        """
+        Initialise guestfs
+        """
+        self.message("Preparing %s" % self.settings['image'])
+        self.guest_os = guestfs.GuestFS(python_return_dict=True)
+        self.guest_os.add_drive_opts(
+            self.settings['image'],
+            format="raw",
+            readonly=1)
+        # ensure launch is only called once per run
+        self.guest_os.launch()
+        drives = self.guest_os.inspect_os()
+        self.mps = self.guest_os.inspect_get_mountpoints(drives[0])
+
+    def download(self, path):
+        """
+        Copy a single file out of the image
+        If a filename is not specified, use the basename of the original.
+        """
+        filename = os.path.basename(path)
+        self.message(
+            "Extracting %s as %s" % (path, filename))
+        self.guest_os.download(path, filename)
+        if not os.path.exists(filename):
+            return RuntimeError("Download failed")
+
+    def mount_root(self):
+        """
+        Mounts the root partition to /
+        """
+        root = [part for part in self.mps if part == '/'][0]
+        if not root:
+            raise RuntimeError("Unable to identify root partition")
+        self.guest_os.mount_ro(self.mps[root], '/')
+
+    def mount_boot(self):
+        """
+        Mounts the /boot partition to a new /boot mountpoint
+        """
+        boot = [part for part in self.mps if part == '/boot'][0]
+        if not boot:
+            raise RuntimeError("Unable to identify boot partition")
+        if not self.guest_os.is_dir('/boot'):
+            self.guest_os.mkmountpoint('/boot')
+        self.guest_os.mount_ro(self.mps[boot], '/boot')
+
+    def extract_directory(self):
+        """
+        Create a tarball of a complete directory existing in the image.
+        """
+        if not self.settings['filename']:
+            self.settings['filename'] = 'vmextract.tgz'
+        self.guest_os.tar_out(
+            self.settings['directory'],
+            self.settings['filename'], compress='gzip')
+        if not tarfile.is_tarfile(self.settings['filename']):
+            raise RuntimeError("Extraction failed")
+
+
+def main():
+    VmExtract(version=__version__).run()
+    return 0
+
+
+if __name__ == '__main__':
+    main()