背景
对业务虚拟机的系统盘做一个快照,每次开机之前或关机之后自动还原快照,就可以避免 Windows 异常之后虚拟机无法修复的情况。
需求分析
- 加
-snapshot
参数做不到只对 vda 加快照 - 加了
-snapshot
参数的机器,如果要更新 vda 需要至少两次重启虚拟机(关机->解快照->开机->更新->关机->加快照->开机) - 尝试单纯使用 libvirt hooks 来实现,发现其有两个限制:
- libvirt hooks 不能回调 libvirt API,会无限递归
- 不像 VDSM hooks,libvirt hooks 无法修改 libvirt XML
潜在的解决方案有几种:
- 对 qemu-kvm 进行包装(wrap)替换原生的程序
- 自行修改 libvirt 源代码,重新编译
- 使用外部配置文件进行控制是否加快照,使用 libvirt hooks 进行快照自动还原
解决方案
最终选择最后一种方案,简单有效。
- 外部配置文件来控制是否加「快照自动还原」
- libvirt hooks 来控制每次开机的时重置快照
- 如果要单次更新 vda,关机后做一次 commit
- hook 需要命名为 /etc/libvirt/hooks/qemu,因为 libvirt 6.5.0 之后才支持 /etc/libvirt/hooks/qemu.d/ 路径
基本逻辑
- 假设虚拟机有两个虚拟磁盘设备 vda 和 vdb,现在需要令 vda 自动还原快照,vdb 不做快照
- 假设 vda 的虚拟磁盘文件为 os.img
- 虚拟机开机之前,将 os.img 重命名为 os.img.backing_file (如果该文件已存在,则不进行重命名)
- 以 os.img.backing_file 作为 backing_file 创建 os.img
- 虚拟机开机,会根据其 libvirt xml 文件查找其虚拟磁盘文件,即 os.img
- 虚拟机运行,增量内容写入到 os.img
- 虚拟机关机,查询外部配置文件,是否要做「单次提交」
- 如果需要,做「单次提交」并修改配置文件,将「单次提交」标记为否
- 「提交」的意思是将一个 os.img 中的内容写入到其 backing_file 中
- 将 os.img.backing_file 重命名回 os.img(完成对增量内容的覆盖)
示例说明
#!/usr/bin/env python3
import json
import logging
import shlex
import subprocess
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
class Config:
def __init__(self, configFile):
self.configFile = configFile
with open(self.configFile, "r") as f:
self.config = json.load(f)
self.disks = self.config["disks"]
def report(self):
""" Do a one-shot-commit, then change the config file. """
with open(self.configFile, "w") as f:
json.dump(self.config, f, indent=2)
def getHookDetails():
raw_xml = "".join((sys.stdin.readlines()))
logging.info(str(sys.argv))
return raw_xml
def getFilePath(target, domXML):
XML = ET.fromstring(domXML)
pattern = f"./devices/disk/target[@dev='{target}']..[@device='disk']"
disk = XML.find(pattern)
diskXML = ET.tostring(disk).decode()
target_path = disk.find("source").get("file")
logging.debug(f"Disk XML Dump:\n{diskXML}")
return target_path
class SnapshotAutoRevert:
def __init__(self, domName, qcow2_file):
""" Take qcow2 file path as argument, which will be auto reverted in every VM life cycle. """
self.domName = domName
self.qcow2_file = qcow2_file
def renameToBackingFile(self):
if not Path(f"{self.qcow2_file}.backing_file").is_file():
Path(self.qcow2_file).rename(f"{self.qcow2_file}.backing_file")
logging.info(f"{self.domName} Rename origin image to backing_file")
def createSnapshot(self):
logging.info(f"{self.domName} Create a snapshot based on origin image")
cmd = (
f"qemu-img create -f qcow2 {self.qcow2_file} "
f"-b {self.qcow2_file}.backing_file -F qcow2"
)
args = shlex.split(cmd)
with subprocess.Popen(args, stdout=subprocess.PIPE) as proc:
logging.info(proc.stdout.read().decode())
def commitSnapshot(self):
logging.info(f"{self.domName} Commit the snapshot")
cmd = f"qemu-img commit {self.qcow2_file}"
args = shlex.split(cmd)
with subprocess.Popen(args, stdout=subprocess.PIPE) as proc:
logging.info(proc.stdout.read().decode())
def revertSnapshot(self):
Path(f"{self.qcow2_file}.backing_file").rename(self.qcow2_file)
logging.info(f"{self.domName} Revert to origin image, delete snapshot")
def main():
hookPath = Path(sys.argv[0])
domName = sys.argv[1]
domAction = sys.argv[2]
configFile = Path.joinpath(hookPath.parent, "config", f"{domName}.json")
if not configFile.exists():
exit(0)
vmConfig = Config(configFile)
domXML = getHookDetails()
logging.debug(f"Config before: {vmConfig.config}")
index = -1
reportFlag = False
for disk in vmConfig.disks:
index += 1
logging.info(disk["target"])
target_file = getFilePath(disk["target"], domXML)
commitFlag = disk["one-shot-commit"]
AutoRevert_target = SnapshotAutoRevert(domName, target_file)
if domAction == "prepare":
AutoRevert_target.renameToBackingFile()
AutoRevert_target.createSnapshot()
elif domAction == "release":
if commitFlag:
AutoRevert_target.commitSnapshot()
vmConfig.config["disks"][index]["one-shot-commit"] = False
reportFlag = True
AutoRevert_target.revertSnapshot()
if reportFlag:
vmConfig.report()
logging.debug(f"Config after: {vmConfig.config}")
if __name__ == "__main__":
logging.basicConfig(
filename="/var/log/libvirt/qemu/hooks.log",
format="%(asctime)s %(message)s",
level=logging.INFO,
)
main()
hook 需要命名为 /etc/libvirt/hooks/qemu
,因此在有快照的机器上查看该文件即可读取源代码。
配置文件位置 /etc/libvirt/hooks/config/{vm_name}.json
(每个虚拟机使用各自的配置文件)。
{
"disks": [
{
"target": "vda",
"one-shot-commit": true
},
{
"target": "sdb",
"one-shot-commit": false
}
]
}
该配置文件的含义是对 {vm_name}
这个虚拟机设置针对 vda 和 sdb 快照自动还原,当该虚拟机的此次生命周期结束之后,对其 vda 进行「单次提交」(维护操作),提交之后该配置文件会被更新为如下内容:
{
"disks": [
{
"target": "vda",
"one-shot-commit": false
},
{
"target": "sdb",
"one-shot-commit": false
}
]
}
在一个典型的业务宿主机上,常见的配置文件如下所示:
{
"disks": [
{
"target": "vda",
"one-shot-commit": false
}
]
}
意思就是「只针对 vda 做快照自动还原,正常工作,不做单次提交」,没有 vdb 或 sdb,就不会对这些虚拟磁盘做快照。
如果找不到虚拟机的配置文件,就不会对虚拟机做快照。
如果配置文件是非法的 json 格式,该虚拟机不会启动。
相关日志会输出到 /var/log/libvirt/qemu/hooks.log
。
想查看一台虚拟机是否运行在「快照自动还原」模式下,可以在虚拟机运行过程中执行 virsh dumpxml vm_name | grep backing_file
,如果有结果,就代表该机器当前运行在「快照自动还原」模式下。此时,如果想使虚拟机中的更新内容固化下来,可以直接去修改其 hook 配置文件 /etc/libvirt/hooks/config/{vm_name}.json
,将 "one-shot-commit"
改为 true
即可。虚拟机生命周期结束时,会由 hook 脚本来执行「单次提交」并自动将 "one-shot-commit"
改为 false