oVirt中文社区

oVirt中文社区
  • 文档导航
  • 专题
    • oVirt虚拟化基础
    • oVirt安装部署
    • oVirt功能和使用
    • oVirt超融合
    • oVirt二次开发
  • 资料汇总
  • 安装包下载
  • 话题
  • oVirt官网
  • 关于
鹏飞
发表于:2020-8-17 10:58:140次点击

oVirt Hook机制和示例分析

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

oVirt二次开发# hook1# ovirt15# vsdmd1# 虚拟机3
oVirt功能和使用
从vmware vsphere导入虚机到oVirt中
2020-8-10 12:06:08
oVirt安装部署
Centos8.2安装oVirt-engine V4.4.1
2020-9-10 9:28:13
1 条回复 A 作者 M 管理员 E
  1. snakermaster 2020-8-24 9:32:17 1

    已喜欢 已反对
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论

扫码加好友拉你进微信技术交流群!

近期文章

  • oVirt中文社区4.4.10离线版本发布!
  • oVirt4.4.10版本发布通知
  • ovirt 4.4.X 解决证书过期问题
  • oVirt中文社区4.4.9离线版本发布!
  • oVirt4.4.9版本发布通知

Since 2018, Build with ♥ by oVirt中文社区

93 queries 0.3872 s

鲁ICP备19004403号-1

oVirt中文社区

致力于开源虚拟化平台oVirt的研究分享、本地化应用和推广