透過KVM建立好了虛擬主機環境之後,有時候想要知道這台Node上面究竟可以執行到多少台VM,想試試看它的能耐到底如何,當然不可能從virt-manager上一台一台安裝,這樣太慢了。最好的方式,是就現有正在執行中的VM,複製它的image檔案,然後再做一些參數上的修改就可以了。這樣做雖然比較快,但是要新增3、4台當然沒有問題,可是如果要部署10、20或甚至100台的話,沒有自動化不只不方便,而且也容易出錯。這時候,透過python-libvirt程式庫來自動化部署,就是非常方便的選擇。(聽說有一些工具可以用,但是筆者還是喜歡自己動手做)

假設,我們已經有一台叫做base的VM,它的image檔案是base.raw(請注意,我們在這裡使用的方法一定要是raw格式,原因請參考【掛載VM硬碟分割區的方法】),已安裝了可以執行的Linux作業系統(目前我們的方法僅適用於主流的Linux作業系統),我們的目標是執行一個Python程式,它可以幫我們複製base.raw(因此,建立出來的虛擬機會和base一模一樣),並且修改虛擬機的名稱、主機名稱以及Mac Address,並使用指定的名字建立新的虛擬機同時開啟執行程序。以下便是快速部署的步驟。

首先,依照現有的主機環境,把base這個Domain的XML傾印出來,指令如下:

# virsh dumpxml base > base.xml

此base.xml如果是正在執行中的domain,則在XML檔案中會有id,需刪除此id。此外,uuid這個標籤的內容也要整個移除,新的XML檔案如果沒有指定UUID,則libivirt會自動指令一個新的。

有了base.xml以及base.raw,假設base.raw位於/var/lib/libvirt/images之下,那麼我們的Python程式要做的事情主要有如下幾點:

  1. 設定raw檔案的正確路徑
  2. 設定要建立的虛擬機名稱清單如[‘vm01’, ‘vm02’, ‘vm03’],放在vm_lists串列中,此串列中有多少的字串,就建立多少台虛擬機,而且每一台虛擬機的名稱以及主機名稱均設定為此串列中的字串名稱
  3. 逐一取出vm_lists中的字串,以此字串複製base.raw,例如vm01.raw
  4. 使用kpartx指令掛載vm01.raw,然後找出/etc/hostname,將主機名稱改為vm01
  5. 讀入base.xml,修改<name>base</name>成為<name>vm01</name>,同時也要修改<disk>標籤中source file的位置,使成為/var/lib/libvirt/images/vm01.raw。此外,<mac>標籤中的address屬性也要設定一個新的(使用亂數產生)。由於我們的虛擬機設定是使用DHCP,所以網路IP位址的部份可以不用理會。
  6. RAW以及XML檔案均準備好了之後,再透過conn.defineXML(xml)來建立虛擬機的Domain,然後使用create來執行,就大功告成了。
  7. 第3步到第6步重複執行到vm_lists清單中的字串清空,所有的虛擬機就完全部署完畢。

在部署的過程中,由於RAW的檔案都很大(以筆者實驗用的Lubuntu作業系統,也要5GB),所以在複製RAW檔案的時候會花比較久的時間(筆者的實驗Intel B820 CPU,傳統硬碟,CentOS 7中複製5GB大約要2~3分鐘),另外你設定虛擬主機數量時,也要注意所有可用的硬碟空間是否足夠(NFS提供的空間)。

還有一點,在mount磁區到實際可以使用中間需要一點時間,所以適當的sleep時間是需要的,不然會出現一些檔案操作的錯誤。以下就是自動化部署的Python程式碼:

#! /usr/bin/python 

import os, subprocess, sys, shutil, time
from sh import mount, umount
import libvirt, random
import xml.dom.minidom

def getMAC():
	mac = [ 0x00, 0x16,0x3e,
                random.randint(0x00, 0x7f),
                random.randint(0x00, 0xff),
                random.randint(0x00, 0xff) ]
        return ':'.join(map(lambda x: "%02x" % x , mac))

def deploy_vm(base_raw, base_xml, vm_name, target_vm):
	ret = True
	mac = getMAC()
	xmlfile = xml.dom.minidom.parse(base_xml)
	dom_root = xmlfile.documentElement
	
	newxml = dom_root.toxml()
	name_tag = dom_root.getElementsByTagName('name')
	name_tag[0].firstChild.data = vm_name
	mac_tag = dom_root.getElementsByTagName('mac')
	mac_tag[0].setAttribute('address', mac)
	disk_tag = dom_root.getElementsByTagName('disk')
	for disk in disk_tag:
		if disk.getAttribute('device') == 'disk':
			source_tag = disk.getElementsByTagName('source')
			source_tag[0].setAttribute('file', target_vm)
			break
	newxml = dom_root.toxml()
	print "Coping the file: " + target_vm
	print "Please wait for serveral miniutes..."
	shutil.copyfile(base_rawfile, target_vm)

	print "Changing the hostname for %s..." % vm_name
	p = subprocess.Popen(['kpartx', '-av', target_vm], 
				stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	out, err = p.communicate()
	parts = out.split()
	mountp = ''
	for str in parts:
		if str.startswith("loop"):
			mountp = str
			break
	target_path = '/mnt/vm'
	mount_path = '/dev/mapper/' + mountp

	if os.path.exists(target_path):
		if os.path.ismount(target_path):
			umount(target_path)
	else:
		os.mkdir(target_path) 
	time.sleep(10)
	print "Try to mount the partition:", mount_path, target_path
	mount(mount_path, target_path)
	with open('/mnt/vm/etc/hostname', 'w') as hostname_file:
		hostname_file.write(vm_name)
	umount(target_path)
	remove_raw_mapper = 'kpartx -d ' + target_vm
	subprocess.call(remove_raw_mapper, shell=True)
#
# Create the new domain
#
	dom = conn.defineXML(newxml)
	dom.create()
	return ret
#
# start of main program
#
# VM settings
#
new_vm_lists = [ 'vm01','vm02','vm03','vm04', 'vm05' ]
current_vm_lists = []
RAW_PATH = '/var/lib/libvirt/images/'
#
# check the number of arguments
#
if len(sys.argv)<2:
	print "Usage: %s <base_raw_image> <base_xml_file>" % sys.argv[0]
	print "       File extension is not required."
	print "Example: %s base base" % sys.argv[0]
	exit(1)

base_rawfile = RAW_PATH + sys.argv[1] + ".raw"
xml_file = os.path.abspath(sys.argv[2] + ".xml")
#
# check the existence of the base file 
#
if not os.path.exists(base_rawfile):
	print " %s is not existing!" % base_rawfile
	exit(1)

if not os.path.exists(xml_file):
	print " %s is not existing!" % xml_file
	exit(1)
#
# collect all of the existing domains for checking the duplication 
#
conn = libvirt.open("qemu:///system")
alldomains = conn.listAllDomains()

for dm in alldomains:
	current_vm_lists.append(dm.name())

print '\nBase image file is %s. ' % base_rawfile
new_vm_count = 0
#
# start to deploy the specified VMs
#
for vm in new_vm_lists:
	if any(vm in domain for domain in current_vm_lists):
		print "The name %s you specified is already exist!" % vm
		continue
	else:
		raw_filename =  RAW_PATH + vm + '.raw'
		if os.path.exists(raw_filename):
			print "The raw file " + raw_filename + " is exist"
			continue
#
# Everything is OK, and go to deploy the virtual machine
#
	if deploy_vm(base_rawfile, xml_file, vm, raw_filename):
		new_vm_count += 1

print "Done!\n %s Virtual Machines have been deployed!" % new_vm_count

由於是自用,所以在程式中省略了許多的錯誤檢查,實用上還是自行加入為宜。亂數產生Mac Address的副程式,取自這個網址:randomMAC。此外,在程式中,我們使用defineXML來建立永久的Domain,如果你要建立的是暫時的Domain,在shutdown之後就會消失的,則直接使用createXML函數即可。

(184)