附录 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. 卸载所有文件系统,然后关闭镜像

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

建议使用 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.3. 自动创建一个完整的磁盘镜像

以下的 Python 脚本使用 guestfish 程序库自动生成完整磁盘镜像。

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

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

$ ./whole-disk-image.py

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

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

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

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

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

以下 Python 脚本是 第 C.3 节 “自动创建一个完整的磁盘镜像” 中脚本的修改版。新脚本中加密了 home 卷:

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

# remove old generated drive
try:
  os.unlink("/tmp/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("/tmp/overcloud-full.qcow2", format="qcow2", readonly=1)
g.disk_create("/tmp/overcloud-full-partitioned.qcow2", "qcow2", 10 * 1024 * 1024 * 1024) #10G
g.add_drive_opts("/tmp/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", 4400)
g.lvcreate("tmp", "vg", 500)
g.lvcreate("swap", "vg", 250)
g.lvcreate("home", "vg", 100)
g.lvcreate("root", "vg", 4000)
g.part_set_bootable("/dev/sdb", 1, True)

# encrypt home partition and write keys
print("Encrypting volume")
random_content = binascii.b2a_hex(os.urandom(1024))
g.luks_format('/dev/vg/home', random_content, 0)

# open the encrypted volume
volumes = ['/dev/vg/var', '/dev/vg/tmp', '/dev/vg/swap', '/dev/mapper/cryptedhome', '/dev/vg/root']

g.luks_open('/dev/vg/home', random_content, 'cryptedhome')
g.vgscan()
g.vg_activate_all(True)

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

count = 0
for volume in volumes:
  if count!=2:
    g.mkfs('ext4', volume)
    if keys[count] == 'home':
      ids['home'] = g.vfs_uuid('/dev/vg/home')
    else:
      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)

# write keyfile for encrypted volume
g.write('/root/root/home_keyfile', random_content)
g.chmod(0400, '/root/root/home_keyfile')

# generate mapper for encrypted home
mapper = """
home UUID={home_id} /root/home_keyfile
""".format(home_id=ids['home'])
g.write('/root/etc/crypttab', mapper)

# create /etc/fstab file
print("Generating fstab content")
fstab_content = """
UUID={boot_id} /boot ext4 defaults 1 2
UUID={root_id} / ext4 defaults 1 1
UUID={swap_id} none swap sw 0 0
UUID={tmp_id} /tmp ext4 defaults 1 2
UUID={var_id} /var ext4 defaults 1 2
/dev/mapper/home /home ext4 defaults 1 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)

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

# close encrypted volume
g.luks_close('/dev/mapper/cryptedhome')

# 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')

# add rd.auto=1 on grub parameters
g.sh('sed  -i "s/.*GRUB_CMDLINE_LINUX.*/GRUB_CMDLINE_LINUX=\\"console=tty0 crashkernel=auto rd.auto=1\\"/" /etc/default/grub')

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))

# 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.umount('/boot')
g.umount('/var')
g.umount('/')

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

此脚本也可以:

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

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

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

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

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

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