Ovirt(openVirtualization)是一个基于KVM的开源IaaS项目,是redhat虚拟化管理平台RHEV的开源版本,其前身是Redhat的虚拟化商业产品。在架构设计上它使用了Node/Engine分离结构,以方便功能的划分与管理。
图1.oVirt逻辑结构图
图1从总体框架上说明了oVirt的工作流程。对外,oVirt中的Engine通过HTTP协议向外提供HTTP API,同时提供内建的网页服务供用户和系统管理员使用。系统管理员通过网页可以创建、修改虚拟机及相关设备或用户权限,用户在拥有权限的情况下可以操作自己的虚拟机,并通过VNC或SSH登陆自己的虚拟机。Engine在整个系统中充当管理者的角色并对外提供管理服务,它挂载了自己的数据库记录整个系统中所有的虚拟机配置,各个节点的自身状态,系统的网络状态,存储器状态。管理的逻辑,状态及策略全部在Engine中设置与实现。Node只负责功能上的实现,不进行任何状态的记录和任何策略的实现。Engine与Node之间的关系十分像Linux中驱动程序与应用程序的功能分割关系:驱动仅仅负责功能的实现,如设备的读、写、开启与关闭,如何使用这些功能留给应用层。同样Node仅仅负责实现虚拟机器与设备的创建与修改,资源的共享与保护,如何使用这些功能交给Engine处理。Node暴露两种基于网络的API与Engine交互,XMLRPC与REST。Engine通过这些接口控制各个Node上功能的启动。当然用户也可以调用这些API进行第三方程序的开发。oVirt里的Node可以由一个普通的Linux上安装VDSM(VirtualDesktop Server Manager) 构成,也可以由一个专为oVirt定制的Linux系统构成。在定制的情况下,Node上的许多文件系统都是ramdisk(基于内存的Linux磁盘设备),系统重启后其中的内容消失,从而保证了Node的无状态性。Engine/Node的设计不仅方便将来的开发,更简化了用户的安装使用,在定制的情况下Node可以快速大量部署。
图2.Node上运行的主要组件
简单概括起来,VDSM的功能主要有:负责Node的自动启动与注册;虚拟机的操作与生命周期管理;网络管理;存储管理;Host与VM状态监视与报告;提供对虚拟机的外部干涉功能;提供内存与存储的合并与超支功能(OverCommitment)。
一个标准的oVirt部署包括三部分,主要有:
1.ovirt-engine,这个被用于部署,监管,移除,停止和创建VM镜像,配置存储,网络,等等。
2.一个或多个ovirt-node,我们在node上运行虚拟机(VMs)。
3.一个或多个存储节点,其上保存着对应虚拟机的磁盘镜像和ISO镜像。
Ovirt-node上安装了VDSM和libvirt以及安装了一些额外的包,这些包可以实现网络和其他系统服务的虚拟化。
存储节点可以使用块和文件存储,并且可以通过NFS在本地或者远程进行访问。通过POSIXFS存储类型是可以支持Gluster这样的存储技术的。存储节点被分在一个存储池中,这样可以保证高可用和冗余。
oVirtengine是一个基于JBoss的java应用程序(由C#演变过来),它作为web服务来运行。这个服务直接指挥node上的VDSM去部署,启动,停止,迁移和监控虚拟机,以及从模板中创建新的镜像至存储中。
engine提供的一些功能:
虚拟机生命周期管理
通过LDAP(微软的AD或IPA)进行身份验证
网络管理-增加本地网络并且将其附加至主机上。
存储管理-管理存储域(NFS/iSCSI/Local)和虚拟VM磁盘。
高可用-在其它主机中失败的主机上自动重启客户虚拟机
在线迁移-在零停机的情况下移动主机之间的虚拟机。
系统调度-基于资源使用/政策持续负载均衡虚拟机。
节能优化-在非高峰时期将续集你集中到较少的服务器上。
维护管理器-在计划的维护期间,无需为虚拟机停机。
镜像管理-基于模板管理,自动精简配置和快照。
监控-为系统中的所有对象-虚拟客户机,主机,网络,存储等等。
导出/导入-使用OVF文件导入和导出虚拟机和模板。
V2V-可以将VMware或RHEL或Xen环境下的虚拟机轻松转换到oVirt环境中
图3展示了OvirtEngine组件的不同层次:
图3.OvirtEngine组件层次
下面的图形显示了Engine-core的不同组件
enginecore中的主要组件有:
-负责所有与之相关的DB操作。
VDS Broker -负责所有需要与VDSM交互的操作。
LDAP Broker -负责验证和抓取LDAP目录下用户/组的属性(当前支持AD,IPA和RHDS)
Backend Bean-一个Singletonbean ,负责运行不同实体的指令,查询和监控。
VDSM是一个以Python开发的组件,它可以为主机,虚拟机,网络和存储管理提供所需要的所有功能。
VDSMAPI是基于XML-RPC的(计划转移到RESTAPI)。这就是ovirt-engine如何与VDSM进行通信的。
配置主机,网络和共享的存储。
使用libvirt,进行虚拟机的生命周期操作
多线程,多进程
通过virtio-serial和客户代理对话。
添加自定义的对扩展到数以百计的node的LVM的集群支持
在所支持的存储类型(本地目录,FCP, FCoE, iSCSI, NFS, SAS)的基础上实现一个分布式镜像库。
多主机系统,一个并发的元数据写入器。
在数据写入器中进行扩展。
允许管理员定义修改虚拟机操作的脚本,例如,添加额外的操作像CPU定位,watchdog设备,直接访问LUN
允许在集成完成前为新的KVM特性扩展oVirt。
有一种简单的方式来测试新的kvm/libvirt/linux特性。
在VDSM利用libvirt开始启动虚拟机之前调用hook机制。
hook改变了虚拟机定义,并且VDSM通过定义至libvirt以开启虚拟机。
下图阐释了虚拟机生命周期中Hook机制。
VDSM是和MoM集成在一起的。MOM的行为配置了策略。随着这些策略的使用者可以微调主机的高内存过量使用或安全操作。为了控制它的mom实例,vdsm确实传递了一个mom配置文件并且mom策略文件可以设置mom的缺省行为。刚开始,vdsmd利用配置和策略文件导入了mom并将其初始化。从那时起,mom通过API.py中定义好的API与vdsm进行交互并且控制了运行在主机上的每个虚拟机的内存。mom实例作为vdsm守护进程内的线程运行。
存储器的管理是oVirt的重点,本节将介绍oVirt是如何组织与分配各种存储器的,用户应该如何使用它。每一个Node上都会运行一个VDSM,实现网络、存储器、虚拟机的创建与修改的功能。VDSM的大部分代码用在了存储系统上,其功能包括数据的组织,集群下的数据共享与保护,故障恢复。通常情况下每一个物理机器当作一个Node,运行一个VDSM,Node本身只携带少量存储器用以保存配置。一个集群中通常有一个Engine和数个Node,这些Node通过网络连接到SAN(StorageArea Network) 上,VDSM把Node上运行的虚拟机存储数据保存在SAN上,Node本身为无状态的节点,重新启动后状态消失,从而保证了系统整体的可用性,一般情况下不会因用户的操作而使Node失效。一旦问题发生,通常一次重启即可恢复工作状态。
在云计算环境中,SAN中往往存储着大量虚拟机器使用的virtualimage,同时每一个virtualimage在任何时候都可能被任意Node访问,同时出于性能的考虑virtualimage可能以文件或者数据块的形式出现,这些对存储系统的设计提出了挑战。
为此,VDSM基于以下原则设计了自己的存储系统:
1高可用性:一群安装有VDSM的Node在组建集群的时候,没有潜在的单点故障存在,任何一个Node崩溃不会影响整个集群的功能,它的角色会被其他Node取代。Engine不可用的情况下,Node将继续工作,用户对虚拟机的操作可以继续进行。
2高伸缩性:添加Node和SAN几乎不需要用户的设置,Node上的VDSM会自己注册自己。
3集群安全性:一个VDSM对正在操作的virtualimage进行排它性保护。
4备份与恢复:virtualimage之间有相互关连的特性记录可进行一系列引用/备份操作。
5性能优化:利用多线程与多进程减少操作堵塞状况。
StorageDomain(以下简程SD)是VDSM中的最基本存储实体,所有的virtualimage和virtualimage对应的元数据都会保存在其中。和VDSM中的StorageImage概念不同,这里的virtualimage表示的是虚拟机程序用到的虚拟磁盘数据,特指虚拟机程序最终能够操作的文件或设备文件对象。元数据是描述virtualimage相关数据大小、状态、锁等内容的一组数据集合。SD包括两种类型:FileDomain和BlockDomain。FileDomain使用文件系统存储数据并同步操作,主要针对NFS(NetworkFile System) 和LOCALFS(LocalFile System) 文件系统。在文件系统的帮助下,FileDomain拥有良好的virtualimage操作能力,每一个虚拟机的存储数据(称为Volume)和对应的元数据都以文件的方式保存。每一个Domain实际对应于Host文件系统上的一个目录,针对NFS文件系统VDSM还有额外的逻辑来处理相关意外与错误情况。而BlockDomain直接操作原始的块数据,使用Linux的LVM(LogicalVolume Manager) 功能来组织数据,主要针对iSCSI(InternetSmall Computer System Interface),FCoE(FibreChannel over Ethernet ) 等块设备。由于目标设备上通常没有一个文件系统来保证访问的安全性,VDSM使用了邮箱机制来保证任意时刻,只有一个Node可以修改Block上的内容,而其他Node则通过Socket邮箱发送自己的修改请求。因此它的操作请求速度和监视功能都会比FileDomain弱一些。通常设备将使用Linux的devicemapper机制进行一次映射,每一个Domain实际上是一个Linux中的VolumeGroup,元数据保存在其中的一个LogicVolume及其tag上,虚拟机的Volume保存在另一个LogicVolume中。
StoragePool(以下简称SP)是一组SD的组合,目标是管理跨越SD之间的操作,也就是说SD之间互相的引用、备份、恢复,合并一般发生在一个SP之中。在数据中心里,一个SP抽象了一组SD的集合供外界的Node访问或者Engine管理,并且一个SP中的所有SD必须是同一类型,如NFS或者iSCSI。
为了保证SP中的数据安全,一组SP中需要选择一个SD作为MasterDomain。这个Domain的不同之处在于它会保存SP中所有的元数据,保存一些异步请求或者任务的数据,保存所在SP的集群存储用到的锁。
为了简化管理,oVirt中抽象出了DataCenter概念,一个DataCenter将拥有一组NodeCluster用来运行虚拟机,一个StoragePool用来保存虚拟磁盘数据。NodeCluster是一组专门用来运行虚拟机的Node的集合,运行在其中的虚拟机可以动态迁移到NodeCluster中的另外一个Node上。一个DataCenter是一个完成oVirt所有功能的实体,在这个DataCenter中用户可以创建虚拟机、备份虚拟机、配置虚拟机的StorageDomain,动态迁移虚拟机。NodeEngine有一些算法在开启的时候可以自动平衡DataCenter中的Node的负载。概括起来一个DataCenter是一个管理NodeCluster与StoragePool的集合。
由于DataCenter中所有的Node都拥有对DataCenter中的StoragePool的访问权限,因此VDSM实现了一个称为SPM(StoragePool Manager) 的功能角色。在一个DataCenter中,所有的Node启动后会自动选举出一个Node充当SPM的角色,被选举者将运行VDSM上的SPM逻辑,负责完成以下功能:创建/删除/缩放所在DataCenter中的Image,快照,模板。这些操作的共同点是会影响StoragePool中的元数据,如SAN上松散块设备的分配。为了保证元数据不被多个Node同时修改,SPM拥有对StoragePool中元数据的排它性操作权限,SPM使用集中式邮箱接受其他Node的相关请求,其他Node只能通过给SPM发送操作请求的方式修改元数据,最终的操作都由SPM线性完成,从而避免了存储器操作竞态的出现。为了兼顾效率,不修改元数据的普通操作,如数据读写,Node可以不同过SPM,自己直接访问StoragePool完成。由于SPM是由一个普通Node选举出来的,因此当它因为外部原因失效后,系统将会选举出另外的Node充当SPM,从而保证系统能继续运行。
4.Ovirt guest agent
Ovirt-guest-agent是一个python编写的运行在虚拟机内部的守护程序(可执行文件为/usr/share/ovirt-guest-agent/ovirt-guest-agent.py),通过宿主机的VDSM为虚拟化管理器(ovirt-engine或rhev-m)提供虚拟机的信息,在虚拟机上通过虚拟串口virtio-serial(默认首选方式)或者isa-serial使用json协议与宿主机上的unixsocket文件进行交互。
Engine将发给guestagent的请求重定向到VDSM,VDSM通过 VirtIO通道使用JSON格式的纯文本命令与guest通信。
VDSM/GuestAgent消息结构
VDSM发送的每条信息都有一个必填字段:'__name__',它包含了指令名。
Alladditional fields in the message are considered arguments to thecommand. Please see the OVirt_Guest_Agent/CommandDefinitions pagefor detailed information about supported commands and theirarguments.
Ovirt-guest-agent通过读写串口设备与宿主机上的socket通道进行交互,宿主机上可以使用普通的unixsocket读写方式对socket文件进行读写,最终实现与Ovirt-guest-agent的交互,交互的协议与qmp(QEMUMonitor Protocol)相同(简单来说就是使用JSON格式进行数据交换),串口设备的速率通常都较低,所以比较适合小数据量的交换。
xml文件中配置方式如下:
<channel type='unix'>
<sourcemode='bind'path='/var/lib/libvirt/qemu/channels/1aea6926-34c2-470d-ac0a d8de68547a2f.com.redhat.rhevm.vdsm'/>
<target type='virtio' name='com.redhat.rhevm.vdsm'/>
</channel>
通过上面的参数就可以在宿主机上生成一个unixsocket文件,socket文件的路径为:/var/lib/libvirt/qemu/channels/1aea6926-34c2-470d-ac0a-d8de68547a2f.com.redhat.rhevm.vdsm,同时在虚拟机内部生成一个serial设备,名字为vport0p1,设备路径为:/dev/vport0p1,映射出来的可读性比较好的路径为:/dev/virtio-ports/vport0p1,可以在运行ovirt-guest-agent的时候通过-p参数指定读写这个设备。
目前,ovirt-guest-agent可以运行在以下系统上:
RedHat Enterprise Linux 5.x
RedHat Enterprise Linux 6.
CentOS6.x
Fedora16-20
Ubuntu12.04+
openSUSE12.x+
WindowsXP (32)
Windows7 (32/64)
Windows8.x (32/64)
Windows2003 (32/64/R2)
Windows2008 (32/64/R2)
Windows2012 (32/64/R2)
对于linux虚拟机来说,guestagent需要python和一个虚拟串口设备,对于windows系统来说,guestagent需要python和安装pywin32包
guestagent是运行在虚拟机后台的一个应用程序,它提供了虚拟机的一些信息和一些无法从虚拟机操作系统外部来执行的预设操作。
guestagent提供虚拟机的如下信息:
●Machinename -显示虚拟机的主机名.
●Operatingsystem version -显示操作系统的版本。对于Linux:这个值指的是内核版本。对于Windows:这个指指的是Windows版本的名称(如WindowsXP或Windows7)。
●Installedapplications -列出所安装的应用程序。对于Linux:应用程序列表是通过配置文件设置的,对于Windows:安装应用程序的列表是基于注册表来显示的。
●AvailableRAM -未使用的物理存储的总数。
●Loggedin users -列出登录的用户。
●Activeuser-列出当前正在使用虚拟机“物理硬件”的用户。
●DisksUsage-虚拟机磁盘的使用信息.
●Network-interfaces-虚拟机网络接口信息(MAC,name, ipv4, ipv6).
●FQDN(FullyQualified Domain Name)-报告配置的虚拟机的全称域名
guestagent会在如下事件发生时发出通知:
●PowerUp – guest agent开始运行时.
●Power Down - guest agent停用时
●Heartbeat–每隔几秒钟发送一个心跳包,通知管理端guestagent正在运行。这个通知还包括了虚拟机可用的RAM信息
●UserInfo-在线用户更改时
●SessionLock-桌面被锁定时(Windows).
●SessionUnlock-桌面解锁时(Windows).
●SessionLogoff-用户注销时(Windows).
●SessionLogon-用户登入时(Windows).
●AgentUninstalled– guest agent被卸载时– 通知VDSM清空缓存
以下操作可以通过guestagent实现
●Lockscreen-锁定桌面.
●Login-执行用户的登录操作
在RHEL下自动登录
在Windows下自动登录
●Logoff-注销在线的用户
●Shutdown-关闭虚拟机
5.Ovirt guest agent主要代码分析
Ovirt guestagent入口程序:/usr/share/ovirt-guest-agent/ovirt-guest-agent.py
程序启动后在OVirtAgentLogic.py中启动两个线程,doListen线程负责监听和执行VDSM发来的指令,doWork线程负责定时获取虚拟机信息并发送给VDSM:
thread.start_new_thread(self.doListen,())
thread.start_new_thread(self.doWork,())
监听线程首先从虚拟串口中读取发来的命令:
cmd, args =self.vio.read()
此函数的最终实现在VirtioChannel.py中:
def_os_read(self, size):
returnos.read(self._vport, size)
获得命令后对命令进行解析:
self.parseCommand(cmd, args)
解析的代码如下(OVirtAgentLogic.py中):
defparseCommand(self, command, args):
logging.info("Received an external command: %s..." %(command))
ifcommand == 'lock-screen':
self.commandHandler.lock_screen()
elifcommand == 'log-off':
self.commandHandler.logoff()
elifcommand == 'api-version':
self._onApiVersion(args)
elifcommand == 'shutdown':
try:
timeout = int(args['timeout'])
except:
timeout = 0
try:
msg = args['message']
except:
msg = 'System is going down'
try:
reboot = args['reboot'].lower() == 'true'
except:
reboot = False
action = 'Shutting down'
ifreboot:
action = 'Rebooting'
logging.info("%s (timeout = %d, message = '%s')"
% (action, timeout, msg))
self.commandHandler.shutdown(timeout, msg, reboot)
elifcommand == 'login':
username = args['username'].encode('utf8')
password = args['password'].encode('utf8')
credentials = struct.pack(
'>I%ds%ds' % (len(username), len(password) + 1),
len(username), username, password)
logging.debug("User log-in (credentials = %s)"
% (safe_creds_repr(credentials)))
self.commandHandler.login(credentials)
elifcommand == 'refresh':
if'apiVersion' not in args and self.dr.getAPIVersion() > 0:
logging.info('API versioning not supported by VDSM. Disabling '
'versioning support.')
self.dr.setAPIVersion(_DISABLED_API_VALUE)
self.sendUserInfo(True)
self.sendAppList()
self.sendInfo()
self.sendDisksUsages()
self.sendFQDN()
elifcommand == 'echo':
logging.debug("Echo: %s", args)
self._send('echo', args)
elifcommand == 'hibernate':
state = args.get('state', 'disk')
self.commandHandler.hibernate(state)
elifcommand == 'set-number-of-cpus':
count = args.get('count', 0)
ifcount > 0:
self.commandHandler.set_number_of_cpus(count)
self.sendNumberOfCPUs()
else:
logging.error("Unknown external command: %s (%s)"
% (command, args))
1.lock-screen:
if command =='lock-screen':
self.commandHandler.lock_screen()
在GuestAgentLinux2.py的CommandHandlerLinux类中:
deflock_screen(self):
cmd =['/usr/share/ovirt-guest-agent/ovirt-locksession']
logging.debug("Executing lock session command: '%s'",cmd)
subprocess.call(cmd)
而ovirt-locksession->/usr/bin/consolehelper
所以是执行一个文件实现的。
其他的命令也类似
由此可知,要想加入新功能,比如修改密码等,就在OVirtAgentLogic.py中的parseCommand函数中相应的添加一种情况elifcommad==……,同时在GuestAgentLinux2.py的CommandHandlerLinux类中添加执行相应操作的函数。
doWork执行的代码如下:
def doWork(self):
logging.debug("AgentLogicBase:: doWork() entered")
self.sendInfo()
self.sendUserInfo()
self.sendAppList()
self.sendFQDN()
counter= 0
hbsecs= self.heartBitRate
appsecs = self.appRefreshRate
disksecs = self.disksRefreshRate
usersecs = self.userCheckRate
numcpusecs = self.numCPUsCheckRate
try:
while not self.wait_stop.isSet():
counter += 1
hbsecs -= 1
if hbsecs <= 0:
self._send('heartbeat',
{'free-ram': self.dr.getAvailableRAM(),
'memory-stat': self.dr.getMemoryStats(),
'apiVersion': _MAX_SUPPORTED_API_VERSION})
hbsecs = self.heartBitRate
usersecs -= 1
if usersecs <= 0:
self.sendUserInfo()
usersecs = self.userCheckRate
appsecs -= 1
if appsecs <= 0:
self.sendAppList()
self.sendInfo()
self.sendFQDN()
appsecs = self.appRefreshRate
disksecs -= 1
if disksecs <= 0:
self.sendDisksUsages()
disksecs = self.disksRefreshRate
numcpusecs -= 1
if numcpusecs <= 0:
self.sendNumberOfCPUs()
numcpusecs = self.numCPUsCheckRate
time.sleep(1)
logging.debug("AgentLogicBase:: doWork() exiting")
except:
logging.exception("AgentLogicBase::doWork")
由此代码可知,当心跳时间、app时间、磁盘时间、用户时间、cpu个数时间的倒数计时为0时即通过串口向VDSM发送信息,发送信息的代码如下:
def _send(self,name, arguments=None):
version= _MESSAGE_MIN_API_VERSION.get(name, None)
ifversion is None:
logging.error('Undocumented message "%s"', name)
elifversion <= self.dr.getAPIVersion():
self.vio.write(name, arguments or {})
else:
logging.debug("Message %s not supported by api version %d.",
name, self.dr.getAPIVersion())
获取各种信息的代码如下:
1.心跳包中的AvailableRAM:
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def getAvailableRAM(self):
free = 0
for line inopen('/proc/meminfo'):
var, value =line.strip().split()[0:2]
if var in ('MemFree:','Buffers:', 'Cached:'):
free += long(value)
return str(free / 1024)
由此可见,获取虚拟机中的空闲RAM信息是通过读取虚拟机中/proc/meminfo文件获得的。
2.心跳包中的MemoryStats:
在GuestAgentLinux2.py的LinuxDataRetriver类中:
defgetMemoryStats(self):
try:
self._get_meminfo()
self._get_vmstat()
except:
logging.exception("Error retrieving memory stats.")
returnself.memStats
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def _get_meminfo(self):
fields = {'MemTotal:': 0, 'MemFree:': 0, 'Buffers:': 0,
'Cached:': 0, 'SwapFree:': 0, 'SwapTotal:': 0}
free = 0
for line in open('/proc/meminfo'):
(key, value) = line.strip().split()[0:2]
if key in fields.keys():
fields[key] = int(value)
if key in ('MemFree:', 'Buffers:', 'Cached:'):
free += int(value)
self.memStats['mem_total'] = fields['MemTotal:']
self.memStats['mem_unused'] = fields['MemFree:']
self.memStats['mem_free'] = free
self.memStats['mem_buffers'] = fields['Buffers:']
self.memStats['mem_cached'] = fields['Cached:']
swap_used = fields['SwapTotal:'] - fields['SwapFree:']
self.memStats['swap_usage'] = swap_used
self.memStats['swap_total'] = fields['SwapTotal:']
由此可知获取虚拟机中的虚拟机内存等信息是通过读取虚拟机中/proc/meminfo文件获得的。
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def _get_vmstat(self):
fields = {'pswpin': 'swap_in', 'pswpout': 'swap_out',
'pgfault': 'pageflt', 'pgmajfault': 'majflt'}
self.vmstat['timestamp_cur'] = time.time()
interval = self.vmstat['timestamp_cur'] -self.vmstat['timestamp_prev']
self.vmstat['timestamp_prev'] =self.vmstat['timestamp_cur']
for line in open('/proc/vmstat'):
(key, value) = line.strip().split()[0:2]
if key in fields.keys():
name = fields[key]
self.vmstat[name + '_prev'] = self.vmstat[name +'_cur']
self.vmstat[name + '_cur'] = int(value)
if self.vmstat[name + '_prev'] is None:
self.vmstat[name + '_prev'] =self.vmstat[name + '_cur']
self.memStats[name] = int((self.vmstat[name +'_cur'] -
self.vmstat[name +'_prev']) /
interval)
由此可知获取虚拟机中的虚拟机状态等信息是通过读取虚拟机中/proc/vmstat文件获得的。
3.UserInfo
在OVirtAgentLogic.py的AgentLogicBase中:
defsendUserInfo(self, force=False):
cur_user = self.dr.getActiveUser()
logging.debug("AgentLogicBase::sendUserInfo - cur_user ='%s'" %
(cur_user))
ifcur_user != self.activeUser or force:
self._send('active-user', {'name': cur_user})
self.activeUser = cur_user
在GuestAgentLinux2.py的LinuxDataRetriver类中:
defgetActiveUser(self):
users =os.popen('/usr/bin/users').read().split()
try:
user = users[0]
except:
user = 'None'
returnuser
由此可知获取虚拟机中的Activeuser的信息是通过读取虚拟机中/usr/bin/users文件获得的。
4.InstalledApplications:
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendAppList(self):
self._send('applications',{'applications':self.dr.getApplications()})
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def getApplications(self):
returnself.list_pkgs(self.app_list)
self.dr.app_list =config.get("general","applications_list")
def list_pkgs(self, app_list)
apps = set()
try:
if self.rpm:
apps.update(self.rpm_list_packages(app_list))
if self.apt_pkg:
apps.update(self.apt_list_packages(app_list))
except Exception:
logging.exception("Failed to get package list")
apps = list(apps)
logging.debug("PkgMgr:list_pkgs returns [%s]" % (str(apps)))
return apps
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def apt_list_packages(self,app_list):
INSTALLED_STATE =self.apt_pkg.CURSTATE_INSTALLED
apps = set()
cache = self.apt_pkg.Cache()
for app in app_list.split():
if app in cache:
pkg = cache[app]
# Normal package
ifpkg.current_state == INSTALLED_STATE:
detail = (app,pkg.current_ver.ver_str)
apps.add("%s-%s" % (detail))
# virtual package
eliflen(pkg.provides_list) > 0:
for _, _, pkgin pkg.provides_list:
ifpkg.parent_pkg.current_state == INSTALLED_STATE:
detail= (app, pkg.parent_pkg.current_ver.ver_str)
apps.add("%s-%s" % (detail))
return apps
由此可见,获取虚拟机中安装的应用程序信息是通过读取配置文件获得的。
5.操作系统、机器名、网络接口等信息:
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendInfo(self):
self._send('host-name', {'name': self.dr.getMachineName()})
self._send('os-version', {'version':self.dr.getOsVersion()})
self._send('network-interfaces',
{'interfaces':self.dr.getAllNetworkInterfaces()})
在GuestAgentLinux2.py的LinuxDataRetriver类中:
defgetMachineName(self):
returnsocket.getfqdn()
由此可见,获取虚拟机名是通过python的socket.getfqdn()函数获得的。
defgetOsVersion(self):
returnos.uname()[2]
由此可见,获取操作系统版本是通过python的os.uname()[2]函数获得的。
defgetAllNetworkInterfaces(self):
returnself.list_nics()
self.list_nics=nicmgr.list_nics
在GuestAgentLinux2.py的NicMgr类中:
self.list_nics=self.ethtool_list_nics
defethtool_list_nics(self):
interfaces = list()
try:
for dev in self.ethtool.get_devices():
flags = self.ethtool.get_flags(dev)
if flags & self.ethtool.IFF_UP and \
not(flags &self.ethtool.IFF_LOOPBACK):
devinfo =self.ethtool.get_interfaces_info(dev)[0]
interfaces.append(
{'name': dev,
'inet':self._get_ipv4_addresses(devinfo),
'inet6':self._get_ipv6_addresses(devinfo),
'hw': self.ethtool.get_hwaddr(dev)})
except:
logging.exception("Error retrieving networkinterfaces.")
return interfaces
在GuestAgentLinux2.py的NicMgr类中
def ethtool_list_nics(self):
interfaces = list()
try:
fordev in self.ethtool.get_devices():
flags = self.ethtool.get_flags(dev)
if flags & self.ethtool.IFF_UP and \
not(flags &self.ethtool.IFF_LOOPBACK):
devinfo =self.ethtool.get_interfaces_info(dev)[0]
interfaces.append(
{'name': dev,
'inet':self._get_ipv4_addresses(devinfo),
'inet6':self._get_ipv6_addresses(devinfo),
'hw': self.ethtool.get_hwaddr(dev)})
except:
logging.exception("Error retrieving networkinterfaces.")
return interfaces
def_get_ipv4_addresses(self, dev):
if hasattr(dev, 'get_ipv4_addresses'):
ipv4_addrs = []
for ip in dev.get_ipv4_addresses():
ipv4_addrs.append(ip.address)
return ipv4_addrs
if dev.ipv4_address is not None:
return [dev.ipv4_address]
else:
return []
def _get_ipv6_addresses(self, dev):
ipv6_addrs = []
for ip in dev.get_ipv6_addresses():
ipv6_addrs.append(ip.address)
return ipv6_addrs
由此可见,获取ipv4地址、ipv6地址和网卡地址是通过python的ethtool获得的。
6.FQDN:(FullyQualified Domain Name)完全合格域名/全称域名:
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendFQDN(self):
self._send('fqdn', {'fqdn': self.dr.getFQDN()})
在OVirtAgentLogic.py的DataRetriverBase类中:
def getFQDN(self):
returnsocket.getfqdn()
由此可见,获取全称域名是通过python的socket包的getfqdn函数获得的。
7.DisksUsage
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendDisksUsages(self):
self._send('disks-usage', {'disks':self.dr.getDisksUsage(),
'mapping': self.dr.getDiskMapping()})
在GuestAgentLinux2.py的LinuxDataRetriver类中:
defgetDisksUsage(self):
usages = list()
try:
mounts =open('/proc/mounts','r')
for mount in mounts:
try:
(device, path, fs) = mount.split()[:3]
if fs not in self.ignored_fs:
# path might include spaces.
path = path.decode("string-escape")
statvfs = os.statvfs(path)
total = statvfs.f_bsize *statvfs.f_blocks
used = total - statvfs.f_bsize *statvfs.f_bfree
if self.ignore_zero_size_fs and used ==total == 0:
continue
usages.append({'path': path, 'fs': fs,'total': total,
'used': used})
except:
logging.exception("Error retrieving disksusages.")
mounts.close()
except Exception:
logging.exception("Error during reading mounteddevices")
if mounts:
mounts.close()
return usages
由此可知获取虚拟机中的磁盘使用信息是通过读取虚拟机中/proc/mounts文件获得的。
def getDiskMapping(self):
CMD = '/usr/share/ovirt-guest-agent/diskmapper'
mapping = {}
for line in _readLinesFromProcess([CMD]):
try:
name, serial = line.split('|', 1)
except ValueError:
logging.exception("diskmapper tool used aninvalid format")
return {}
mapping[serial] = {'name': name}
return mapping
由此可知获取虚拟机中的DiskMapping信息是通过执行diskmapper文件获得的。
8.NumberOfCPUs:
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendNumberOfCPUs(self):
self._send('number-of-cpus', {'count':self.dr.getNumberOfCPUs()})
defgetNumberOfCPUs(self):
try:
returnmultiproc.cpu_count()
except NotImplementedError:
return -1
又有:
multiproc= None
try:
importmultiprocessing
multiproc = multiprocessing
except ImportError:
class MultiProcessingFake:
def cpu_count(self):
return -1
multiproc = MultiProcessingFake()
由此可知获取虚拟机中的NumberOfCPUs信息是通过multiprocessing包中的cpu_count()函数获得的。
六.Vdsm代码分析
1.init启动脚本:
init启动脚本运行/usr/share/vdsm/vdsm后台进程,参数如下:
/bin/bash -e/usr/share/vdsm/respawn --minlifetime 10 --daemon --masterpid/var/run/vdsm/respawn.pid /usr/share/vdsm/vdsm –pidfile/var/run/vdsm/vdsmd.pid
在vdsm中:run()-> serve_clients(log)
在serve_clients(log)函数中首先通过libvirtconnection.start_event_loop()来registerthe handleimplementations withlibvirt,libvirtconnection在/usr/lib/python2.6/site-package/vdsm/libvirtconnection.py中:
defstart_event_loop():
__event_loop.stop()
接着在serve_clients(log)中运行主程序:
from clientIFimport clientIF # must import after config is read
cif =clientIF.getInstance(irs, log)
cif.start()
在clientIF.py的clientIF类中启动守护程序:
defstart(self):
forbinding in self.bindings.values():
binding.start()
self.thread = threading.Thread(target=self._acceptor.serve_forever,
name='Detector thread')
self.thread.setDaemon(True)
self.thread.start()
其中,bindings来自clientIF.py的clientIF类中:
(1)def_prepareXMLRPCBinding(self, port):
ifconfig.getboolean('vars', 'xmlrpc_enable'):
try:
from rpc.BindingXMLRPC import BindingXMLRPC
from rpc.BindingXMLRPC import XmlDetector
exceptImportError:
self.log.error('Unable to load the xmlrpc server module. '
'Please make sure it is installed.')
else:
xml_binding = BindingXMLRPC(self, self.log,port)
self.bindings['xmlrpc'] = xml_binding
xml_detector = XmlDetector(xml_binding)
self._acceptor.add_detector(xml_detector)
在BindingXMLRPC.py的BindingXMLRPC类中:
def __init__(self,cif, log, port):
self.cif= cif
self.log= log
self.serverPort = port
self._enabled = False
self.server = self._createXMLRPCServer()
BindingXMLRPC.py里面的代码,这个脚本基本上是其他脚本的接口,ovirt-engine通过这里提供的XMLRPC进行和ovirt-node的交互。
(2)def_prepareJSONRPCBinding(self):
ifconfig.getboolean('vars', 'jsonrpc_enable'):
try:
from rpc import Bridge
from rpc.BindingJsonRpc import BindingJsonRpc
from yajsonrpc.stompReactor import StompDetector
exceptImportError:
self.log.warn('Unable to load the json rpc server module. '
'Please make sure it is installed.')
else:
bridge = Bridge.DynamicBridge()
json_binding = BindingJsonRpc(bridge)
self.bindings['jsonrpc'] = json_binding
stomp_detector = StompDetector(json_binding)
self._acceptor.add_detector(stomp_detector)
在BindingJsonRPC.py的BindingJsonRPC类中:
def __init__(self,bridge):
self._server = JsonRpcServer(bridge,_simpleThreadFactory)
self._reactors = []
BindingJSONRPC.py里面的代码,这个脚本基本上是其他脚本的接口,ovirt-engine通过这里提供的JSONRPC进行和ovirt-node的交互。
Ovirt调研
Ovirt(openVirtualization)是一个基于KVM的开源IaaS项目,是redhat虚拟化管理平台RHEV的开源版本,其前身是Redhat的虚拟化商业产品。在架构设计上它使用了Node/Engine分离结构,以方便功能的划分与管理。
图1.oVirt逻辑结构图
图1从总体框架上说明了oVirt的工作流程。对外,oVirt中的Engine通过HTTP协议向外提供HTTPAPI,同时提供内建的网页服务供用户和系统管理员使用。系统管理员通过网页可以创建、修改虚拟机及相关设备或用户权限,用户在拥有权限的情况下可以操作自己的虚拟机,并通过VNC或SSH登陆自己的虚拟机。Engine在整个系统中充当管理者的角色并对外提供管理服务,它挂载了自己的数据库记录整个系统中所有的虚拟机配置,各个节点的自身状态,系统的网络状态,存储器状态。管理的逻辑,状态及策略全部在Engine中设置与实现。Node只负责功能上的实现,不进行任何状态的记录和任何策略的实现。Engine与Node之间的关系十分像Linux中驱动程序与应用程序的功能分割关系:驱动仅仅负责功能的实现,如设备的读、写、开启与关闭,如何使用这些功能留给应用层。同样Node仅仅负责实现虚拟机器与设备的创建与修改,资源的共享与保护,如何使用这些功能交给Engine处理。Node暴露两种基于网络的API与Engine交互,XMLRPC与REST。Engine通过这些接口控制各个Node上功能的启动。当然用户也可以调用这些API进行第三方程序的开发。oVirt里的Node可以由一个普通的Linux上安装VDSM(VirtualDesktop Server Manager) 构成,也可以由一个专为oVirt定制的Linux系统构成。在定制的情况下,Node上的许多文件系统都是ramdisk(基于内存的Linux磁盘设备),系统重启后其中的内容消失,从而保证了Node的无状态性。Engine/Node的设计不仅方便将来的开发,更简化了用户的安装使用,在定制的情况下Node可以快速大量部署。
图2.Node上运行的主要组件
简单概括起来,VDSM的功能主要有:负责Node的自动启动与注册;虚拟机的操作与生命周期管理;网络管理;存储管理;Host与VM状态监视与报告;提供对虚拟机的外部干涉功能;提供内存与存储的合并与超支功能(OverCommitment)。
一个标准的oVirt部署包括三部分,主要有:
1.ovirt-engine,这个被用于部署,监管,移除,停止和创建VM镜像,配置存储,网络,等等。
2.一个或多个ovirt-node,我们在node上运行虚拟机(VMs)。
3.一个或多个存储节点,其上保存着对应虚拟机的磁盘镜像和ISO镜像。
Ovirt-node上安装了VDSM和libvirt以及安装了一些额外的包,这些包可以实现网络和其他系统服务的虚拟化。
存储节点可以使用块和文件存储,并且可以通过NFS在本地或者远程进行访问。通过POSIXFS存储类型是可以支持Gluster这样的存储技术的。存储节点被分在一个存储池中,这样可以保证高可用和冗余。
oVirtengine是一个基于JBoss的java应用程序(由C#演变过来),它作为web服务来运行。这个服务直接指挥node上的VDSM去部署,启动,停止,迁移和监控虚拟机,以及从模板中创建新的镜像至存储中。
engine提供的一些功能:
虚拟机生命周期管理
通过LDAP(微软的AD或IPA)进行身份验证
网络管理-增加本地网络并且将其附加至主机上。
存储管理-管理存储域(NFS/iSCSI/Local)和虚拟VM磁盘。
高可用-在其它主机中失败的主机上自动重启客户虚拟机
在线迁移-在零停机的情况下移动主机之间的虚拟机。
系统调度-基于资源使用/政策持续负载均衡虚拟机。
节能优化-在非高峰时期将续集你集中到较少的服务器上。
维护管理器-在计划的维护期间,无需为虚拟机停机。
镜像管理-基于模板管理,自动精简配置和快照。
监控-为系统中的所有对象-虚拟客户机,主机,网络,存储等等。
导出/导入-使用OVF文件导入和导出虚拟机和模板。
V2V-可以将VMware或RHEL或Xen环境下的虚拟机轻松转换到oVirt环境中
图3展示了OvirtEngine组件的不同层次:
图3.OvirtEngine组件层次
下面的图形显示了Engine-core的不同组件
enginecore中的主要组件有:
-负责所有与之相关的DB操作。
VDS Broker -负责所有需要与VDSM交互的操作。
LDAP Broker -负责验证和抓取LDAP目录下用户/组的属性(当前支持AD,IPA和RHDS)
Backend Bean-一个Singletonbean ,负责运行不同实体的指令,查询和监控。
VDSM是一个以Python开发的组件,它可以为主机,虚拟机,网络和存储管理提供所需要的所有功能。
VDSMAPI是基于XML-RPC的(计划转移到RESTAPI)。这就是ovirt-engine如何与VDSM进行通信的。
配置主机,网络和共享的存储。
使用libvirt,进行虚拟机的生命周期操作
多线程,多进程
通过virtio-serial和客户代理对话。
添加自定义的对扩展到数以百计的node的LVM的集群支持
在所支持的存储类型(本地目录,FCP, FCoE, iSCSI, NFS, SAS)的基础上实现一个分布式镜像库。
多主机系统,一个并发的元数据写入器。
在数据写入器中进行扩展。
允许管理员定义修改虚拟机操作的脚本,例如,添加额外的操作像CPU定位,watchdog设备,直接访问LUN
允许在集成完成前为新的KVM特性扩展oVirt。
有一种简单的方式来测试新的kvm/libvirt/linux特性。
在VDSM利用libvirt开始启动虚拟机之前调用hook机制。
hook改变了虚拟机定义,并且VDSM通过定义至libvirt以开启虚拟机。
下图阐释了虚拟机生命周期中Hook机制。
VDSM是和MoM集成在一起的。MOM的行为配置了策略。随着这些策略的使用者可以微调主机的高内存过量使用或安全操作。为了控制它的mom实例,vdsm确实传递了一个mom配置文件并且mom策略文件可以设置mom的缺省行为。刚开始,vdsmd利用配置和策略文件导入了mom并将其初始化。从那时起,mom通过API.py中定义好的API与vdsm进行交互并且控制了运行在主机上的每个虚拟机的内存。mom实例作为vdsm守护进程内的线程运行。
存储器的管理是oVirt的重点,本节将介绍oVirt是如何组织与分配各种存储器的,用户应该如何使用它。每一个Node上都会运行一个VDSM,实现网络、存储器、虚拟机的创建与修改的功能。VDSM的大部分代码用在了存储系统上,其功能包括数据的组织,集群下的数据共享与保护,故障恢复。通常情况下每一个物理机器当作一个Node,运行一个VDSM,Node本身只携带少量存储器用以保存配置。一个集群中通常有一个Engine和数个Node,这些Node通过网络连接到SAN(StorageArea Network) 上,VDSM把Node上运行的虚拟机存储数据保存在SAN上,Node本身为无状态的节点,重新启动后状态消失,从而保证了系统整体的可用性,一般情况下不会因用户的操作而使Node失效。一旦问题发生,通常一次重启即可恢复工作状态。
在云计算环境中,SAN中往往存储着大量虚拟机器使用的virtualimage,同时每一个virtualimage在任何时候都可能被任意Node访问,同时出于性能的考虑virtualimage可能以文件或者数据块的形式出现,这些对存储系统的设计提出了挑战。
为此,VDSM基于以下原则设计了自己的存储系统:
1高可用性:一群安装有VDSM的Node在组建集群的时候,没有潜在的单点故障存在,任何一个Node崩溃不会影响整个集群的功能,它的角色会被其他Node取代。Engine不可用的情况下,Node将继续工作,用户对虚拟机的操作可以继续进行。
2高伸缩性:添加Node和SAN几乎不需要用户的设置,Node上的VDSM会自己注册自己。
3集群安全性:一个VDSM对正在操作的virtualimage进行排它性保护。
4备份与恢复:virtualimage之间有相互关连的特性记录可进行一系列引用/备份操作。
5性能优化:利用多线程与多进程减少操作堵塞状况。
StorageDomain(以下简程SD)是VDSM中的最基本存储实体,所有的virtualimage和virtualimage对应的元数据都会保存在其中。和VDSM中的StorageImage概念不同,这里的virtualimage表示的是虚拟机程序用到的虚拟磁盘数据,特指虚拟机程序最终能够操作的文件或设备文件对象。元数据是描述virtualimage相关数据大小、状态、锁等内容的一组数据集合。SD包括两种类型:FileDomain和BlockDomain。FileDomain使用文件系统存储数据并同步操作,主要针对NFS(NetworkFile System) 和LOCALFS(LocalFile System) 文件系统。在文件系统的帮助下,FileDomain拥有良好的virtualimage操作能力,每一个虚拟机的存储数据(称为Volume)和对应的元数据都以文件的方式保存。每一个Domain实际对应于Host文件系统上的一个目录,针对NFS文件系统VDSM还有额外的逻辑来处理相关意外与错误情况。而BlockDomain直接操作原始的块数据,使用Linux的LVM(LogicalVolume Manager) 功能来组织数据,主要针对iSCSI(InternetSmall Computer System Interface),FCoE(FibreChannel over Ethernet ) 等块设备。由于目标设备上通常没有一个文件系统来保证访问的安全性,VDSM使用了邮箱机制来保证任意时刻,只有一个Node可以修改Block上的内容,而其他Node则通过Socket邮箱发送自己的修改请求。因此它的操作请求速度和监视功能都会比FileDomain弱一些。通常设备将使用Linux的devicemapper机制进行一次映射,每一个Domain实际上是一个Linux中的VolumeGroup,元数据保存在其中的一个LogicVolume及其tag上,虚拟机的Volume保存在另一个LogicVolume中。
StoragePool(以下简称SP)是一组SD的组合,目标是管理跨越SD之间的操作,也就是说SD之间互相的引用、备份、恢复,合并一般发生在一个SP之中。在数据中心里,一个SP抽象了一组SD的集合供外界的Node访问或者Engine管理,并且一个SP中的所有SD必须是同一类型,如NFS或者iSCSI。
为了保证SP中的数据安全,一组SP中需要选择一个SD作为MasterDomain。这个Domain的不同之处在于它会保存SP中所有的元数据,保存一些异步请求或者任务的数据,保存所在SP的集群存储用到的锁。
为了简化管理,oVirt中抽象出了DataCenter概念,一个DataCenter将拥有一组NodeCluster用来运行虚拟机,一个StoragePool用来保存虚拟磁盘数据。NodeCluster是一组专门用来运行虚拟机的Node的集合,运行在其中的虚拟机可以动态迁移到NodeCluster中的另外一个Node上。一个DataCenter是一个完成oVirt所有功能的实体,在这个DataCenter中用户可以创建虚拟机、备份虚拟机、配置虚拟机的StorageDomain,动态迁移虚拟机。NodeEngine有一些算法在开启的时候可以自动平衡DataCenter中的Node的负载。概括起来一个DataCenter是一个管理NodeCluster与StoragePool的集合。
由于DataCenter中所有的Node都拥有对DataCenter中的StoragePool的访问权限,因此VDSM实现了一个称为SPM(StoragePool Manager) 的功能角色。在一个DataCenter中,所有的Node启动后会自动选举出一个Node充当SPM的角色,被选举者将运行VDSM上的SPM逻辑,负责完成以下功能:创建/删除/缩放所在DataCenter中的Image,快照,模板。这些操作的共同点是会影响StoragePool中的元数据,如SAN上松散块设备的分配。为了保证元数据不被多个Node同时修改,SPM拥有对StoragePool中元数据的排它性操作权限,SPM使用集中式邮箱接受其他Node的相关请求,其他Node只能通过给SPM发送操作请求的方式修改元数据,最终的操作都由SPM线性完成,从而避免了存储器操作竞态的出现。为了兼顾效率,不修改元数据的普通操作,如数据读写,Node可以不同过SPM,自己直接访问StoragePool完成。由于SPM是由一个普通Node选举出来的,因此当它因为外部原因失效后,系统将会选举出另外的Node充当SPM,从而保证系统能继续运行。
4.Ovirt guest agent
Ovirt-guest-agent是一个python编写的运行在虚拟机内部的守护程序(可执行文件为/usr/share/ovirt-guest-agent/ovirt-guest-agent.py),通过宿主机的VDSM为虚拟化管理器(ovirt-engine或rhev-m)提供虚拟机的信息,在虚拟机上通过虚拟串口virtio-serial(默认首选方式)或者isa-serial使用json协议与宿主机上的unixsocket文件进行交互。
Engine将发给guestagent的请求重定向到VDSM,VDSM通过 VirtIO通道使用JSON格式的纯文本命令与guest通信。
VDSM/GuestAgent消息结构
VDSM发送的每条信息都有一个必填字段:'__name__',它包含了指令名。
Alladditional fields in the message are considered arguments to thecommand. Please see the OVirt_Guest_Agent/CommandDefinitions pagefor detailed information about supported commands and theirarguments.
Ovirt-guest-agent通过读写串口设备与宿主机上的socket通道进行交互,宿主机上可以使用普通的unixsocket读写方式对socket文件进行读写,最终实现与Ovirt-guest-agent的交互,交互的协议与qmp(QEMUMonitor Protocol)相同(简单来说就是使用JSON格式进行数据交换),串口设备的速率通常都较低,所以比较适合小数据量的交换。
xml文件中配置方式如下:
<channel type='unix'>
<sourcemode='bind'path='/var/lib/libvirt/qemu/channels/1aea6926-34c2-470d-ac0a d8de68547a2f.com.redhat.rhevm.vdsm'/>
<target type='virtio' name='com.redhat.rhevm.vdsm'/>
</channel>
通过上面的参数就可以在宿主机上生成一个unixsocket文件,socket文件的路径为:/var/lib/libvirt/qemu/channels/1aea6926-34c2-470d-ac0a-d8de68547a2f.com.redhat.rhevm.vdsm,同时在虚拟机内部生成一个serial设备,名字为vport0p1,设备路径为:/dev/vport0p1,映射出来的可读性比较好的路径为:/dev/virtio-ports/vport0p1,可以在运行ovirt-guest-agent的时候通过-p参数指定读写这个设备。
目前,ovirt-guest-agent可以运行在以下系统上:
RedHat Enterprise Linux 5.x
RedHat Enterprise Linux 6.
CentOS6.x
Fedora16-20
Ubuntu12.04+
openSUSE12.x+
WindowsXP (32)
Windows7 (32/64)
Windows8.x (32/64)
Windows2003 (32/64/R2)
Windows2008 (32/64/R2)
Windows2012 (32/64/R2)
对于linux虚拟机来说,guestagent需要python和一个虚拟串口设备,对于windows系统来说,guestagent需要python和安装pywin32包
guestagent是运行在虚拟机后台的一个应用程序,它提供了虚拟机的一些信息和一些无法从虚拟机操作系统外部来执行的预设操作。
guestagent提供虚拟机的如下信息:
●Machinename -显示虚拟机的主机名.
●Operatingsystem version -显示操作系统的版本。对于Linux:这个值指的是内核版本。对于Windows:这个指指的是Windows版本的名称(如WindowsXP或Windows7)。
●Installedapplications -列出所安装的应用程序。对于Linux:应用程序列表是通过配置文件设置的,对于Windows:安装应用程序的列表是基于注册表来显示的。
●AvailableRAM -未使用的物理存储的总数。
●Loggedin users -列出登录的用户。
●Activeuser-列出当前正在使用虚拟机“物理硬件”的用户。
●DisksUsage-虚拟机磁盘的使用信息.
●Network-interfaces-虚拟机网络接口信息(MAC,name, ipv4, ipv6).
●FQDN(FullyQualified Domain Name)-报告配置的虚拟机的全称域名
guestagent会在如下事件发生时发出通知:
●PowerUp – guest agent开始运行时.
●Power Down - guest agent停用时
●Heartbeat–每隔几秒钟发送一个心跳包,通知管理端guestagent正在运行。这个通知还包括了虚拟机可用的RAM信息
●UserInfo-在线用户更改时
●SessionLock-桌面被锁定时(Windows).
●SessionUnlock-桌面解锁时(Windows).
●SessionLogoff-用户注销时(Windows).
●SessionLogon-用户登入时(Windows).
●AgentUninstalled– guest agent被卸载时– 通知VDSM清空缓存
以下操作可以通过guestagent实现
●Lockscreen-锁定桌面.
●Login-执行用户的登录操作
在RHEL下自动登录
在Windows下自动登录
●Logoff-注销在线的用户
●Shutdown-关闭虚拟机
5.Ovirt guest agent主要代码分析
Ovirt guestagent入口程序:/usr/share/ovirt-guest-agent/ovirt-guest-agent.py
程序启动后在OVirtAgentLogic.py中启动两个线程,doListen线程负责监听和执行VDSM发来的指令,doWork线程负责定时获取虚拟机信息并发送给VDSM:
thread.start_new_thread(self.doListen,())
thread.start_new_thread(self.doWork,())
监听线程首先从虚拟串口中读取发来的命令:
cmd, args =self.vio.read()
此函数的最终实现在VirtioChannel.py中:
def_os_read(self, size):
returnos.read(self._vport, size)
获得命令后对命令进行解析:
self.parseCommand(cmd, args)
解析的代码如下(OVirtAgentLogic.py中):
defparseCommand(self, command, args):
logging.info("Received an external command: %s..." %(command))
ifcommand == 'lock-screen':
self.commandHandler.lock_screen()
elifcommand == 'log-off':
self.commandHandler.logoff()
elifcommand == 'api-version':
self._onApiVersion(args)
elifcommand == 'shutdown':
try:
timeout = int(args['timeout'])
except:
timeout = 0
try:
msg = args['message']
except:
msg = 'System is going down'
try:
reboot = args['reboot'].lower() == 'true'
except:
reboot = False
action = 'Shutting down'
ifreboot:
action = 'Rebooting'
logging.info("%s (timeout = %d, message = '%s')"
% (action, timeout, msg))
self.commandHandler.shutdown(timeout, msg, reboot)
elifcommand == 'login':
username = args['username'].encode('utf8')
password = args['password'].encode('utf8')
credentials = struct.pack(
'>I%ds%ds' % (len(username), len(password) + 1),
len(username), username, password)
logging.debug("User log-in (credentials = %s)"
% (safe_creds_repr(credentials)))
self.commandHandler.login(credentials)
elifcommand == 'refresh':
if'apiVersion' not in args and self.dr.getAPIVersion() > 0:
logging.info('API versioning not supported by VDSM. Disabling '
'versioning support.')
self.dr.setAPIVersion(_DISABLED_API_VALUE)
self.sendUserInfo(True)
self.sendAppList()
self.sendInfo()
self.sendDisksUsages()
self.sendFQDN()
elifcommand == 'echo':
logging.debug("Echo: %s", args)
self._send('echo', args)
elifcommand == 'hibernate':
state = args.get('state', 'disk')
self.commandHandler.hibernate(state)
elifcommand == 'set-number-of-cpus':
count = args.get('count', 0)
ifcount > 0:
self.commandHandler.set_number_of_cpus(count)
self.sendNumberOfCPUs()
else:
logging.error("Unknown external command: %s (%s)"
% (command, args))
1.lock-screen:
if command =='lock-screen':
self.commandHandler.lock_screen()
在GuestAgentLinux2.py的CommandHandlerLinux类中:
deflock_screen(self):
cmd =['/usr/share/ovirt-guest-agent/ovirt-locksession']
logging.debug("Executing lock session command: '%s'",cmd)
subprocess.call(cmd)
而ovirt-locksession->/usr/bin/consolehelper
所以是执行一个文件实现的。
其他的命令也类似
由此可知,要想加入新功能,比如修改密码等,就在OVirtAgentLogic.py中的parseCommand函数中相应的添加一种情况elifcommad==……,同时在GuestAgentLinux2.py的CommandHandlerLinux类中添加执行相应操作的函数。
doWork执行的代码如下:
def doWork(self):
logging.debug("AgentLogicBase:: doWork() entered")
self.sendInfo()
self.sendUserInfo()
self.sendAppList()
self.sendFQDN()
counter= 0
hbsecs= self.heartBitRate
appsecs = self.appRefreshRate
disksecs = self.disksRefreshRate
usersecs = self.userCheckRate
numcpusecs = self.numCPUsCheckRate
try:
while not self.wait_stop.isSet():
counter += 1
hbsecs -= 1
if hbsecs <= 0:
self._send('heartbeat',
{'free-ram': self.dr.getAvailableRAM(),
'memory-stat': self.dr.getMemoryStats(),
'apiVersion': _MAX_SUPPORTED_API_VERSION})
hbsecs = self.heartBitRate
usersecs -= 1
if usersecs <= 0:
self.sendUserInfo()
usersecs = self.userCheckRate
appsecs -= 1
if appsecs <= 0:
self.sendAppList()
self.sendInfo()
self.sendFQDN()
appsecs = self.appRefreshRate
disksecs -= 1
if disksecs <= 0:
self.sendDisksUsages()
disksecs = self.disksRefreshRate
numcpusecs -= 1
if numcpusecs <= 0:
self.sendNumberOfCPUs()
numcpusecs = self.numCPUsCheckRate
time.sleep(1)
logging.debug("AgentLogicBase:: doWork() exiting")
except:
logging.exception("AgentLogicBase::doWork")
由此代码可知,当心跳时间、app时间、磁盘时间、用户时间、cpu个数时间的倒数计时为0时即通过串口向VDSM发送信息,发送信息的代码如下:
def _send(self,name, arguments=None):
version= _MESSAGE_MIN_API_VERSION.get(name, None)
ifversion is None:
logging.error('Undocumented message "%s"', name)
elifversion <= self.dr.getAPIVersion():
self.vio.write(name, arguments or {})
else:
logging.debug("Message %s not supported by api version %d.",
name, self.dr.getAPIVersion())
获取各种信息的代码如下:
1.心跳包中的AvailableRAM:
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def getAvailableRAM(self):
free = 0
for line inopen('/proc/meminfo'):
var, value =line.strip().split()[0:2]
if var in ('MemFree:','Buffers:', 'Cached:'):
free += long(value)
return str(free / 1024)
由此可见,获取虚拟机中的空闲RAM信息是通过读取虚拟机中/proc/meminfo文件获得的。
2.心跳包中的MemoryStats:
在GuestAgentLinux2.py的LinuxDataRetriver类中:
defgetMemoryStats(self):
try:
self._get_meminfo()
self._get_vmstat()
except:
logging.exception("Error retrieving memory stats.")
returnself.memStats
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def _get_meminfo(self):
fields = {'MemTotal:': 0, 'MemFree:': 0, 'Buffers:': 0,
'Cached:': 0, 'SwapFree:': 0, 'SwapTotal:': 0}
free = 0
for line in open('/proc/meminfo'):
(key, value) = line.strip().split()[0:2]
if key in fields.keys():
fields[key] = int(value)
if key in ('MemFree:', 'Buffers:', 'Cached:'):
free += int(value)
self.memStats['mem_total'] = fields['MemTotal:']
self.memStats['mem_unused'] = fields['MemFree:']
self.memStats['mem_free'] = free
self.memStats['mem_buffers'] = fields['Buffers:']
self.memStats['mem_cached'] = fields['Cached:']
swap_used = fields['SwapTotal:'] - fields['SwapFree:']
self.memStats['swap_usage'] = swap_used
self.memStats['swap_total'] = fields['SwapTotal:']
由此可知获取虚拟机中的虚拟机内存等信息是通过读取虚拟机中/proc/meminfo文件获得的。
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def _get_vmstat(self):
fields = {'pswpin': 'swap_in', 'pswpout': 'swap_out',
'pgfault': 'pageflt', 'pgmajfault': 'majflt'}
self.vmstat['timestamp_cur'] = time.time()
interval = self.vmstat['timestamp_cur'] -self.vmstat['timestamp_prev']
self.vmstat['timestamp_prev'] =self.vmstat['timestamp_cur']
for line in open('/proc/vmstat'):
(key, value) = line.strip().split()[0:2]
if key in fields.keys():
name = fields[key]
self.vmstat[name + '_prev'] = self.vmstat[name +'_cur']
self.vmstat[name + '_cur'] = int(value)
if self.vmstat[name + '_prev'] is None:
self.vmstat[name + '_prev'] =self.vmstat[name + '_cur']
self.memStats[name] = int((self.vmstat[name +'_cur'] -
self.vmstat[name +'_prev']) /
interval)
由此可知获取虚拟机中的虚拟机状态等信息是通过读取虚拟机中/proc/vmstat文件获得的。
3.UserInfo
在OVirtAgentLogic.py的AgentLogicBase中:
defsendUserInfo(self, force=False):
cur_user = self.dr.getActiveUser()
logging.debug("AgentLogicBase::sendUserInfo - cur_user ='%s'" %
(cur_user))
ifcur_user != self.activeUser or force:
self._send('active-user', {'name': cur_user})
self.activeUser = cur_user
在GuestAgentLinux2.py的LinuxDataRetriver类中:
defgetActiveUser(self):
users =os.popen('/usr/bin/users').read().split()
try:
user = users[0]
except:
user = 'None'
returnuser
由此可知获取虚拟机中的Activeuser的信息是通过读取虚拟机中/usr/bin/users文件获得的。
4.InstalledApplications:
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendAppList(self):
self._send('applications',{'applications':self.dr.getApplications()})
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def getApplications(self):
returnself.list_pkgs(self.app_list)
self.dr.app_list =config.get("general","applications_list")
def list_pkgs(self, app_list)
apps = set()
try:
if self.rpm:
apps.update(self.rpm_list_packages(app_list))
if self.apt_pkg:
apps.update(self.apt_list_packages(app_list))
except Exception:
logging.exception("Failed to get package list")
apps = list(apps)
logging.debug("PkgMgr:list_pkgs returns [%s]" % (str(apps)))
return apps
在GuestAgentLinux2.py的LinuxDataRetriver类中:
def apt_list_packages(self,app_list):
INSTALLED_STATE =self.apt_pkg.CURSTATE_INSTALLED
apps = set()
cache = self.apt_pkg.Cache()
for app in app_list.split():
if app in cache:
pkg = cache[app]
# Normal package
ifpkg.current_state == INSTALLED_STATE:
detail = (app,pkg.current_ver.ver_str)
apps.add("%s-%s" % (detail))
# virtual package
eliflen(pkg.provides_list) > 0:
for _, _, pkgin pkg.provides_list:
ifpkg.parent_pkg.current_state == INSTALLED_STATE:
detail= (app, pkg.parent_pkg.current_ver.ver_str)
apps.add("%s-%s" % (detail))
return apps
由此可见,获取虚拟机中安装的应用程序信息是通过读取配置文件获得的。
5.操作系统、机器名、网络接口等信息:
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendInfo(self):
self._send('host-name', {'name': self.dr.getMachineName()})
self._send('os-version', {'version':self.dr.getOsVersion()})
self._send('network-interfaces',
{'interfaces':self.dr.getAllNetworkInterfaces()})
在GuestAgentLinux2.py的LinuxDataRetriver类中:
defgetMachineName(self):
returnsocket.getfqdn()
由此可见,获取虚拟机名是通过python的socket.getfqdn()函数获得的。
defgetOsVersion(self):
returnos.uname()[2]
由此可见,获取操作系统版本是通过python的os.uname()[2]函数获得的。
defgetAllNetworkInterfaces(self):
returnself.list_nics()
self.list_nics=nicmgr.list_nics
在GuestAgentLinux2.py的NicMgr类中:
self.list_nics=self.ethtool_list_nics
defethtool_list_nics(self):
interfaces = list()
try:
for dev in self.ethtool.get_devices():
flags = self.ethtool.get_flags(dev)
if flags & self.ethtool.IFF_UP and \
not(flags &self.ethtool.IFF_LOOPBACK):
devinfo =self.ethtool.get_interfaces_info(dev)[0]
interfaces.append(
{'name': dev,
'inet':self._get_ipv4_addresses(devinfo),
'inet6':self._get_ipv6_addresses(devinfo),
'hw': self.ethtool.get_hwaddr(dev)})
except:
logging.exception("Error retrieving networkinterfaces.")
return interfaces
在GuestAgentLinux2.py的NicMgr类中
def ethtool_list_nics(self):
interfaces = list()
try:
fordev in self.ethtool.get_devices():
flags = self.ethtool.get_flags(dev)
if flags & self.ethtool.IFF_UP and \
not(flags &self.ethtool.IFF_LOOPBACK):
devinfo =self.ethtool.get_interfaces_info(dev)[0]
interfaces.append(
{'name': dev,
'inet':self._get_ipv4_addresses(devinfo),
'inet6':self._get_ipv6_addresses(devinfo),
'hw': self.ethtool.get_hwaddr(dev)})
except:
logging.exception("Error retrieving networkinterfaces.")
return interfaces
def_get_ipv4_addresses(self, dev):
if hasattr(dev, 'get_ipv4_addresses'):
ipv4_addrs = []
for ip in dev.get_ipv4_addresses():
ipv4_addrs.append(ip.address)
return ipv4_addrs
if dev.ipv4_address is not None:
return [dev.ipv4_address]
else:
return []
def _get_ipv6_addresses(self, dev):
ipv6_addrs = []
for ip in dev.get_ipv6_addresses():
ipv6_addrs.append(ip.address)
return ipv6_addrs
由此可见,获取ipv4地址、ipv6地址和网卡地址是通过python的ethtool获得的。
6.FQDN:(FullyQualified Domain Name)完全合格域名/全称域名:
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendFQDN(self):
self._send('fqdn', {'fqdn': self.dr.getFQDN()})
在OVirtAgentLogic.py的DataRetriverBase类中:
def getFQDN(self):
returnsocket.getfqdn()
由此可见,获取全称域名是通过python的socket包的getfqdn函数获得的。
7.DisksUsage
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendDisksUsages(self):
self._send('disks-usage', {'disks':self.dr.getDisksUsage(),
'mapping': self.dr.getDiskMapping()})
在GuestAgentLinux2.py的LinuxDataRetriver类中:
defgetDisksUsage(self):
usages = list()
try:
mounts =open('/proc/mounts','r')
for mount in mounts:
try:
(device, path, fs) = mount.split()[:3]
if fs not in self.ignored_fs:
# path might include spaces.
path = path.decode("string-escape")
statvfs = os.statvfs(path)
total = statvfs.f_bsize *statvfs.f_blocks
used = total - statvfs.f_bsize *statvfs.f_bfree
if self.ignore_zero_size_fs and used ==total == 0:
continue
usages.append({'path': path, 'fs': fs,'total': total,
'used': used})
except:
logging.exception("Error retrieving disksusages.")
mounts.close()
except Exception:
logging.exception("Error during reading mounteddevices")
if mounts:
mounts.close()
return usages
由此可知获取虚拟机中的磁盘使用信息是通过读取虚拟机中/proc/mounts文件获得的。
def getDiskMapping(self):
CMD = '/usr/share/ovirt-guest-agent/diskmapper'
mapping = {}
for line in _readLinesFromProcess([CMD]):
try:
name, serial = line.split('|', 1)
except ValueError:
logging.exception("diskmapper tool used aninvalid format")
return {}
mapping[serial] = {'name': name}
return mapping
由此可知获取虚拟机中的DiskMapping信息是通过执行diskmapper文件获得的。
8.NumberOfCPUs:
在OVirtAgentLogic.py的AgentLogicBase类中:
defsendNumberOfCPUs(self):
self._send('number-of-cpus', {'count':self.dr.getNumberOfCPUs()})
defgetNumberOfCPUs(self):
try:
returnmultiproc.cpu_count()
except NotImplementedError:
return -1
又有:
multiproc= None
try:
importmultiprocessing
multiproc = multiprocessing
except ImportError:
class MultiProcessingFake:
def cpu_count(self):
return -1
multiproc = MultiProcessingFake()
由此可知获取虚拟机中的NumberOfCPUs信息是通过multiprocessing包中的cpu_count()函数获得的。
六.Vdsm代码分析
1.init启动脚本:
init启动脚本运行/usr/share/vdsm/vdsm后台进程,参数如下:
/bin/bash -e/usr/share/vdsm/respawn --minlifetime 10 --daemon --masterpid/var/run/vdsm/respawn.pid /usr/share/vdsm/vdsm –pidfile/var/run/vdsm/vdsmd.pid
在vdsm中:run()-> serve_clients(log)
在serve_clients(log)函数中首先通过libvirtconnection.start_event_loop()来registerthe handleimplementations withlibvirt,libvirtconnection在/usr/lib/python2.6/site-package/vdsm/libvirtconnection.py中:
defstart_event_loop():
__event_loop.stop()
接着在serve_clients(log)中运行主程序:
from clientIFimport clientIF # must import after config is read
cif =clientIF.getInstance(irs, log)
cif.start()
在clientIF.py的clientIF类中启动守护程序:
defstart(self):
forbinding in self.bindings.values():
binding.start()
self.thread = threading.Thread(target=self._acceptor.serve_forever,
name='Detector thread')
self.thread.setDaemon(True)
self.thread.start()
其中,bindings来自clientIF.py的clientIF类中:
(1)def_prepareXMLRPCBinding(self, port):
ifconfig.getboolean('vars', 'xmlrpc_enable'):
try:
from rpc.BindingXMLRPC import BindingXMLRPC
from rpc.BindingXMLRPC import XmlDetector
exceptImportError:
self.log.error('Unable to load the xmlrpc server module. '
'Please make sure it is installed.')
else:
xml_binding = BindingXMLRPC(self, self.log,port)
self.bindings['xmlrpc'] = xml_binding
xml_detector = XmlDetector(xml_binding)
self._acceptor.add_detector(xml_detector)
在BindingXMLRPC.py的BindingXMLRPC类中:
def __init__(self,cif, log, port):
self.cif= cif
self.log= log
self.serverPort = port
self._enabled = False
self.server = self._createXMLRPCServer()
BindingXMLRPC.py里面的代码,这个脚本基本上是其他脚本的接口,ovirt-engine通过这里提供的XMLRPC进行和ovirt-node的交互。
(2)def_prepareJSONRPCBinding(self):
ifconfig.getboolean('vars', 'jsonrpc_enable'):
try:
from rpc import Bridge
from rpc.BindingJsonRpc import BindingJsonRpc
from yajsonrpc.stompReactor import StompDetector
exceptImportError:
self.log.warn('Unable to load the json rpc server module. '
'Please make sure it is installed.')
else:
bridge = Bridge.DynamicBridge()
json_binding = BindingJsonRpc(bridge)
self.bindings['jsonrpc'] = json_binding
stomp_detector = StompDetector(json_binding)
self._acceptor.add_detector(stomp_detector)
在BindingJsonRPC.py的BindingJsonRPC类中:
def __init__(self,bridge):
self._server = JsonRpcServer(bridge,_simpleThreadFactory)
self._reactors = []
BindingJSONRPC.py里面的代码,这个脚本基本上是其他脚本的接口,ovirt-engine通过这里提供的JSONRPC进行和ovirt-node的交互。