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技术交流群!
✗棒棒的✗