附录 C. 完整的磁盘镜像

主要的 overcloud 镜像是一个平面分区镜像。这表示镜像本身不包含分区信息或引导加载程序。director 在引导时使用单独的内核和 ramdisk,在将 overcloud 镜像写入磁盘时创建一个基本的分区布局。但是,您可以创建包含分区布局和引导加载程序的完整磁盘镜像。

C.1. 创建完整的磁盘镜像

overcloud-full.qcow2 平面分区镜像创建完整的磁盘镜像包含以下步骤:

  1. 打开 overcloud-full 平面分区,这是完整磁盘镜像的基础。
  2. 按照所需的尺寸大小,创建完整的新磁盘镜像。本例使用 10 GB 镜像。
  3. 在完整的磁盘镜像上创建分区和卷。根据完整磁盘镜像所需的数量创建分区和卷。本例为 boot 创建了一个隔离分区,为文件系统中的其他内容创建了逻辑卷。
  4. 在分区和卷上创建初始文件系统。
  5. 安装平面分区文件系统,将内容复制到完整磁盘镜像的适当分区上。
  6. 生成 fstab 内容,并将其保存到完整磁盘镜像的 /etc/fstab 中。
  7. 卸载所有文件系统。
  8. 仅在完整磁盘镜像上挂载分区。先将根分区挂载到 / 中,然后将其他分区挂载到相应的目录中。
  9. 使用 shell 命令安装引导加载程序,以便在完整磁盘镜像上执行 grub2-installgrub2-mkconfig。此操作会将 grub2 引导加载程序安装到完整磁盘镜像中。
  10. 更新 dracut,以便支持逻辑卷管理
  11. 卸载所有文件系统,然后关闭镜像

手动创建一个完整的磁盘镜像

建议使用 guestfish 工具创建镜像,运行以下命令可安装此工具:

$ sudo yum install -y guestfish

安装完成后,运行 guestfish 交互式 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>

有关使用 guestfish 的更多信息,请参阅 Red Hat Enterprise Linux 7 虚拟部署和管理指南中的 "The Guestfish Shell"

C.2. 自动创建一个完整的磁盘镜像

以下 Python 脚本使用 guestfish 库来自动编写镜像创建脚本。该脚本根据 logical_volumes (Python 格式的词典)中的信息创建一系列卷。该词典中的键表示待创建的卷,它的值表示卷的大小。

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

logical_volumes = {
  'root': 4 * 1024,
  'swap': 250,
  'home': 500,
  'tmp':  250,
  'var':  5 * 1024
}

# 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', 12 * 1024 * 1024 * 1024)
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', ])
volume_names = []
for volume,size in logical_volumes.iteritems():
  g.lvcreate(volume, 'vg', size)
  volume_names.append(volume)

volume_names.append('boot')
g.part_set_bootable('/dev/sdb', 1, True)

g.mkmountpoint('/old')
g.mount('/dev/sda', '/old')

for volume_name in volume_names:
    # add filesystems to volumes
    print('Adding filesystems to %s' % volume_name)
    if volume_name == 'boot':
        g.mkfs('ext4', '/dev/sdb1')
    elif volume_name == 'swap':
        g.mkswap_opts('/dev/vg/%s' % volume_name)
    else:
        g.mkfs('ext4', '/dev/vg/%s' % volume_name)

    # Mount the volume and copy files
    if volume_name not in ['tmp', 'swap']:
        print('Mounting %s' % volume_name)
        g.mkmountpoint('/%s' % volume_name)
        if volume_name == 'boot':
            g.mount('/dev/sdb1', '/%s' % volume_name)
        else:
            g.mount('/dev/vg/%s' % volume_name, '/%s' % volume_name)

        print('Copying files to %s' % volume_name)
        if volume_name == 'root':
            results = [filename for filename in g.ls('/old/') if filename not in volume_names]
            results.append ('root')
            for result in results:
                print('- Copying %s to root' % result)
                g.cp_a('/old/%s' % result, '/root/')
        else:
            results = g.ls('/old/%s/' % volume_name)
            if results == []:
                print("- No files found")
            else:
                for result in results:
                    print('- Copying %s to %s' % (result, volume_name))
                    g.cp_a('/old/%s/%s' % (volume_name, result), '/%s/' % volume_name)

        g.umount('/%s' % volume_name)

g.umount('/old')
g.mount('/dev/vg/root', '/')

for volume_name in volume_names:
    if volume_name not in ['root','swap']:
        g.mkdir('/%s' % volume_name)
        if volume_name == 'boot':
            g.mount('/dev/sdb1', '/%s' % volume_name)
        else:
            g.mount('/dev/vg/%s' % volume_name, '/%s' % volume_name)

# create /etc/fstab file
print('Generating fstab content:')

fstab_content = []
fstab_content.append('UUID=%s /boot ext4 defaults 0 2' % g.vfs_uuid('/dev/sdb1'))
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'))
for volume_name in volume_names:
    if volume_name not in ['boot', 'root', 'swap']:
        fstab_content.append('UUID=%s /%s ext4 defaults 0 2' % (g.vfs_uuid('/dev/vg/%s' % volume_name), volume_name))

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

# Generate bootloader
print('Installing bootloader')
g.sh('grub2-install --target=i386-pc /dev/sdb')
g.sh('grub2-mkconfig -o /boot/grub2/grub.cfg')
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))

for volume_name in volume_names:
    if volume_name not in ['swap', 'root']:
        g.umount("/%s" % volume_name)

g.umount("/")

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

将此脚本作为可执行文件保存到 undercloud,然后以 stack 用户的身份运行该文件。

$ ./whole-disk-image.py

此操作会从平面分区镜像自动创建完整的磁盘镜像。完整的磁盘镜像创建完成之后,替换旧的 overcloud-full.qcow2 镜像:

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

现在,您可以同时上传完整的磁盘镜像和其他镜像。

C.3. 在完整的磁盘镜像上对卷进行加密

您也可以使用 guestfish 在完整的磁盘镜像上对卷进行加密。此操作需要使用 luks-format 子命令来擦除现有的卷并创建一个加密卷。

以下 Python 脚本可用于打开之前创建的 overcloud-full-partitioned.qcow2 镜像,移除当前的 home 卷(为空),并使用加密的 home 卷代替:

#!/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()

此脚本也可以:

  • 创建密钥 (random_content)
  • 使用新加密卷重新生成 /etc/fstab 文件
  • /root/home_keyfile 中保存加密密钥
  • 生成一个 crypttab 文件,以便使用 /root/home_keyfile) 自动解密卷

以此脚本为例,创建加密卷,这是完整的磁盘镜像创建流程的组成部分。

C.4. 上传完整的磁盘镜像

要上传完整的磁盘镜像,请使用镜像上传命令的 --whole-disk-image 选项。例如:

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

此命令从 /home/stack/images 上传镜像,但将 overcloud-full.qcow2 文件视为完整的磁盘镜像。这表示,在运行磁盘上传命令之前,您必须先将所需的完整磁盘镜像重命名为 overcloud-full.qcow2