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:
-
Open the
overcloud-full
flat partition as a base for our whole disk image. - Create a new whole disk image with the desired size. This example uses a 10 GB image.
-
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. - Create the initial filesystems on the partitions and volumes.
- Mount flat partition filesystem and copy content to the right partitions on the whole disk image.
-
Generate the
fstab
content and save it to/etc/fstab
on the whole disk image. - Unmount all the filesystems.
-
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. -
Install the bootloader using shell commands to execute
grub2-install
andgrub2-mkconfig
on the whole disk image. This installs thegrub2
bootloader in the whole disk image. -
Update
dracut
to provide support for logical volume management - 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
$ sudo yum install -y guestfish
Once installed, run the guestfish
interactive shell:
guestfish <fs>
$ 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.
remove old generated drive import old and new images create the partitions for new image add filesystems to volumes create filesystem on boot and swap mount drives and copy content copy content to root copy extra content create /etc/fstab file unmount filesystems mount in the right directories to install bootloader do a selinux relabel create dracut.conf file update initramfs to include lvm and crypt close images
#!/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
$ ./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
$ 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()
#!/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
$ 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.