OpenStack基于Libvirt的虚拟化平台调度实现----Nova虚拟机启动源码实现(1)

时间:2023-02-13 08:42:31

感谢朋友支持本博客,欢迎共同探讨交流,由于能力和时间有限,错误之处在所难免,欢迎指正!
如果转载,请保留作者信息。
博客地址:http://blog.csdn.net/gaoxingnengjisuan
邮箱地址:dong.liu@siat.ac.cn


    Nova通过独立的软件管理模块实现XenServer、Hyper-V和VMWare ESX的调用与管理,同时对于其他的Hypervisor,如KVM、LXC、QEMU、UML和Xen则是通过Libvirt标准接口统一实现,其中KVM是Nova-Compute中Libvirt默认调用的底层虚拟化平台。为了更好地理解在Nova环境下Libvirt是如何管理底层的Hypervisor,先要基本了解Libvirt的体系架构与实现方法。这部分详细内容请见另外一篇博文libvirt学习总结。

    Libvirt是一种实现虚拟化平台能力交互的工具集,它为所支持的Hypervisor提供了一种通用的API接口套件,上层管理平台(如Nova)通过Libvirt来实现对虚拟机的生命周期管理。Libvirt当前支持以下底层虚拟化平台:
    KVM:Linux平台仿真器;
    QEMU:面向各种架构的平台仿真器;
    Xen:面向IA-32、IA-64和PowerPC970架构的虚拟机监控程序;
    LXC:用于操作系统虚拟化的Linux(轻量级)容器;
    OpenVZ:基于Linux内核的操作系统级虚拟化;
    User Mode Linux:面向各种架构的Linux平台仿真器;
    VirtualBox:x86虚拟化虚拟机监控程序;
    ESX、GSX:VMW爱热企业级虚拟化平台;
    VMWare Workstation、VMWare Player:VMWare用户级虚拟化平台;
    Hyper-V:Microsoft虚拟化平台。
    另外,Libvirt支持以Bridging、NAT、VEPA和VN-LINK方式构建虚拟网络,以及支持基于不同制式的IDE/SCSI/USB disks、FibreChannel、LVM、iSCSI、NFS和filesystems存储。因此,Libvirt在功能性、兼容性以及管理等方面的优势十分明显。

    Libvirt对底层虚拟化平台的调用与管理有两种方式:本地管理与远程管理。其中,本地管理模式下、上层管理平台系统、Libvirt、虚拟化平台以及客户虚拟机均不属在同一物理主机节点之上;远程管理模式下,上层管理平台系统与底层虚拟化平台以及客户虚拟机分别不属在不同的物理主机节点上。当所有的组件部署在同一物理节点时,上层管理平台系统通过Libvirt工作,以控制本地域中的所有底层虚拟化软件,该种方式在安全性、可靠性以及可扩展性方面存在一定弊端。因此,相比而言基于Libvirt的远程控制模式可以较好地解决本地管理模式所遇到的问题。

    在Libvirt环境下,一台虚拟机随着用户需求的改变可能会经历如下状态:
    Undefined:一台虚拟机的初始化状态,Libvirt对处于该状态的Guest Domain不执行任何创建或定义的操作;
    Defined:只有持久类型的Domain存在本状态,Guest Domain已经被创建;
    Running:基于某种Hypervisor的Guest Domain虚拟机已经正常运行,可被客户控制并操作;
    Paused:Hypervisor上对该Guest Domain虚拟机执行挂起操作,状态被临时存储直至恢复;
    Saved:类似于Paused状态,不同的是Guest Domain虚拟机的相关状态与数据被永久存储,当接收到客户请求恢复的指令之后,处于Saved状态下的Guest Domain虚拟机被恢复。

    以上简要描述了Libvirt相关的管理能力与技术特征,Nova基于Libvirt在功能方面实现与底层虚拟化平台的无缝兼容。Nova对于底层Hypervisor(如KVM/QEMU等)的调用与管理主要通过LibvirtDriver类(见Nova源代码“nova/virt/libvirt/driver.py”)来实现,该类与类XenAPIDriver、VMwareESXDriver、HyperVDriver类似,均是ComputeDriver基类的一个特殊实现。LibvirtDriver类通过“_conn = property(_get_connection)”语句定义了一个连接属性,在执行虚拟机管理相关的操作时,调用LibvirtDriver实例的这个属性的同时,就意味着执行_get_connection方法,打开底层Hypervisor的连接。

    Libvirt对Hypervisor的连接有两种方式:一种是只读式;另一种是认证式。只读式的连接只能对Hypervisor进行读访问,所允许的API调用集合有限,比较适合监控性的应用程序;认证式的连接能够经过认证对Hypervisor进行读写访问,但是需要包含一个认证参数才能执行相关的写操作。LibvirtConnection类中虽然能根据“ReadOnly”条件来选择其中一种方式,但是实际执行Nova时,由于“ReadOnly”参数的默认值是False,并且在建立Libvirt链接时并无修改,因此系统连接方式是认证是连接。Nova通过Libvirt建立于底层Hypervisor认证式连接的关键语句是“libvirt.openAuth(uri, auth, 0)”。其中,Libvirt作为一个Python模块,通过它能够调用Libvirt库中与Hypervisor相关的管理API;“uri”参数主要是连接到Hypervisor的入口地址,如系统要求连接到本地的Xen Hypervisor,其本地URI是“xen:///”;“auth”参数是提供相应的认证信息,进一步实现Libvirt对Hypervisor的读写操作。
    以下选取创建虚拟机的进程具体说明Nova如何基于Libvirt实现对底层虚拟化平台的调用与管理。在LibvirtConnection类中,通过“spawn”方法来实现虚拟机的创建,“spawn”调用方法_create_domain_and_network来启动一个新的Domain,在方法_create_domain_and_network中,实际与Hypervisor建立连接的语句是“domain = self._conn.defineXML(xml)”;在建立与Hypervisor连接的基础上执行defineXML方法,创建并定义一个Domain,但是此时的Domain还未启动,必须执行“domain.createWithFlags(launch_flags)”语句来启动之前定义好的虚拟机Domain。

    下面来详细的解析方法spawn,来看看OpenStack Nova中是如何应用Libvirt库来启动虚拟机的。

    我们来看方法spawn:

def spawn(self, context, instance, image_meta, injected_files,
admin_password, network_info=None, block_device_info=None):
"""
# 调用之一传进来的参数:
# context:上下文信息;
# instance:实例信息;
# image_meta:从glance获取的镜像文件image的元数据;
# injected_files:编码后的注入文件;
# admin_password:admin密码;
# network_info=self._legacy_nw_info(network_info):转换为传统格式的网络资源信息;
# block_device_info:实例错误记录的块设备;
"""

# get_disk_info:确定来宾系统的磁盘映射信息;
# 返回disk_bus、cdrom_bus和mapping的值这些磁盘映射信息给disk_info;;
# CONF.libvirt_type:这个参数定义了libvirt的域名类型,参数的默认值为'kvm';

# get_disk_info返回值:
# return {'disk_bus': disk_bus, # 获取disk类型的磁盘总线;
# 'cdrom_bus': cdrom_bus, # 获取cdrom类型的磁盘总线;
# 'mapping': mapping} # 确定怎样映射从默认的磁盘到虚拟机,返回客户对于设备的磁盘映射;
disk_info = blockinfo.get_disk_info(CONF.libvirt_type,
instance,
block_device_info,
image_meta)

# to_xml:为新建立的实例参数获取配置数据conf,并把获取的数据conf转换为xml格式;
# instance:实例信息;
# network_info:转换为传统格式的网络资源信息;
# disk_info:来宾系统的磁盘映射信息;
# image_meta:从glance获取的镜像文件image的元数据;
# block_device_info:实例错误记录的块设备;
xml = self.to_xml(instance, network_info,
disk_info, image_meta,
block_device_info=block_device_info)

# _create_image:建立虚拟机实例镜像;
# context:上下文信息;
# instance:实例信息;
# xml:为新建立的实例参数获取配置数据conf,并把获取的数据conf转换为xml格式;
# disk_info['mapping']:来宾系统磁盘的映射信息;
# network_info=network_info:转换为传统格式的网络资源信息;
# block_device_info=block_device_info:实例错误记录的块设备;
# files=injected_files:编码后的注入文件;
# admin_pass=admin_password:admin密码;
self._create_image(context, instance, xml,
disk_info['mapping'],
network_info=network_info,
block_device_info=block_device_info,
files=injected_files,
admin_pass=admin_password)

# 执行所需网络的安装以及建立域;
self._create_domain_and_network(xml, instance, network_info,block_device_info)
LOG.debug(_("Instance is running"), instance=instance)

def _wait_for_boot():
"""
在固定时间间隔调用,检测虚拟机启动状态,直到虚拟机成功运行;
"""
state = self.get_info(instance)['state']

if state == power_state.RUNNING:
LOG.info(_("Instance spawned successfully."),instance=instance)
raise utils.LoopingCallDone()

# _wait_for_boot:在固定时间间隔调用,检测虚拟机启动状态,直到虚拟机成功运行;
# 以一定的时间间隔(0.5)循环调用_wait_for_boot方法;
timer = utils.FixedIntervalLoopingCall(_wait_for_boot)
timer.start(interval=0.5).wait()
    首先来看方法get_disk_info,这个方法实现了确定来宾系统的磁盘映射信息,返回disk_bus、cdrom_bus和mapping的值这些磁盘映射信息给disk_info。具体的方法解析见方法中的注释信息:

def get_disk_info(virt_type, instance, block_device_info=None,
image_meta=None, rescue=False):
"""
确定来宾系统的磁盘映射信息;
返回disk_bus、cdrom_bus和mapping的值;

# 调用之一传进来的参数;
# virt_type=CONF.libvirt_type:这个参数定义了libvirt的域名类型,参数的默认值为'kvm';
# instance:实例信息;
# block_device_info:实例错误记录的块设备;
# image_meta:从glance获取的镜像文件image的元数据;
# rescue=False;
"""

# get_disk_bus_for_device_type:获取虚拟机设备最佳的磁盘总线接口类型信息;
# 首先尝试根据虚拟机元数据获取虚拟机磁盘总线接口类型,并判断它和虚拟机类型是否正确对应;
# 如果成功获取虚拟机的磁盘总线接口类型,并且验证正确,直接返回它;
# 如果在虚拟机元数据中没有获取到磁盘总线接口类型信息,则根据虚拟机类型,直接获取对应的最佳的磁盘总线接口类型值,并返回它;
# 考虑到当前配置的虚拟化类型,为给定的设备类型返回最佳的disk_bus(磁盘总线);
# 比如,对于KVM的disk,它会返回virtio,而对KVM的cdrom,它会返回ide;

# virt_type:定义了libvirt的域名类型,参数的默认值为'kvm';
# image_meta:从glance获取的镜像文件image的元数据;

# 获取disk类型的磁盘总线;
disk_bus = get_disk_bus_for_device_type(virt_type, image_meta, "disk")
# 获取cdrom类型的磁盘总线;
cdrom_bus = get_disk_bus_for_device_type(virt_type, image_meta, "cdrom")

# get_disk_mapping:确定怎样映射从默认的磁盘到虚拟机;
# 这里是计算出默认的'disk','disk.local', 'disk.swap'和'disk.config'镜像是否已经被块设备映射重写;
# 返回客户对于设备的磁盘映射;
# virt_type:定义了libvirt的域名类型,参数的默认值为'kvm';
# instance:实例信息;
# disk_bus:获取得到的disk类型的磁盘总线;
# cdrom_bus:获取得到的cdrom类型的磁盘总线;
# block_device_info:实例错误记录的块设备;
# image_meta:从glance获取的镜像文件image的元数据;
# rescue=False;
mapping = get_disk_mapping(virt_type, instance,
disk_bus, cdrom_bus,
block_device_info,
image_meta, rescue)

return {'disk_bus': disk_bus, # 获取disk类型的磁盘总线;
'cdrom_bus': cdrom_bus, # 获取cdrom类型的磁盘总线;
'mapping': mapping} # 确定怎样映射从默认的磁盘到虚拟机,返回客户对于设备的磁盘映射;
    接下来调用方法to_xml实现为新建立的虚拟机实例获取参数配置数据conf,并把获取的配置数据conf转换为xml格式文件,方法的具体实现如下:

def to_xml(self, instance, network_info, disk_info,
image_meta=None, rescue=None,
block_device_info=None, write_to_disk=False):
"""
为新建立的实例参数获取配置数据conf,并把获取的数据conf转换为xml格式;
# 调用之一传进来的参数:
# instance:实例信息;
# network_info:转换为传统格式的网络资源信息;
# disk_info:来宾系统的磁盘映射信息;
# image_meta:从glance获取的镜像文件image的元数据;
# rescue=None;
# block_device_info:实例错误记录的块设备;
# write_to_disk=False;
"""
LOG.debug(_("Start to_xml instance=%(instance)s "
"network_info=%(network_info)s "
"disk_info=%(disk_info)s "
"image_meta=%(image_meta)s rescue=%(rescue)s"
"block_device_info=%(block_device_info)s") %
locals())

# get_guest_config:为新建的实例参数获取配置数据;
# instance:实例信息;
# network_info:转换为传统格式的网络资源信息;
# image_meta:从glance获取的镜像文件image的元数据;
# disk_info:来宾系统的磁盘映射信息;
# rescue=None;
# block_device_info:实例错误记录的块设备;
conf = self.get_guest_config(instance, network_info, image_meta, disk_info, rescue, block_device_info)

# 调用to_xml把conf(实例的配置数据)生成xml格式文件;
xml = conf.to_xml()

#注:这里还不知道具体的是什么意思;
if write_to_disk:
# 确定实例存储的正确的路径;
instance_dir = libvirt_utils.get_instance_path(instance)
# 确定xml文件路径,文件名称为libvirt.xml;
xml_path = os.path.join(instance_dir, 'libvirt.xml')
# 写前面获得的xml文件内容到libvirt.xml之中;
libvirt_utils.write_to_file(xml_path, xml)

LOG.debug(_('End to_xml instance=%(instance)s xml=%(xml)s') % locals())
return xml

    接下来调用一个比较重要的方法_create_image来实现虚拟机镜像的建立,需要注意的是这个方法中还没有启动所建立的虚拟机实例。代码的详细解析请见 OpenStack基于Libvirt的虚拟化平台调度实现----Nova虚拟机启动源码实现(2)