I spent the weekend in bed with a cold … and with a laptop hacking on ways to get OE rootfs images packaged and installed in other OE rootfs images. My solution isn’t great but it’s functional and it doesn’t require that every image be built in series. But before I get too far let’s go over what I set out to achieve and get a rough set of requirements:
- I’m building a rootfs that itself contains other rootfs’. The inner rootfs’ will be VMs. The outer rootfs will be the host with the hypervisor (Xen dom0).
- Build speeds are important so I’d like to share as much of the build infrastructure between VMs as possible.
- Running builds in parallel is a good thing. Running all builds as serial operations is a non-starter. Bonus points for being able to distribute them across multiple hosts.
- Having to implement a pile of shell script outside of bitbake to make this work means you’re doing it wrong. The script that automates this build should be doing little more than calling bitbake.
First things first: my solution isn’t perfect. It does work pretty well though and achieves much of the above. Below is a quick visual of what I intend for the end product to support:
On the left is the simple case I’m working to support currently. The boxes represent the root file systems (rootfs) that bitbake is churning out. The lines pointing from one rootfs to another represent one rootfs being packaged in another. dom0 here would be a live image and it would boot the NDVM automatically. Naturally the NDVM rootfs must be contained within dom0 for this to work. The right hand side is an eventual goal.
To support what most people think of as a ‘distro’ you need an installer to lay things down on a physical disk and if users expect to be able to run arbitrary workloads / VMs then they’ll want the whole disk available. In this scenario the installer image rootfs will have the image packages for the VMs installed in it (including dom0!). The installer when do it’s thing laying dom0 down in a partition but it can also drop the supporting VMs images into another partition. After installation, dom0 is booted from physical media and it will be able to boot these supporting VMs.
Notice the two level hierarchical relationship between the rootfs images in the diagram. The rootfs’ on the lower part of the diagram are completely independent and thus can be built in parallel. This will make them easily distributed across multiple build systems. Now on to some of the methods I tried to realize this goal and eventually one that worked!
Changing DISTRO in a build tree
The first thing I played around with was rewriting my local.conf
between image builds to change the DISTRO
. I use a different DISTRO
configs to make package customizations that differentiate my images. This would allow me to add a simple IMAGE_POSTPROCESS_COMMAND
to copy service VM rootfs images into the outer image (again, dom0 or an installer).
I had hoped I’d be able to pull this off and just have bitbake magically pick up the differences so I could build multiple images in the same build tree. This would make my image builds serial but possibly very efficient. This caused several failures in my tests however so I decided it would be best to keep separate builds for my individual images. I should probably find the right mailing lists to help track down the root cause of this but I expect this is well outside of the ‘supported’ bitbake / OE use cases.
Copying data across build trees
As a fall-back I came up with a hack in which I copy the needed build artifacts (rootfs & kernel image) to a common location as a post processing step in the image recipe. I’ve encapsulated this in a bbclass
in anticipation of using the same pattern for other VM images. I’ve called this class integral-image-export.bbclass
:
inherit core-image do_export() { manifest_install() { if [ ! -z "$1" ]; then install -m 0644 "$1" "$4" printf "%s *%sn" "$(sha256sum --binary $1 | awk '{ print $1 }')" "$2" >> $3 fi } # only do export if export dir is defined if [ ! -z "${INTEGRAL_EXPORT_DIR}" ]; then ROOT="${INTEGRAL_EXPORT_DIR}/${PN}-$(date --utc +%Y-%m-%dT%H:%M:%S.%NZ)" FS_FILE="${IMAGE_BASENAME}-${MACHINE}.ext3" KERN_FILE="${KERNEL_IMAGETYPE}-${MACHINE}.bin" KERN_PATH="${DEPLOY_DIR_IMAGE}/${KERN_FILE}" MANIFEST="${ROOT}/manifest" mkdir -p ${ROOT} manifest_install "${KERN_PATH}" "${KERN_FILE}" "${MANIFEST}" "${ROOT}" manifest_install "${ROOTFS}" "${FS_FILE}" "${MANIFEST}" "${ROOT}" fi } addtask export before do_build after do_rootfs
It lives here https://github.com/flihp/meta-integral/blob/master/classes/integral-image-export.bbclass. So by having my NDVM image inherit this class, and properly defining the INTEGRAL_EXPORT_DIR
in my builds local.conf, the NDVM image recipe will copy these build artifacts out of the build tree.
Notice that the destination directory has an overly precise time stamp as part of its name. This is an attempt to create unique identifiers for images without having to track incrementing build numbers. Also worth noting is the manifest_install
function. Basically this generates a file in the same format as the sha*sum
utilities with the intent of those programs being able to verify the manifest.
Eventually I think it would be useful for a manifest to contain data about the meta layers that went into building the image and the hashes of the git commit checked out at the time of the build. This later bit will be useful if a build ever has to be recreated. Not something that’s necessary yet however.
Consuming exported images
After exporting these build artifacts we have to cope with other images that want to consume them. My main complaint about using a build script outside of my built tree to place images within one another is that I’d have to re-size existing file systems. Bitbake already builds file systems so resizing them from an external script seemed very ugly. Further changes to the images built by bitbake (ext3/iso/hddimg etc) would have to be coordinated with said external script. Very ugly indeed.
The most logical solution was to create a new recipe as a way to package the existing build artifacts into a package that can be consumed by an image. By ‘package’ I mean your typical ipk or rpm. This allows bitbake to continue to do all of the heavy lifting in image building for us. Assuming the relationships between images shown above, it allows the outer image to include the image package using the standard IMAGE_INSTALL
mechanism. That feels borderline elegant compared to rewriting the generated file systems IMHO.
So from the last section we have builds that are pumping out build artifacts and for the case of our example we’ll say they land in /mnt/integral/image-$stamp
where $stamp is a unique time stamp. On the other hand we need to create a recipe that consumes the artifacts (I’ll call it an ‘image package recipe’ from here out) in these directories. Typically in a bitbake recipe you’ll provide a URI to your source code in the SRC_URI
variable and define the files that go into the image using FILES_${PN}
. These are generally defined statically in the recipe. Our case is weird in that we want the image package recipe to grab the latest image exported by some other build. So we must dynamically generate these variables.
Though I’ve never seen these variables generated dynamically (aside from using the PN
and PV
variables in URIs) but it’s surprisingly easy. bitbake
supports anonymous python functions that get run when the recipe is parsed. This happens before any tasks are executed so setting SRC_URI
and PV
in this function works quite well. The method for determining the latest images that our build has exported is a simple directory listing and sorting operation:
python() { import glob, os, subprocess # check for valid export dir INTEGRAL_EXPORT_DIR = d.getVar ('INTEGRAL_EXPORT_DIR', True) if INTEGRAL_EXPORT_DIR is None: bb.note ('INTEGRAL_EXPORT_DIR is empty') return 0 if not os.path.isdir (INTEGRAL_EXPORT_DIR): bb.fatal ('INTEGRAL_EXPORT_DIR is set, but not a directory: {0}'.format (INTEGRAL_EXPORT_DIR)) return 1 PN = d.getVar ('PN', True) LIBDIR = d.getVar ('libdir', True) XENDIR = d.getVar ('XENDIR', True) VMNAME = d.getVar ('VMNAME', True) # find latest ndvm and verify hashes IMG_NAME = PN[:PN.rfind ('-')] DIR_LIST = glob.glob ('{0}/{1}*'.format (INTEGRAL_EXPORT_DIR, IMG_NAME)) DIR_LIST.sort (reverse=True) DIR_SAVE = os.getcwd () os.chdir (DIR_LIST [0]) try: DEV_NULL = open ('/dev/null', 'w') subprocess.check_call ('sha256sum -c manifest', stdout=DEV_NULL, shell=True) except subprocess.CalledProcessError: return 1 finally: DEV_NULL.close () os.chdir (DIR_SAVE) # build up SRC_URI and FILES_${PN} from latest NDVM image d.appendVar ('SRC_URI', 'file://{0}/*'. format (DIR_LIST [0])) d.appendVar ('FILES_{0}'.format (PN), ' {0}/{1}/{2}/*'.format (LIBDIR, XENDIR, VMNAME)) # set up ${S} WORKDIR = d.getVar ('WORKDIR', True) d.setVar ('S', '{0}/{1}'.format (WORKDIR, DIR_LIST [0])) return 0 }
If you’re interested in the full recipe for this image package you can find it here: https://github.com/flihp/meta-integral/blob/master/recipes-integral/images/integral-image-ndvm-pkg.bb
The ‘manifest’ described above is also verified and processed. Using the file format of the sha256sum
utility is a cheap approximation of the OE SRC_URI[sha256sum]
metadata. This is a pretty naive approach to finding the “right” image to package as it doesn’t give the outer image much say over which inner image to pull in: It just grabs the latest. Some mechanism for the consuming image to specify which image it consumes would be useful.
So that’s about it. I’m pretty pleased with the outcome but time will tell how useful this approach is. Hopefully I’ll get a chance to see if it scales well in the future. Throw something in the comments if you get a chance to play around with this or have thoughts on the topic.