📜 ⬆️ ⬇️

Work with KVM virtual machines. Virtual machine cloning

Clone

We continue a series of articles on virtualization based on KVM. In previous articles it was told about the toolkit , about setting up a host machine and creating a virtual machine . Today we will talk about creating an image of a virtual machine and its cloning.


')
The research of the question gave depressing results: the information on creating images of virtual machines on the network is very difficult to find, and the one that is, the quality and completeness is not different.

To obtain an image of a virtual machine in a minimal system, it is enough to change just a couple of files in it to get a normally working system, but in the case of Debian there are some minor difficulties.

To create a new virtual machine based on the existing system, you need to make the following changes:



The libguestfs library turned out to be a great find for me - it allows you to manage disks and operate virtual machine files both interactively and according to a predefined script.

This library was written by Richard Jones of the notorious Red Hat company. It allows you to work with file systems (starting from ext2 and ending with NTFS in Windows, UFS in FreeBSD - in general, with all the file systems with which the kernel can work), system images, LVM partitions, in case of installing guest OS from the MS family Windows - edit the registry (via the hivex library). In general, the utility is very feature rich and very flexible. And most importantly - does not require administrative (root) rights to use it.

Explore the image



So let's get down to work.

The main tool with which we will work with the image of the guest system is guestfish .

Let's try to perform some operations online:

$ guestfish
><fs> add-drive debian_5_i386.img
><fs> run
><fs> list-filesystems
/dev/vda1: ext3
><fs> mount-vfs rw ext3 /dev/vda1 /
><fs> cat /etc/fstab
# /etc/fstab: static file system information.
#
# <file system> <mount point> <type> <options> <dump> <pass>
proc /proc proc defaults 0 0
/dev/vda1 / ext3 errors=remount-ro 0 1
/dev/hdc /media/cdrom0 udf,iso9660 user,noauto 0 0


What is very cool - all the necessary operations can be performed in non-interactive mode (according to a pre-compiled script). I will give an example of a script that edits the hosts, hostname and interfaces files in the system:

$ guestfish <<EOF
add-drive debian_guest.img
run
mount-vfs rw ext3 /dev/vda1 /
upload -<<END /etc/hosts
127.0.0.1 localhost.localdomain localhost debian_guest.local debian_guest
10.10.10.100 debian_guest.local
END
upload -<<END /etc/resolv.conf
nameserver 8.8.8.8
END
upload -<<END /etc/hostname
debian_guest.local
END
upload -<<END /etc/network/interfaces
auto lo
iface lo inet loopback
allow-hotplug eth0
iface eth0 inet static
address 10.10.10.100
gateway 10.10.10.10
netmask 255.255.255.0
network 10.10.10.0
broadcast 10.10.10.255
END
EOF


The use of heredoc was very convenient in this context.

(By the way: if there are any questions about the library, the author himself responds very quickly to the IRC channel #libguestfs on irc.freenode.net. And indeed the guy is very interesting.)

Secure hell



As the name implies, I suffered for a long time with this question: in Debian / Ubuntu, there is simply no automatic key regeneration when they are deleted. In other systems that I tried to use, this is all right, but for deb-based operating systems there are problems with this.

I did this:

$ guestfish
><fs> add-drive debian_guest.img
><fs> run
><fs> mount-vfs rw ext3 /dev/vda1 /
><fs> download /etc/init.d/ssh /home/username/debian_5_etc_init_ssh


Further, the following changes were made:

--- /home/username/debian_5_etc_init_ssh 2012-12-21 00:00:00.000000000 +0000
+++ /home/username/debian_5_etc_init_ssh_fixed 2012-12-21 00:00:00.000000000 +0000
@@ -32,6 +32,10 @@
([ "$previous" ] && [ "$runlevel" ]) || [ "$runlevel" = S ]
}

+check_ssh_host_key() {
+ if [ ! -e /etc/ssh/ssh_host_key ] ; then
+ echo "Generating Hostkey..."
+ /usr/bin/ssh-keygen -t rsa1 -f /etc/ssh/ssh_host_key -N '' || return 1
+ fi
+ if [ ! -e /etc/ssh/ssh_host_dsa_key ] ; then
+ echo "Generating DSA-Hostkey..."
+ /usr/bin/ssh-keygen -d -f /etc/ssh/ssh_host_dsa_key -N '' || return 1
+ fi
+ if [ ! -e /etc/ssh/ssh_host_rsa_key ] ; then
+ echo "Generating RSA-Hostkey..."
+ /usr/bin/ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N '' || return 1
+ fi
+}
+
check_for_no_start() {
# forget it if we're trying to start, and /etc/ssh/sshd_not_to_be_run exists
if [ -e /etc/ssh/sshd_not_to_be_run ]; then
@@ -75,6 +79,7 @@

case "$1" in
start)
+ check_ssh_host_key
check_privsep_dir
check_for_no_start
check_dev_null
@@ -106,6 +111,7 @@
;;

restart)
+ check_ssh_host_key
check_privsep_dir
check_config
log_daemon_msg "Restarting OpenBSD Secure Shell server" "sshd"


Attention, the patch is not working, it is given as an example of the necessary changes.

And for two versions of Debian / Ubuntu, I made a similar file with an already modified ssh file. Then you can simply load it into a virtual machine.

><fs> upload /home/username/debian_5_etc_init_ssh_fixed /etc/init.d/ssh

Now delete the keys so that they are generated automatically:

><fs> glob rm /etc/ssh_host_*_key*

Deleting by mask does not work. Since this method is not implemented in the API, the glob prefix allows you to expand the mask into a list of files.

For FreeBSD and CentOS, it is enough just to delete the keys, they will be generated at startup.

User identification



First of all, it’s worthwhile to talk about how the storage of information about users in Linux / FreeBSD is presented. It will be a bit boring, but necessary to understand what we are doing. Although at a minimum there is enough information only about the shadow file.

All necessary for user authentication is stored in the / etc / passwd and /etc/shadow (/etc/master.passwd in FreeBSD) files.

Consider the structure of the / etc / passwd file

root:x:0:0:root:/root:/bin/bash

I will quote from the wiki how to use the fields:



Consider the structure / etc / shadow

root:$1$APv1HQOB$HJQhYFq9JSnhusQ.1Ql10.:14977:0:99999:7:::

Again from the wiki :



We need to change specifically the second field (password hash). It can be divided into three parts:



The hash is generated by the command:

$ mkpasswd --method=md5 --salt="APv1HQOB" "$password"
$1$APv1HQOB$HJQhYFq9JSnhusQ.1Ql10.


We need to substitute it in the / etc / shadow file.

I wrote a small script that will generate a random password and an 8-character salt, output it, generate a hash and substitute it into the desired file:

#!/bin/bash
tempfile=`mktemp`
shadow="/etc/shadow"
salt=`pwdgen`
passwd=`pwdgen`
hash=`pwhash $salt $password`
hash_esc=`escape_hash $hash `
pwdgen() {
charspool=('a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' 'x' 'y' 'z' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' '0' 'A' 'B' 'C' 'D' 'E' 'F' 'G' 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' 'P' 'Q' 'R' 'S' 'T' 'U' 'V' 'W' 'X' 'Y' 'Z');
len=${#charspool[*]}
for c in $(seq 8); do
echo -n ${charspool[$((RANDOM % len))]}
done
}

pwhash(){
salt=$1
password=$2
hash=`mkpasswd --method=md5 --salt=$salt $password`
echo $hash
}

# , sed $
escape_hash() {
echo $1 | sed -e 's/\//\\\//g' -e 's/\$/\\\$/g'
}

guestfish <<EOF
add-drive debian_guest.img
run
mount-vfs rw ext3 /dev/vda1 /
download /etc/shadow $tempfile
! sed 's/^root:[^:]\+:/root:$hash_esc:/' $tempfile > $tempfile.new
upload $tempfile.new $shadow
EOF


As you probably noticed, we used an external command inside the script, in which we replaced the contents of the first section with the hash received in the script. The external operator " ! " Is used for this: it is very convenient when we need to do some small operations without interrupting the process of working with guestfish (since it takes some time to launch the guestfish ).

Preparing Master Image



Since the images need to be updated periodically (in case of important updates or errors in the image are corrected), we should prepare master images in which we will perform the necessary manipulations. For deployment, we will prepare these images using a separate script.

What we need to remove in our image:

  1. Clear logs
  2. Remove traces of being in the system
  3. Remove downloaded packages (relevant for Debian and Ubuntu, only they are littering)
  4. Delete the file with the settings of the network card
  5. Delete keys.


After that we will need to reduce the size of the file system, reduce the partition and cut off the excess from the image.

I will attach a small section of code that performs the first part of the required action:

guestfish <<EOF
add-drive debian_guest.img
run
mount-vfs rw ext3 /dev/vda1 /
upload /home/username/debian_5_etc_init_ssh_fixed /etc/init.d/ssh
-glob rm /etc/ssh/ssh_host_*
-glob rm /etc/udev/rules.d/70-persistent-net.rules
-glob rm /root/*
-glob rm /root/.*
-glob rm /var/log/*
-glob rm /var/cache/apt/archives/*deb

EOF

The "-" flag in front of the command means that we should not exit if any of the commands returns -1 . This was done on purpose so that the absence of any files does not interrupt the execution of the remaining commands; thus, customization of this script for different distributions becomes unnecessary, although it is possible.

And now we proceed to reducing the image:

$ guestfish <<EOF
add-drive add-drive ${images}/${os}_${version}_${arch}.img
run
e2fsck-f /dev/vda1
resize2fs-M /dev/vda1
tune2fs /dev/vda1 | grep "Block count:" | sed -e 's/Block\ count:\ //g' -e 's/$/*4+2144/g' | bc > /tmp/block_count
EOF
$ foo=`cat /tmp/block_count`
$ guestfish <<EOF
allocate debian_guest_minimal.img ${foo}k
EOF


The number 2144 is the size of the boot loader and partition table.

Briefly, the essence of what was done is as follows: we shrink the file system to the minimum size, calculate how much it began to occupy (the minimum number of blocks), and multiply them by 4, since the block size is 4 kB, then create an image of the resulting value.

After that, we will need to use the virt-resize utility from the libguestfs toolkit to transfer the resulting file system to a new, smaller image.

$ virt-resize --shrink /dev/vda1 debian_guestl.img debian_guest_minimal.img

You should immediately discuss the limitations of this method: this is applicable only to ext2-4 file systems, since resize2fs only works with them. For something non-standard, you can easily finish the necessary functionality (although, as I mentioned earlier , libguestfs is very difficult to build). For a sample, you can view my patch to implement resize2fs-M .

Unfortunately, with FreeBSD everything is much more complicated, and so far there are no options for solving the problem with it except adding another disk to the virtual machine config and mounting it.

Now we must, of course, optionally pack the resulting image with xz (this is a long time, but the result is worth it):

$ xz -9 debian_guest.img
$ ls -lsha debian_guest.img.xz
107M -rw-r--r-- 1 username username 107M Dec 21 00:00 debian_guest.img.xz


Unfolding image



So, we got the virtual machine image, but the images are not ready-made working systems. To get a working system, we need to perform several operations:

  1. Allocate image to disk
  2. Copy bootloader and partition table
  3. Transfer information from the template to the virtual machine image
  4. Expand file system
  5. Change root password
  6. register network settings


For Linux, everything is simple : as part of libguestfs, there is a wonderful utility written in OCaml - virt-resize , items 2 through 4 are executed by it without problems.

For a number of reasons, it is impossible to implement a resizing of a disk on guestfish (copying mbr into guestfish is impossible), therefore more functional means should be used.

$ guestfish <<EOF
allocate debian_guest_clone.img 10G
EOF
$ virt-resize --expand /dev/vda1 debian_guest.img debian_guest_clone.img


Actually, this is all that is minimally required to know to implement the cloning of images of virtual machines.

The next article will tell about the limitation of virtual machine resources.

Source: https://habr.com/ru/post/121218/


All Articles