什么是KVM

KVM(Kernel-based Virtual Machine)是基于内核的虚拟化解决方案,目前Intel和AMD的CPU都对其提供了好很的支持。

平时KVM也会被说成是管理KVM虚拟化的一个工具,类似于qemu(quick emulator)。在网上看的文档,有的说KVM只能在物理机上做虚拟化,而qemu还适合在虚拟机上面进一步做虚拟化。目前大家常用的KVM虚拟化工具都是qemu。

什么是libvirt

libvirt是一套免费、开源的支持Linux下主流虚拟化工具的C函数库,这个函数库是一种实现Linux虚拟化功能的API,它支持虚拟机监控程序,比如Xen, KVM, Qemu等。

KVM虚拟化镜像制作

创建img镜像文件(快照)

# cd /var/instances
# qemu-img create ubuntu.img 5G

使用qemu-img来创建一个空的镜像文件,可以用-f参数来指定镜像文件格式,默认为raw。

在img文件上安装虚拟机系统

# cd /var/instances
# kvm -hda ubuntu.img -cdrom ubuntu-12.04-server-amd64.iso -boot d -m 512

注:在CentOS下面,kvm命令为qemu-kvm,在软件包qemu-kvm里。以上两步操作可以一步完成。

# cd /var/instances
# virt-install --name=ubuntu --ram=512 --vcpu=2 \
--disk path=/var/instances/ubuntu.img,size=5 \
--cdrom=/var/images/ubuntu-12.04-server-amd64.iso \
--graphics=vnc,listen=0.0.0.0
#

virt-install命令在virtinst包里,在CentOS下该包名为python-virtinst,其实最终调用的命令还是qemu-img和qemu-kvm。

按照提示完成操作系统的安装

打开VNC客户端,进入安装界面,然后一步步的完成安装。安装完成之后会在相应目录下生成镜像文件及xml配置文件(/etc/libvirt/qemu/ubuntu.xml)。

注意:安装过程中请只分一个分区(通常叫sda1),并将所有空间都分配给这个分区,安装完成之后会生成一个大小为5G的镜像文件​​/var/instances/ubuntu.img​​。

制作qcow2格式镜像

安装完成之后生成的​​ubuntu.img​​​是可以启动(bootable)的​​raw​​​格式文件,这个比较占用空间,你可以用以下命令将其压缩并转换成​​qcow2​​格式。

# virt-sparsify --compress --convert qcow2 ubuntu.img ubuntu.qcow2

最终会得到一个可以启动(bootable)的​​qcow2​​​格式镜像​​ubuntu.qcow2​​。

制作raw格式镜像

  • 挂载镜像

由于你在安装操作系统时分了一个分区(sda1),所以你是无法直接挂载镜像的。如果你用命令​​mount -o loop ubuntu.img /mnt​​​挂载镜像时会提示​​mount: you must specify the filesystem type​​​,这是因为你这个镜像有偏移量,即并是不所有空间都分配给了​​sda1​​,这时你就需要来将操作系统根分区(/)提取出来。

  • 挂载有偏移量的镜像
# cd /var/instances
# sfdisk -l -uS ubuntu.img
-------- output of sfdisk command --------
Disk ubuntu.img: cannot get geometry

Disk ubuntu.img: 652 cylinders, 255 heads, 63 sectors/track
Warning: extended partition does not start at a cylinder boundary.
DOS and Linux will interpret the contents differently.
Units = sectors of 512 bytes, counting from 0

Device Boot Start End #sectors Id System
ubuntu.img1 * 2048 9437183 9435136 83 Linux
ubuntu.img2 9439230 10483711 1044482 5 Extended
ubuntu.img3 0 - 0 0 Empty
ubuntu.img4 0 - 0 0 Empty
ubuntu.img5 9439232 10483711 1044480 82 Linux swap / Solaris

从以上输出可以看到这个镜像其实包含了5个分区!!!

我们要想挂载第一个分区,需要知道它的偏移量为​​512*2048=1048576​​​,然后挂载​​mount -o loop,offset=1048576 ubuntu.img /mnt/​​。

注: 偏移量是柱面的偏移数,实际偏移的字节数等于512*柱面偏移数,sfdisk命令输出的第三列即为柱面偏移数。

  • 提取根(/)文件系统
# cd /var/instances
# losetup -f ubuntu.img
# losetup -a

/dev/loop0: [0811]:56885251 (/var/instances/ubuntu.img)

# sfdisk -l -uS /dev/loop0

Disk /dev/loop0: cannot get geometry

Disk /dev/loop0: 652 cylinders, 255 heads, 63 sectors/track
Warning: extended partition does not start at a cylinder boundary.
DOS and Linux will interpret the contents differently.
Units = sectors of 512 bytes, counting from 0

Device Boot Start End #sectors Id System
/dev/loop0p1 * 2048 9437183 9435136 83 Linux
/dev/loop0p2 9439230 10483711 1044482 5 Extended
/dev/loop0p3 0 - 0 0 Empty
/dev/loop0p4 0 - 0 0 Empty
/dev/loop0p5 9439232 10483711 1044480 82 Linux swap / Solaris

# losetup -d /dev/loop0
# losetup -f -o 1048576 ubuntu.img
# losetup -a
/dev/loop0: [0811]:56885251 (/var/instances/ubuntu.img), offset 1048576
# dd if=/dev/loop0 of=ubuntu.final.img

以上这些操作就是提取根(/)文件系统的步骤。首先找到ubuntu.img镜像的根所在的分区的偏移量,并用losetup命令挂载成loop设备,然后使用dd命令将该loop设备(/dev/loop0)转换成文件,而这个文件就是最终的镜像文件。

将内核文件及ramdisk提取出来:

# cd /var/instances
# mount -o loop ubuntu.final.img /mnt
# cp /mnt/boot/vmlinuz-3.2.0-23-generic kernel
# cp /mnt/boot/initrd.img-3.2.0-23-generic ramdisk

最终你会得到一个没有偏移量的可以直接挂载的​​raw​​​格式的镜像​​ubuntu.final.img​​​,一个​​kernel​​​和一个​​ramdisk​​文件。

对虚拟机镜像进行定制

用以下命令挂载并操作镜像(qcow2):

# mount -o loop ubuntu.final.img /mnt
# mount --bind /dev /mnt/dev
# mount --bind /proc /mnt/proc
# chroot /mnt

用以下命令挂载并操作镜像(raw):

# mount -o loop ubuntu.final.img /mnt
# mount --bind /dev /mnt/dev
# mount --bind /proc /mnt/proc
# chroot /mnt

接下来你就可以对镜像就可以各种操作了,比如安装软件包、修改相关配置等等。

镜像制作2

当你看了之前的制作镜像步骤后会发现很麻烦,现在介绍一种比较简单的制作方法。

# qemu-img create ubuntu-12.10.img 10G
# mkfs.ext4 -F -q ubuntu-12.10.img
# kvm -hda ubuntu-12.10.img -cdrom ubuntu-12.10-server-amd64.iso -boot d -m 1024 -vnc 0.0.0.0:20
# mount -o loop ubuntu-12.10.img /mnt
# cp /mnt/boot/initrd.img-3.5.0-17-generic ramdisk
# cp /mnt/boot/vmlinuz-3.5.0-17-generic kernel
# tune2fs -L new-lable ubuntu-12.10.img
# vim /mnt/etc/fstab
edit to the format you like
LABEL=new-lable / ext4 defaults 0 0
/dev/vda / ext4 defaults 0 0
# umount /mnt

以上几条命令的意思是将系统安装在整个磁盘上,即磁盘不分区,也就意味着这需要你自己指定内核来启动系统,详情请参见​​instance-hostname.xml​​文件中的配置。

注意:这种制作方式我只在Ubuntu系统上成功实验过,CentOS貌似不能将操作系统安装在一个没有分区的镜像上面。

挂载qcow2格式的文件

# modprobe nbd max_part=16
# qemu-nbd -c /dev/nbd0 disk.qcow2
# partprobe /dev/nbd0
# mount /dev/nbd0 /mnt
# umount /mnt
# qemu-nbd -d /dev/nbd0

以上几条命令的意思是给内核添加​​nbd​​​模块,使用​​qemu-nbd​​​命令去连接(connect)文件​​disk.qcow2​​​,通知操作系统分区表已经被修改,挂载与卸载,断开连接(disconnect)。其中​​max_part=16​​​表示每块nbd设备支持的分区个数(​​modinfo nbd​​​),命令​​modprobe -r nbd​​来删除内核中的nbd模块。

更多相关内容: ​​ http://en.wikibooks.org/wiki/QEMU/Images#Mounting_an_image_on_the_host​

KVM虚拟机管理

按照以上方式创建完成虚拟机镜像之后libvirt会自动生成一个xml配置文件,管理虚拟机主要就靠这个xml文件了,管理的命令是virsh。

# virsh list
# virsh list --all
# virsh create /etc/libvirt/qemu/ubuntu.xml
# virsh define /etc/libvirt/qemu/ubuntu.xml
# virsh undefine /etc/libvirt/qemu/ubuntu.xml
# virsh autostart domain_id|instance_name
# virsh destory domain_id|instance_name
# virsh start/shutdown/reboot/... domain_id|instance_name

如果你已经创建好虚拟机的镜像了,并且xml文件模板也已经有了,那么就可以直接用virsh来进行管理,免去了以上那么多的麻烦。

Ubuntu已经有自己做好的虚拟机镜像了,可以直接拿去使用。Ubuntu Enerprise Cloud(UEC): ​​ http://uec-images.ubuntu.com/releases/​

libvirt XML文件格式

常规信息区域

<domain type='xen' id='3'>
<name>instance-name</name>
<uuid>d9ef885b-634a-4437-adb6-e7abe1f792a5</uuid>
<title>A short description - title - of the domain</title>
<description>Some human readable description</description>
<metadata>
<app1:foo xmlns:app1="http://app1.org/app1/">..</app1:foo>
<app2:bar xmlns:app2="http://app1.org/app2/">..</app2:bar>
</metadata>
...

其中,type是虚拟化类型,其值可以是kvm, xen, qemu, lxc, kqemu等。id是标识正在运行的虚拟机,可以省略。

  • name

虚拟机的名字,可以由数字、字母、中横线和下划线组成。

  • uuid

虚拟机的全局唯一标识,可以用​​uuidgen​​命令生成。如果在定义(define)或创建(create)虚拟机实例时省略,系统会自动分配一个随机值这个实例。

  • title, description

这两个东西都可以省略,见名知义,如果有特殊需求可以加上。

  • metadata

metadata可以被应用(applications)以xml格式来存放自定义的metadata,该项也可以省略。

操作系统启动区域

...
<os>
<type arch='x86_64' machine='pc'>hvm</type>
<boot dev='hd'/>
<bootmenu enable='yes'/>
<kernel>/var/instances/instance-hostname/kernel</kernel>
<initrd>/var/instances/instance-hostname/ramdisk</initrd>
<cmdline>root=/dev/vda console=ttyS0</cmdline>
</os>
...
  • type

虚拟机启动的操作系统类型,hvm表示操作系统是在裸设备上运行的,需要完全虚拟化。

  • boot

boot属性的值可以是fd, hd, cdrom, network等,用来定义下一个启动方式(启动顺序)。该属性可以有多个。

  • bootmenu

在虚拟机启动时是否弹出启动菜单,该属性缺省是弹出启动菜单。

  • kernel

内核镜像文件的绝对路径。

  • initrd

ramdisk镜像文件的绝对路径,该属性是可选的。

  • cmdline

这个属性主要是在内核启动时传递一些参数给它。

内存和CPU区域

...
<vcpu placement='static' cpuset="1-4,^3,6" current="1">2</vcpu>
<memory unit='KiB'>2097152</memory>
<currentMemory unit='KiB'>2000000</currentMemory>
...
  • vcpu

vcpu属性表示分配给虚拟机实例的最大CPU个数。其中cpuset表示该vcpu可以运行在哪个物理CPU上面,一般如果设置cpuset,那么placement就设置成static。current的意思是是否允许虚拟机使用较少的CPU个数(current can be used to specify whether fewer than the maximum number of virtual CPUs should be enabled)。vcpu下面的这几个属性貌似只在kvm与qemu中有。

  • memory

memory表示分配给虚拟机实例的最大内存大小。unit是内存的计算单位,可以是KB, KiB, MB, MiB,默认为Kib。(1KB=10^3bytes,1KiB=2^10bytes)

  • currentMemory

currentMemory表示实际分配给虚拟实例的内存,该值可以比memory的值小。

磁盘区域

...
<devices>
<emulator>/usr/bin/kvm</emulator>

<disk type='file' device='disk'>
<source file='/var/instances/instance-hostname/disk' />
<target dev='vda' bus='ide' />
</disk>

<disk type='file' device='disk'>
<driver name='qemu' type='raw'/>
<source file='/var/instances/instance-hostname/disk.raw'/>
<target dev='vda' bus='virtio'/>
<alias name='virtio-disk0'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
</disk>

<disk type='block' device='disk'>
<driver name='qemu' type='raw'/>
<source dev='/dev/sdc'/>
<geometry cyls='16383' heads='16' secs='63' trans='lba'/>
<blockio logical_block_size='512' physical_block_size='4096'/>
<target dev='hda' bus='ide'/>
</disk>
</devices>
...
  • emulator

模拟器的二进制文件全路径。

  • disk

定义单块虚拟机实例上的磁盘。

type 可以是block, file, dir, network。分别表示什么意思就不多说了。

device 表示该磁盘以什么形式暴露给虚拟机实例,可以是disk, floppy, cdrom, lun,默认为disk。

  • driver

可以定义对disk更为详细的使用结节。

  • source

定义磁盘的源地址,由type来确定该值应该是文件、目录或设备地址。

  • target

控制着磁盘的bus/device以什么方式暴露给虚拟机实例,可以是ide, scsi, virtio, sen, usb, sata等,如果未设置的系统会自动根据设备名字来确定。如: 设备名字为hda那么就是ide。

  • mirror

这个mirror属性比较牛B,可以将虚拟机实例的某个盘做镜像,具体不细说了。

网络接口区域

...
<devices>
<interface type='bridge'>
<source bridge='br0'/>
<mac address='00:16:3e:5d:c7:9e'/>
<model type='virtio'/>
</interface>
</devices>
...

以上内容就不多解释了,看名知义。

相关事件的配置

...
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>restart</on_crash>
<on_lockfailure>poweroff</on_lockfailure>
...
  • on_poweroff, on_reboot, on_crash

其属性值为遇到这三项时进行的操作,可以是以下操作:

destroy 虚拟机实例完全终止并且释放所占资源。

restart 虚拟机实例终止并以相同配置重新启动。

preserve 虚拟机实例终止,但其所占资源保留来做进一步的分析。

rename-restart 虚拟机实例终止并且以一个新名字来重新启动。

on_crash 还支持以下操作:

coredump-destroy crash的虚拟机实例会发生coredump,然后该实例完全终止并释放所占资源。

coredump-restart crash的虚拟机实例会发生coredump,然后该实例会重新启动。

  • on_lockfailure(我对这个不了解)

当锁管理器(lock manager)失去对资源的控制时(lose resource locks)所采取的操作:

poweroff 虚拟机实例被强制停止。

restart 虚拟机实例被停止后再启动来重新获取它的锁(locks)。

pause 虚拟机实例会被暂停,并且当你解决了锁(lock)问题后可以将其手动恢复运行。

ignore 让虚拟机实例继续运行,仿佛一切都没发生过。

时间区域

<clock offset='localtime'/>

offset支持utc, localtime, timezone, variable等四个值,表示虚拟机实例以什么方式与宿主机同步时间。(并不是所有虚拟化技术都支持这些模式)

图形管理接口

...
<devices>
<graphics type='vnc' port='5904'>
<listen type='address' address='1.2.3.4'/>
</graphics>
<graphics type='vnc' port='-1' autoport='yes' keymap='en-us' listen='0.0.0.0'/>
</devices>
...

type为管理类型,可以是VNC,rdp等。其中port可以自动分配(从5900开始分配)。

日志记录

...
<devices>
<console type='stdio'>
<target port='1'/>
</console>
<serial type="file">
<source path='/var/instances/instance-hostname/console.log'/>
<target port="1"/>
</serial>
</devices>
...

以上意思是禁止字符设备的输入,并将其输出定向到虚拟机的日志文件中(domain log)。将设备的日志写到一个文件里(Device log),比如:开机时的屏幕输出。

如你所想,libvirt的XML配置文件不可能就这么项内容,还有很多很多配置及详细配置,我在此不写出了,想深入了解的话可以看参考资料部分。

使用示例

镜像文件及XML文件位置: /var/instances/base ,要创建的虚拟机位置: /var/instances/instance-name/,操作步骤如下:

# cd /var/instances/base
# ls
ubuntu.final.img ramdisk kernel instance-name.xml
# cp -rf /var/instances/base /var/instances/instance-host180
# cd /var/instances/instance-host180
# mount -o loop ubuntu.final.img /mnt
# vim /mnt/etc/network/interfaces
edit eth0 and eth1 section
CentOS interface file: /etc/sysconfig/network-scripts/ifcfg-eth0[1]
# umount /mnt
# mv instance-name.xml instance-host180.xml
# vim instance-host180.xml
edit some sections of this XML file
# virsh define instance-host180.xml
# virsh create instance-host180.xml
# virsh autostart host180

经过以上几步,一台虚拟机就创建OK了。当然如果虚拟机使用DHCP方式来获取IP的话,就根本不用修改网卡配置文件。

其他说明

本文档同级目录下有一个XML文件模板,可以参考一下。

这个文档是step by step,注重操作和使用。我当时就是这样一步步学习的,当使用熟练之后可以进行更深入的研究学习。

新加内容

主要是关于​​raw​​​和​​qcow2​​格式的相互转换及文件大小压缩相关。

扩大raw或qcow2格式文件

改变raw或qcow2格式的文件大小。

# virt-filesystems --long -h --all -a old.file.raw
Name Type VFS Label MBR Size Parent
/dev/sda1 filesystem ext4 - - 10G -
/dev/sda1 partition - - 83 10G /dev/sda
/dev/sda device - - - 10G -
# truncate -r old.file.raw new.file.raw
# truncate -s +5G new.file.raw
# virt-resize --expand /dev/sda1 old.file.raw new.file.raw

the other way

# virt-filesystems --long -h --all -a old.file.raw
Name Type VFS Label MBR Size Parent
/dev/sda1 filesystem ext4 - - 10G -
/dev/sda1 partition - - 83 10G /dev/sda
/dev/sda device - - - 10G -
# cp -pf old.file.raw new.file.raw
# qemu-img resize new.file.raw +2G
# virt-resize --expand /dev/sda1 old.file.raw new.file.raw

or

仅当这个镜像​​disk.raw​​是个没有偏移量的镜像时:

# qemu-img resize disk.raw +2G
# e2fsck -f disk.raw
# resize2fs disk.raw

挂载qcow2或raw格式文件

用libguestfs挂载qcow2和raw格式的文件

# guestmount -a disk.file -i /mnt

注:Ubuntu下安装guestmount时会有个提示,如果选否的话会导致在挂载时出错​​libguestfs: error: /usr/bin/supermin-helper exited with error status 1​​​,这时需要执行​​sudo update-guestfs-appliance​​来解决,其实在你选否的时候已经告诉你可以在安装完成之后手动执行这条命令了。

压缩qcow2或raw格式文件的大小

  • 压缩​​qcow2​​格式的文件
# virt-sparsify --compress --convert qcow2 --format qcow2 input.qcow2 output.qcow2
  • ​raw​​格式的文件不支持压缩,但是可以缩小

​raw​​​格式文件整起来比较费劲,这里我只做当​​raw​​​文件是一个​​ext4​​文件系统的情况。

# qemu-img create disk.final.raw 10G
# // dd if=/dev/zero of=disk.final.raw bs=512 seek=1000000
# mkfs.ext3 -Fq disk.final.raw
# mount -o loop disk.origin.raw /origin
# mount -o loop disk.final.raw /final
# cp -arf /origin/* /final/*
# umount /final
# umount /origin

卸载挂载点

# fuser -m /mnt
# fuser -k /mnt
# umount /mnt
# umount -l /mnt

让虚拟机可以访问宿主机的网络

很多时候,你创建的虚拟机网段和宿主机是完全不一样的,而这时你想让你创建的虚拟机可以访问宿主机能访问到的网络。

假如宿主机上的 Bridge 是 br0 ,其上的 IP 地址是 ​​192.168.0.1​​​,虚拟机网段是 ​​192.168.0.0/24​​。

# iptables -t nat -A POSTROUTING -s 192.168.0.0/24 ! -d 192.168.0.0/24 -p tcp -j MASQUERADE --to-ports 1024-65535
# iptables -t nat -A POSTROUTING -s 192.168.0.0/24 ! -d 192.168.0.0/24 -p udp -j MASQUERADE --to-ports 1024-65535
# iptables -t nat -A POSTROUTING -s 192.168.0.0/24 ! -d 192.168.0.0/24 -j MASQUERADE
# iptables -t mangle -A POSTROUTING -o br0 -p udp -m udp --dport 68 -j CHECKSUM --checksum-fill
# iptables -t filter -A INPUT -i br0 -p udp -m udp --dport 53 -j ACCEPT
# iptables -t filter -A INPUT -i br0 -p tcp -m tcp --dport 53 -j ACCEPT
# iptables -t filter -A INPUT -i br0 -p udp -m udp --dport 67 -j ACCEPT
# iptables -t filter -A INPUT -i br0 -p tcp -m tcp --dport 67 -j ACCEPT
# iptables -t filter -A FORWARD -d 192.168.0.0/24 -o br0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# iptables -t filter -A FORWARD -s 192.168.0.0/24 -i br0 -j ACCEPT
# iptables -t filter -A FORWARD -i br0 -o br0 -j ACCEPT
# iptables -t filter -A FORWARD -o br0 -j REJECT --reject-with icmp-port-unreachable
# iptables -t filter -A FORWARD -i br0 -j REJECT --reject-with icmp-port-unreachable

问题

  • Cannot open /dev/urandom for reading: No such file or directory

执行 ​​mount --bind /dev /mnt/dev​​ 来解决,其他相似问题用类似处理方法。