hook的功能介绍
在ovirt的开发中,当相关的某些功能不满足自己的实际需求时,通常会修改ovirt的源码来满足自己的功能需求,但此方法会带来相关问题:
- 修改源代码会增加漏洞(bug)的风险,影响系统的稳定性。
- 升级ovirt版本时,增加了代码维护的成本。
- 如果ovirt的版本变化较大,甚至导致无法顺利升级ovirt版本。
- 鉴于以上问题,ovirt增加了Hook机制,只需将自己的相关代码(实现某功能)放到指定目录,ovirt的hook机制会自动调用该代码,来满足用户的个性化功能需求。hook为虚拟机的功能扩展提供了方便,hook机制优点如下:
- 无需修改源码来实现功能需求。
- 增加系统的稳定性。
- 减少代码的维护成本。
- 减少升级的成本。
- 增加功能的可移植性。
- 快速满足多样化,定制化需求。
- 无需重启相关服务(vsdmd服务),代码即可运行。
hook机制的原理分析
以虚拟机操作为例:虚拟机的相关操作有许多,在执行某个虚拟机操作之前或之后,想执行自定义的相关功能或操作(例如,启动虚拟机之前,通过修改XML来实现设备挂载透传,增加磁盘或USB设备等)来实现个性化的功能时,ovirt在虚拟机的对应操作之前或之后增加了hook函数。不同的hook函数会调用相应目录下的脚本文件实现相应功能。
hook相关目录及脚本
ovirt中存放hook脚本的目录及内容如下:
[root@host71 hooks]# pwd /usr/libexec/vdsm/hooks [root@host71 hooks]# ls after_device_create #设备创建完成后 执行该目录下的脚本 after_ifcfg_write after_vm_cont #虚拟机恢复执行之后 执行目录下的脚本 before_device_migrate_source #虚拟机从源主机迁移之前 执行该目录下的脚本 before_set_num_of_cpus #设置虚拟机CPU数量之前 执行该目录下的脚本 after_device_destroy #虚拟机custom设备删除之前 执行该目录下的脚本 after_memory_hotplug #虚拟机热插(增加)内存之后 执行该目录下的脚本 after_vm_dehibernate #虚拟机从暂停中恢复之后 执行该目录下的脚本 before_disk_hotplug #虚拟机磁盘热插之前 执行该目录下的脚本 before_update_device #虚拟机设备更新之前 执行该目录下的脚本 after_device_migrate_destination after_network_setup after_vm_destroy #虚拟机删除之后 执行该目录下的脚本 before_disk_hotunplug #虚拟机磁盘热拔(移除)之前 执行该目录下的脚本 before_vdsm_start #启动vdsm服务之前 执行该目录下的脚本 after_device_migrate_source after_network_setup_fail after_vm_hibernate #虚拟机停止并保持状态之后 执行该目录下的脚本 before_get_all_vm_stats #获取所有虚拟机的状态信息之前 执行该目录下的脚本 before_vm_cont #虚拟机恢复运行之前 执行该目录下的脚本 after_disk_hotplug #虚拟机磁盘热插之后 执行该目录下的脚本 after_nic_hotplug #虚拟机网卡热插之后 执行该目录下的脚本 after_vm_migrate_destination #虚拟机迁移到目的主机之后 执行该目录下的脚本 before_get_caps #获取主机capabilities之前 执行该目录下的脚本 before_vm_dehibernate #虚拟机停止并保持状态之前 执行该目录下的脚本 after_disk_hotunplug #虚拟机磁盘热拔之后 执行该目录下的脚本 after_nic_hotplug_fail #虚拟机网卡热插失败之后 执行该目录下的脚本 after_vm_migrate_source #虚拟机从原主机迁移之后 执行该目录下的脚本 before_get_stats #获取主机stat信息之前 执行该目录下的脚本 before_vm_destroy #销毁虚拟机之前 执行该目录下的脚本 after_disk_prepare after_nic_hotunplug #虚拟机网卡热拔之后 执行该目录下的脚本 after_vm_pause #虚拟机暂停之后 执行该目录下的脚本 before_get_vm_stats #获取虚拟机stat信息之前 执行该目录下的脚本 before_vm_hibernate #虚拟机进入停止状态并保存状态之前 执行该目录下的脚本 after_get_all_vm_stats #获取所有虚拟机的stats之后 执行该目录下的脚本 after_nic_hotunplug_fail #虚拟机网卡热拔失败之后 执行该目录下的脚本 after_vm_set_ticket #虚拟机设置ticket之后 执行该目录下的脚本 before_ifcfg_write before_vm_migrate_destination #虚拟机迁移到目的主机之前 执行该目录下的脚本 after_get_caps #获取主机capabilities之后 执行该目录下的脚本 after_set_num_of_cpus #设置虚拟机CPU数量之后 执行该目录下的脚本 after_vm_start #虚拟机启动之后 执行该目录下的脚本 before_memory_hotplug #虚拟机内存热插之前 执行该目录下的脚本 before_vm_migrate_source #虚拟机从源主机迁移之前 执行该目录下的脚本 after_get_stats #获取主机stat信息之后 执行该目录下的脚本 after_update_device #更新虚拟机设备之后 执行该目录下的脚本 before_device_create before_network_setup before_vm_pause #虚拟机暂停之前 执行该目录下的脚本 after_get_vm_stats #获取虚拟机stat信息之后 执行该目录下的脚本 after_update_device_fail #更新虚拟机设备失败之后 执行该目录下的脚本 before_device_destroy #销毁虚拟机custom设备之前 执行该目录下的脚本 before_nic_hotplug #虚拟机网卡热插之前 执行该目录下的脚本 before_vm_set_ticket #虚拟机设置ticket之前 执行该目录下的脚本 after_hostdev_list_by_caps after_vdsm_stop #停止vdsm服务之后 执行该目录下的脚本 before_device_migrate_destination before_nic_hotunplug #虚拟机网卡热拔之前 执行该目录下的脚本 before_vm_start #虚拟机启动之前 执行该目录下的脚本
以上内容全部为目录,通常会把脚本文件放入对于的目录中,所有脚本必须拥有可执行权限。通过目录名称能够区分该目录下的脚本何时被调用执行。默认系统在部分目录下存放了部分脚本。
进入目录before_vm_start。
[root@host71 ~]# cd /usr/libexec/vdsm/hooks/before_vm_start/ [root@host71 before_vm_start]# ls -rwxr-xr-x. 1 root root 2808 Mar 10 22:11 50_hostedengine -rwxr-xr-x. 1 root root 1327 Apr 3 16:30 50_nestedvt -rwxr-xr-x. 1 root root 1158 Apr 3 16:30 50_rmsmbios -rwxr-xr-x. 1 root root 1709 Mar 24 09:13 50_vhostmd
查看脚本‘50_nestedvt’的内容(脚本需要有可执行权限)。
import hooking
from vdsm import osinfo
cpu_nested_features = {
"kvm_intel": "vmx",
"kvm_amd": "svm",
}
nestedvt = osinfo.nested_virtualization()
if nestedvt.enabled:
domxml = hooking.read_domxml()
feature_vmx = domxml.createElement("feature")
feature_vmx.setAttribute("name", cpu_nested_features[nestedvt.kvm_module])
feature_vmx.setAttribute("policy", "require")
domxml.getElementsByTagName("cpu")[0].appendChild(feature_vmx)
hooking.write_domxml(domxml)
以上脚本50_nestedvt的功能为:查看系统是否开启嵌套虚拟化,如果开启则修改虚拟机的XML文件使其支持嵌套虚拟化功能。
hook调用源码分析
在实际的开发中,hook中使用情况较多的是“启动虚拟机前”的操作,即目录before_vm_start中的脚本,
首先,查找“before_vm_start”在ovirt中的位置。
#/usr/lib/python2.7/site-packages/vdsm/virt/vm.py
def _run(self):
self.log.info("VM wrapper has started")
if not self.recovering and \
self._altered_state.origin != _MIGRATION_ORIGIN:
self._remove_domain_artifacts()
...
...
else:
flags = libvirt.VIR_DOMAIN_NONE
with self._confLock:
# We use this flag only when starting VM, and we need to
# make sure not to pass or use it on migration creation.
if self._launch_paused:
flags |= libvirt.VIR_DOMAIN_START_PAUSED
self._pause_code = 'NOERR'
hooks.dump_vm_launch_flags_to_file(self.id, flags)
if self.hugepages:
self._prepare_hugepages()
try:
hooks.before_vm_start( #hook执行函数
self._buildDomainXML(),
self._custom,
final_callback=self._updateDomainDescriptor)
flags = hooks.load_vm_launch_flags_from_file(self.id)
# TODO: this is debug information. For 3.6.x we still need to
# see the XML even with 'info' as default level.
self.log.info("%s", self._domain.xml)
dom = self._connection.defineXML(self._domain.xml)
self._dom = virdomain.Defined(self.id, dom)
self._update_metadata()
dom.createWithFlags(flags) #启动虚拟机
self._dom = virdomain.Notifying(dom, self._timeoutExperienced)
hooks.after_vm_start(self._dom.XMLDesc(0), self._custom)
for dev in self._customDevices():
hooks.after_device_create(dev._deviceXML, self._custom,
dev.custom)
finally:
hooks.remove_vm_launch_flags_file(self.id)
- 通过以上的代码可以看出,hook值为“before_vm_start”的逻辑处理代码在文件/usr/lib/python2.7/site-packages/vdsm/virt/vm.py中,对应的函数为“def _run(self)”。
- hook的执行处理函数为27行代码。第一个参数:虚拟机的xml信息,第二个参数:自定义的设备对象,第三个参数:hook脚本执行完的“回掉函数”。
- 第42行为虚拟机调用libvirt接口启动虚拟机的代码。
第二,分析“hooks.before_vm_start”函数
#/usr/lib/python2.7/site-packages/vdsm/common/hooks.py
def before_vm_start(domxml, vmconf={}, final_callback=None):
errors = []
final_xml = _runHooksDir(domxml, 'before_vm_start', vmconf=vmconf,
raiseError=False, errors=errors)
if final_callback is not None:
final_callback(final_xml)
if errors:
raise exception.HookError(errors[-1])
return final_xml
- 第4行代码:运行hooks目录下的脚本(即/usr/libexec/vdsm/hooks/before_vm_start/下),运行完目录下的脚本(脚本会修改虚拟机的xml)后,返回修改后的虚机xml信息给变量final_xml。
- 第6,7行代码:如果传递了回掉函数,则执行该回掉函数。
第三,分析“_runHooksDir”函数
#/usr/lib/python2.7/site-packages/vdsm/common/hooks.py
def _runHooksDir(data, dir, vmconf={}, raiseError=True, errors=None, params={},
hookType=_DOMXML_HOOK):
if errors is None:
errors = []
scripts = _scriptsPerDir(dir) #获取before_vm_start目录下的具有可执行权限的脚本文件
scripts.sort() #对脚本文件进行排序
if not scripts: #before_vm_start目录下没有可执行的脚本文件,则退出函数
return data
data_fd, data_filename = tempfile.mkstemp() #创建临时文件data_filename
try:
if hookType == _DOMXML_HOOK: #默认参数为_DOMXML_HOOK,将data(虚拟机xml)数据写入临时文件data_filename中
os.write(data_fd, data or '')
elif hookType == _JSON_HOOK:
os.write(data_fd, json.dumps(data))
os.close(data_fd)
scriptenv = os.environ.copy() #复制系统环境变量
# Update the environment using params and custom configuration
env_update = [six.iteritems(params),
six.iteritems(vmconf.get('custom', {}))]
# Encode custom properties to UTF-8 and save them to scriptenv
# Pass str objects (byte-strings) without any conversion
for k, v in itertools.chain(*env_update): #更新系统变量的值
try:
if isinstance(v, unicode):
scriptenv[k] = v.encode('utf-8')
else:
scriptenv[k] = v
except UnicodeDecodeError:
pass
if vmconf.get('vmId'):
scriptenv['vmId'] = vmconf.get('vmId')
ppath = scriptenv.get('PYTHONPATH', '')
hook = pkgutil.get_loader('vdsm.hook').filename
scriptenv['PYTHONPATH'] = ':'.join(ppath.split(':') + [hook])
if hookType == _DOMXML_HOOK:
scriptenv['_hook_domxml'] = data_filename #将临时文件data_filename放入scriptenv中
elif hookType == _JSON_HOOK:
scriptenv['_hook_json'] = data_filename
for s in scripts: #遍历执行脚本文件,脚本文件对虚拟机xml修改后的结果会保存在临时文件data_filename中
rc, out, err = commands.execCmd([s], raw=True,
env=scriptenv)
logging.info('%s: rc=%s err=%s', s, rc, err)
if rc != 0:
errors.append(err)
if rc == 2:
break
elif rc > 2:
logging.warn('hook returned unexpected return code %s', rc)
if errors and raiseError:
raise exception.HookError(err)
with open(data_filename) as f: #读取临时文件data_filename中的内容(虚拟机xml信息)到变量final_data中
final_data = f.read()
finally:
os.unlink(data_filename) #删除临时文件
if hookType == _DOMXML_HOOK:
return final_data #返回虚拟机最新的xml信息
elif hookType == _JSON_HOOK:
return json.loads(final_data)
第四,分析“_updateDomainDescriptor”回掉函数
def before_vm_start(domxml, vmconf={}, final_callback=None):
errors = []
final_xml = _runHooksDir(domxml, 'before_vm_start', vmconf=vmconf,
raiseError=False, errors=errors)
if final_callback is not None:
final_callback(final_xml) #执行回掉函数,由上面的代码可以看出回掉函数为“_updateDomainDescriptor”,参数为虚拟机最新的xml信息
if errors:
raise exception.HookError(errors[-1])
return final_xml
#/usr/lib/python2.7/site-packages/vdsm/virt/vm.py
def _updateDomainDescriptor(self, xml=None):
domxml = self._dom.XMLDesc(0) if xml is None else xml #如果参数xml信息不为None,则将虚拟机最新的xml信息赋值给变量domxml
self._domain = DomainDescriptor(domxml) #更新虚拟机域描述符变量self._domain
由以上几步分析,可以了解hook原理机制的过程。
以before_vm_start情况为例,简单概括:
- 将虚拟机xml信息写入到临时文件中。
- 在hook脚本中读取临时文件,执行相关的操作,最后将数据写入临时文件中。
- 读取临时文件内容到变量,删除临时文件。
- 执行回掉函数,将虚拟机的xml信息更新到虚拟机对象的域描述符中。
自定义hook脚本
分析完了ovirt hook机制的原理,我们应该如何编写hook脚本呢!首先我们需要了解hook脚本中常用的模块和函数。
- import hooking #执行hook脚本重要的模块,模块目录/usr/lib/python2.7/site-packages/vdsm/common/hooking.py
- hooking.read_domxml() #读取虚拟机的XML信息函数
- hooking.write_domxml(domxml) #将虚拟机的XML信息写到文件中
- hooking.log() #日志打印函数,日志会打印到/var/log/vdsm/vdsm.log文件中
读取虚拟机的XML信息函数的实现
def read_domxml():
with io.open(os.environ['_hook_domxml'], 'rb') as f:
return minidom.parseString(f.read().decode('utf-8'))
#os.environ['_hook_domxml']的值为_runHooksDir函数中创建的临时文件data_filename。
#读取临时文件的内容
虚拟机的XML信息写入文件函数
def write_domxml(domxml):
with io.open(os.environ['_hook_domxml'], 'wb') as f:
f.write(domxml.toxml(encoding='utf-8'))
#写入domxml到临时文件data_filename中。
编写一个hook脚本50_vm_channels,实现虚拟机启动前,给虚拟机增加两个通道channel,直接上代码:
# /usr/libexec/vdsm/hooks/before_vm_start/50_vm_channels
[root@host71 before_vm_start]# cat 50_vm_channels
#!/usr/bin/python2
import os
import hooking #导入hooking模块
import uuid
VM_CHANNELS_PATH = "/var/lib/libvirt/qemu/channels/" #libvirt的channels路径
VM_CHANNELS_NAME = ["tools-guest-agent.0", "ovirt-client-redirect.0"] #通道的名称
def get_vm_uuid(): #获取虚拟机的UUID函数
domxml = hooking.read_domxml()
domain = domxml.getElementsByTagName('domain')[0]
name = domain.getElementsByTagName('uuid')[0].childNodes[0].nodeValue
hooking.log("vm-uuid: %s" % name)
return name
def create_channel(channel_name):
domxml = hooking.read_domxml() #读取虚拟机的xml,即读取临时文件data_filename的内容
devs = domxml.getElementsByTagName('devices')[0] #提取设备devices元素
channel = domxml.createElement('channel') #创建channel元素,并设置属性type=unix
channel.setAttribute('type', 'unix')
vm_uuid = get_vm_uuid()
valid_channel_name = vm_uuid + "." + channel_name
path = os.path.join(VM_CHANNELS_PATH, valid_channel_name) #组织channel的路径信息
source = domxml.createElement('source') #创建source元素,并设置属性mode=bind,path=path
source.setAttribute('mode', 'bind')
source.setAttribute('path', path)
target = domxml.createElement('target') #创建target元素,并设置属性type=virtio,name=channel_name
target.setAttribute('type', 'virtio')
target.setAttribute('name', channel_name)
channel.appendChild(source) #将source元素,target元素包含在channel元素下面。
channel.appendChild(target)
devs.appendChild(channel) #将channel元素包含在devices元素下面。
hooking.write_domxml(domxml) #将修改后的domxml信息写入临时文件data_filename中
for name in VM_CHANNELS_NAME: #遍历通道列表,创建通道
create_channel(name)
创建一个虚拟机test-window7,然后开启。查看日志文件可以看到hook脚本50_vm_channels被执行:
#vim /var/log/vdsm/vdsm.log 2020-08-17 10:27:33,673+0800 INFO (vm/c6ea26ae) [root] /usr/libexec/vdsm/hooks/before_device_create/openstacknet_utils.py: rc=0 err= (hooks:114) 2020-08-17 10:27:33,810+0800 INFO (vm/c6ea26ae) [root] /usr/libexec/vdsm/hooks/before_vm_start/50_hostedengine: rc=0 err= (hooks:114) 2020-08-17 10:27:33,979+0800 INFO (vm/c6ea26ae) [root] /usr/libexec/vdsm/hooks/before_vm_start/50_nestedvt: rc=0 err= (hooks:114) 2020-08-17 10:27:34,095+0800 INFO (vm/c6ea26ae) [root] /usr/libexec/vdsm/hooks/before_vm_start/50_rmsmbios: rc=0 err= (hooks:114) 2020-08-17 10:27:34,197+0800 INFO (vm/c6ea26ae) [root] /usr/libexec/vdsm/hooks/before_vm_start/50_vhostmd: rc=0 err= (hooks:114) 2020-08-17 10:27:34,298+0800 INFO (vm/c6ea26ae) [root] /usr/libexec/vdsm/hooks/before_vm_start/50_vm3d: rc=0 err= (hooks:114) 2020-08-17 10:27:34,311+0800 INFO (jsonrpc/4) [jsonrpc.JsonRpcServer] RPC call Host.ping2 succeeded in 0.00 seconds (__init__:312) 2020-08-17 10:27:34,449+0800 INFO (vm/c6ea26ae) [root] /usr/libexec/vdsm/hooks/before_vm_start/50_vm_channels: rc=0 err=vm-uuid: c6ea26ae-05d2-4a60-92fe-b15ecde2fcf6 vm-uuid: c6ea26ae-05d2-4a60-92fe-b15ecde2fcf6 (hooks:114)
通过”virsh -r dumpxml test-window7″命令查看,虚拟机test-window7的XML信息如下:
<domain type='kvm' id='97' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>test-window7</name>
<uuid>c6ea26ae-05d2-4a60-92fe-b15ecde2fcf6</uuid>
<maxMemory slots='16' unit='KiB'>33554432</maxMemory>
<memory unit='KiB'>8388608</memory>
<currentMemory unit='KiB'>8388608</currentMemory>
<vcpu placement='static' current='4'>64</vcpu>
<iothreads>1</iothreads>
<resource>
<partition>/machine</partition>
</resource>
<devices>
<emulator>/usr/libexec/qemu-kvm</emulator>
<channel type='unix'>
<source mode='bind' path='/var/lib/libvirt/qemu/channels/c6ea26ae-05d2-4a60-92fe-b15ecde2fcf6.ovirt-guest-agent.0'></source>
<target type='virtio' name='ovirt-guest-agent.0' state='disconnected'></target>
<alias name='channel0'></alias>
<address type='virtio-serial' controller='0' bus='0' port='1'></address>
</channel>
<channel type='unix'>
<source mode='bind' path='/var/lib/libvirt/qemu/channels/c6ea26ae-05d2-4a60-92fe-b15ecde2fcf6.org.qemu.guest_agent.0'></source>
<target type='virtio' name='org.qemu.guest_agent.0' state='disconnected'></target>
<alias name='channel1'></alias>
<address type='virtio-serial' controller='0' bus='0' port='2'></address>
</channel>
<channel type='spicevmc'>
<target type='virtio' name='com.redhat.spice.0' state='disconnected'></target>
<alias name='channel2'></alias>
<address type='virtio-serial' controller='0' bus='0' port='3'></address>
</channel>
<channel type='unix'> #增加的通道tools-guest-agent
<source mode='bind' path='/var/lib/libvirt/qemu/channels/c6ea26ae-05d2-4a60-92fe-b15ecde2fcf6.tools-guest-agent.0'></source>
<target type='virtio' name='tools-guest-agent.0' state='disconnected'></target>
<alias name='channel3'></alias>
<address type='virtio-serial' controller='0' bus='0' port='4'></address>
</channel>
<channel type='unix'> #增加的通道ovirt-client-redirect
<source mode='bind' path='/var/lib/libvirt/qemu/channels/c6ea26ae-05d2-4a60-92fe-b15ecde2fcf6.ovirt-client-redirect.0'></source>
<target type='virtio' name='ovirt-client-redirect.0' state='disconnected'></target>
<alias name='channel4'></alias>
<address type='virtio-serial' controller='0' bus='0' port='5'></address>
</channel>
</devices>
</domain>
由以上输出可以看出虚拟机的XML中增加了两个自定义的通道。
PS:转载文章请注明来源:oVirt中文社区(www.cnovirt.com)
扫码加好友拉你进oVirt技术交流群!
