Appendix C. Whole Disk Images

The main overcloud image is a flat partition image. This means it contains no partitioning information or bootloader on the images itself. The director uses a seperate kernel and ramdisk when booting and creates a basic partitioning layout when writing the overcloud image to disk. However, you can create a whole disk image, which includes a partitioning layout and bootloader.

C.1. Creating Whole Disk Images

Creating a whole disk image from the overcloud-full.qcow2 flat partition image involves the following steps:

  1. Open the overcloud-full flat partition as a base for our whole disk image.
  2. Create a new whole disk image with the desired size. This example uses a 10 GB image.
  3. Create partitions and volumes on the whole disk image. Create as many partitions and volumes necessary for your desired whole disk image. This example creates an isolated partition for boot and logical volumes for the other content in the filesystem.
  4. Create the initial filesystems on the partitions and volumes.
  5. Mount flat partition filesystem and copy content to the right partitions on the whole disk image.
  6. Generate the fstab content and save it to /etc/fstab on the whole disk image.
  7. Unmount all the filesystems.
  8. Mount the partitions on the whole disk image only. Start with the root partition mounted at / and mount the other partition in their respective directories.
  9. Install the bootloader using shell commands to execute grub2-install and grub2-mkconfig on the whole disk image. This installs the grub2 bootloader in the whole disk image.
  10. Update dracut to provide support for logical volume management
  11. Unmount all the filesystems and close the image

Manually Creating a Whole Disk Image

A recommended tool for creating images is guestfish, which you install using the following command:

$ sudo yum install -y guestfish

Once installed, run the guestfish interactive shell:

$ guestfish

Welcome to guestfish, the guest filesystem shell for
editing virtual machine filesystems and disk images.

Type: 'help' for help on commands
      'man' to read the manual
      'quit' to quit the shell

><fs>

For more information on using guestfish see "The Guestfish Shell" in the Virtualization Deployment and Administration Guide for Red Hat Enterprise Linux 7.

C.2. Automatically Creating a Whole Disk Image

The following Python script uses the guestfish library to automatically generate the whole disk image.

#!/usr/bin/env python
import guestfs
import os

# remove old generated drive
try:
  os.unlink("/home/stack/images/overcloud-full-partitioned.qcow2")
except:
  pass

g = guestfs.GuestFS(python_return_dict=True)

# import old and new images
print("Creating new repartitioned image")
g.add_drive_opts("/home/stack/images/overcloud-full.qcow2", format="qcow2", readonly=1)
g.disk_create("/home/stack/images/overcloud-full-partitioned.qcow2", "qcow2", 10.2 * 1024 * 1024 * 1024) #10.2G
g.add_drive_opts("/home/stack/images/overcloud-full-partitioned.qcow2", format="qcow2", readonly=0)
g.launch()

# create the partitions for new image
print("Creating the initial partitions")
g.part_init("/dev/sdb", "mbr")
g.part_add("/dev/sdb", "primary", 2048, 616448)
g.part_add("/dev/sdb", "primary", 616449, -1)

g.pvcreate("/dev/sdb2")
g.vgcreate("vg", ['/dev/sdb2', ])
g.lvcreate("var", "vg", 5 * 1024)
g.lvcreate("tmp", "vg", 500)
g.lvcreate("swap", "vg", 250)
g.lvcreate("home", "vg", 100)
g.lvcreate("root", "vg", 4 * 1024)
g.part_set_bootable("/dev/sdb", 1, True)

# add filesystems to volumes
print("Adding filesystems")
ids = {}
keys = [ 'var', 'tmp', 'swap', 'home', 'root' ]
volumes = ['/dev/vg/var', '/dev/vg/tmp', '/dev/vg/swap', '/dev/vg/home', '/dev/vg/root']
swap_volume = volumes[2]

count = 0
for volume in volumes:
  if count!=2:
    g.mkfs('ext4', volume)
    ids[keys[count]] = g.vfs_uuid(volume)
  count +=1

# create filesystem on boot and swap
g.mkfs('ext4', '/dev/sdb1')
g.mkswap_opts(volumes[2])
ids['swap'] = g.vfs_uuid(volumes[2])

# mount drives and copy content
print("Start copying content")
g.mkmountpoint('/old')
g.mkmountpoint('/root')
g.mkmountpoint('/boot')
g.mkmountpoint('/home')
g.mkmountpoint('/var')
g.mount('/dev/sda', '/old')

g.mount('/dev/sdb1', '/boot')
g.mount(volumes[4], '/root')
g.mount(volumes[3], '/home')
g.mount(volumes[0], '/var')

# copy content to root
results = g.ls('/old/')
for result in results:
  if result not in ('boot', 'home', 'tmp', 'var'):
    print("Copying %s to root" % result)
    g.cp_a('/old/%s' % result, '/root/')

# copy extra content
folders_to_copy = ['boot', 'home', 'var']
for folder in folders_to_copy:
  results = g.ls('/old/%s/' % folder)
  for result in results:
    print("Copying %s to %s" % (result, folder))
    g.cp_a('/old/%s/%s' % (folder, result),
           '/%s/' % folder)

# create /etc/fstab file
print("Generating fstab content")
fstab_content = """
UUID={boot_id} /boot ext4 defaults 0 2
UUID={root_id} / ext4 defaults 0 1
UUID={swap_id} none swap sw 0 0
UUID={tmp_id} /tmp ext4 defaults 0 2
UUID={home_id} /home ext4 defaults 0 2
UUID={var_id} /var ext4 defaults 0 2
""".format(
  boot_id=g.vfs_uuid('/dev/sdb1'),
  root_id=ids['root'],
  swap_id=ids['swap'],
  tmp_id=ids['tmp'],
  home_id=ids['home'],
  var_id=ids['var'])

g.write('/root/etc/fstab', fstab_content)

# unmount filesystems
g.umount('/root')
g.umount('/boot')
g.umount('/old')
g.umount('/var')

# mount in the right directories to install bootloader
print("Installing bootloader")
g.mount(volumes[4], '/')
g.mkdir('/boot')
g.mkdir('/var')
g.mount('/dev/sdb1', '/boot')
g.mount(volumes[0], '/var')

# do a selinux relabel
g.selinux_relabel('/etc/selinux/targeted/contexts/files/file_contexts', '/', force=True)
g.selinux_relabel('/etc/selinux/targeted/contexts/files/file_contexts', '/var', force=True)

g.sh('grub2-install --target=i386-pc /dev/sdb')
g.sh('grub2-mkconfig -o /boot/grub2/grub.cfg')

# create dracut.conf file
dracut_content = """
add_dracutmodules+="lvm crypt"
"""
g.write('/etc/dracut.conf', dracut_content)

# update initramfs to include lvm and crypt
kernels = g.ls('/lib/modules')
for kernel in kernels:
  print("Updating dracut to include modules in kernel %s" % kernel)
  g.sh('dracut -f /boot/initramfs-%s.img %s --force' % (kernel, kernel))
g.umount('/boot')
g.umount('/var')
g.umount('/')

# close images
print("Finishing image")
g.shutdown()
g.close()

Save this script as a executable file on the undercloud and run it as the stack user:

$ ./whole-disk-image.py

This automatically creates the whole disk image from the flat partition image. Once the whole disk image creation completes, replace the old overcloud-full.qcow2 image:

$ mv ~/images/overcloud-full.qcow2 ~/images/overcloud-full-old.qcow2
$ cp ~/images/overcloud-full-partitioned.qcow2 ~/images/overcloud-full.qcow2

You can now upload the whole disk image along with your other images.

C.3. Encrypting Volumes on Whole Disk Images

You can also use guestfish to encrypt volumes on your whole disk image. This requires using the luks-format subcommand, which erases the current volume and creates an encrypted volume.

The following Python script opens the overcloud-full-partitioned.qcow2 image created previously, removes the current home volume (which is empty), and replaces it with an encrypted home volume:

#!/usr/bin/env python
import binascii
import guestfs

g = guestfs.GuestFS(python_return_dict=True)
g.add_drive_opts("/home/stack/images/overcloud-full-partitioned.qcow2", format="qcow2", readonly=0)
g.launch()

random_content = binascii.b2a_hex(os.urandom(1024))
g.luks_format('/dev/vg/home', random_content, 0)
g.luks_open('/dev/vg/home', random_content, 'cryptedhome')
g.vgscan()
g.vg_activate_all(True)
g.mkfs('ext4', '/dev/mapper/cryptedhome')
g.mount('/dev/vg/root','/')

volumes = lvs()
volumes.remove('/dev/vg/home')
volumes.remove('/dev/vg/root')
volumes.remove('/dev/vg/swap')
fstab_content = []
fstab_content.append('UUID=%s /boot ext4 defaults 0 2' % g.vfs_uuid('/dev/sda1'))
fstab_content.append('UUID=%s / ext4 defaults 0 1' % g.vfs_uuid('/dev/vg/root'))
fstab_content.append('UUID=%s none swap sw 0 0' % g.vfs_uuid('/dev/vg/swap'))
fstab_content.append('/dev/mapper/cryptedhome /home ext4 defaults 0 1')
for volume in volumes:
  volume_name = volume.replace('/dev/vg/', '')
  fstab_content.append('UUID=%s /%s ext4 defaults 0 2' % (g.vfs_uuid(volume), volume_name))

g.write('/etc/fstab', '\n'.join(fstab_content))
print '\n'.join(fstab_content)

g.write('/root/home_keyfile', random_content)
g.chmod(0400, '/root/home_keyfile')

mapper = """
home UUID={home_id} /root/home_keyfile
""".format(home_id=g.vfs_uuid('/dev/mapper/cryptedhome'))
g.write('/etc/crypttab', mapper)

g.luks_close('/dev/mapper/cryptedhome')
g.selinux_relabel('/etc/selinux/targeted/contexts/files/file_contexts', '/', force=True)
g.shutdown()
g.close()

This script also:

  • Creates a key (random_content)
  • Regenerates the /etc/fstab file with the new encrypted volume
  • Saves the encryption key at /root/home_keyfile
  • Generates a crypttab file to automatically decrypt the volume using the /root/home_keyfile)

Use this script as an example to create encrypted volumes as part of your whole disk image creation process.

C.4. Uploading Whole Disk Images

To upload a whole disk image, use the --whole-disk-image option with the image upload command. For example:

$ openstack overcloud image upload --whole-disk --image-path /home/stack/images

This command uploads the images from /home/stack/images but treats the overcloud-full.qcow2 file as a whole disk image. This means you must rename the desired whole disk image to overcloud-full.qcow2 before running the image upload command.