From db25f4d3190d62798884ad80012a7b3d88fbd504 Mon Sep 17 00:00:00 2001 From: Neil Williams Date: Sat, 2 May 2015 18:57:16 +0100 Subject: [PATCH] Add an image extraction helper vmextract can use python-guestfs to extract files and directories from an image without needing root privileges. --- README | 19 ++++++ vmextract.py | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100755 vmextract.py diff --git a/README b/README index 404db87..ef9aa8b 100644 --- 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 index 0000000..f91908b --- /dev/null +++ b/vmextract.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2015 Neil Williams +# +# 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 . + +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() -- 2.39.5