18 mai 2022

Tuto Virtualisation avec KVM, partie VIII : Scripts d’automatisation

Dans cette huitième partie de la série sur KVM, nous allons voir comment automatiser la création de nos VMs afin de ne pas avoir à faire à la main tout ce que je vous ai expliqué jusqu’à présent.

Il se peut que vous arriviez ici suite à :

Pour information, entre cet article et les précédents de la série dédiée à KVM, il y a eu du changement au niveau de mon stockage et de mon réseau.
Du coup, ici, on trouvera les versions adaptées à l’usage de KVM fait dans cette série Virtualisation 2022 avec KVM.

Je vous propose donc ici quelques scripts pour :

  • créer un template prêt à déployer
  • créer des VMs à partir de ce template
  • supprimer des VMs

C’est loin d’être parfait, mais ça a le mérite d’exister et de fonctionner.

 

I – Script Make.sh

Script nommé make.sh qui permet de générer une archive TAR avec une Debian destinée à nos futures bécanes.

Le script :

#!/bin/bash

name="debugo"
echo -e "\e[32m *** Création template ${name} *** \e[39m"
mkdir ${name}
echo "Deboostrap"
debootstrap --no-check-gpg --include=grub-pc,linux-image-amd64,locales --exclude=os-prober stable ${name} > /dev/null
echo "Configuration"

# Montages nécessaires pour le chroot
mount -t sysfs /sys ${name}/sys
mount -t proc /proc ${name}/proc
mount --bind /dev ${name}/dev
mount -t devpts /dev/pts ${name}/dev/pts
mount --bind /tmp ${name}/tmp

# Config date
LANG=C chroot ${name} /bin/bash -c "ln -fs /usr/share/zoneinfo/Europe/Paris /etc/localtime" > /dev/null 2>&1
LANG=C chroot ${name} /bin/bash -c "dpkg-reconfigure --frontend=noninteractive tzdata" > /dev/null 2>&1
# Config Locales
sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' ${name}/etc/locale.gen
sed -i -e 's/# fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' ${name}/etc/locale.gen
LANG=C chroot ${name} /bin/bash -c "dpkg-reconfigure --frontend=noninteractive locales" > /dev/null 2>&1
LANG=C chroot ${name} /bin/bash -c "update-locale LANG=fr_FR.UTF-8"

echo "Installation Package"
chroot ${name} /bin/bash -c "apt-get update && apt-get upgrade -y" > /dev/null
chroot ${name} /bin/bash -c "apt-get install acpid ssh python3 python3-apt ntp nftables sudo -y -q" > /dev/null 2>&1
chroot ${name} /bin/bash -c "systemctl enable acpid" > /dev/null 2>&1
chroot ${name} /bin/bash -c "apt-get remove iptables --purge -y -q" > /dev/null 2>&1
chroot ${name} /bin/bash -c "apt-get install htop bash-completion wget man-db git python3-apt apt-transport-https ca-certificates curl gnupg-agent software-properties-common -y -q" > /dev/null 2>&1
chroot ${name} /bin/bash -c "apt-get clean" > /dev/null 2>&1

echo -e "Création de l'archive"
umount ${name}/{sys,proc,dev/pts,dev,tmp};
tar zcvf ${name}.tar.gz ${name}/ > /dev/null
echo -e "\e[32mTemplate ${name} prêt !\e[39m"

Le script est simple : debootstrap, montages qui vont bien, configuration (date et locales), installation de paquets (bye iptable, hello nftables, gestion acpid, python, …).

A la différence de mon article ou je détaillais la préparation du template, on ne copie pas le squelette (les fichiers de configuration), cela sera fait lors de la création de la VM.

Ensuite, création de l’archive. Je ne supprime pas le répertoire, il servira pour les updates par la suite.

Tout ceci donc dans un fichier make.sh, puis :

# chmod +x make.sh
# ./make.sh

And voila !

En quelques minutes, un TAR tout chaud pour nos futures machines !

 

II – Création VM

Pour la création de VM,  le script est plus complexe, je le découpe donc en bloc pour le présenter.

Dans ce script, j’utilise des variables dans le fichier et des variables en ligne de commande. Pour les options qui ne bougent pas trop, j’ai fait le choix de ne pas les passer en CLI, car ça l’alourdit vraiment. Comme en général, je fais mes bécanes de mêmes types au même moment, je règle leurs tailles, RAM, CPU, dans le script, puis les options nominales par CLI.

Les options par CLI sont donc  le nom, l’ID (sert pour la génération de l’IP) et le stockage

./create -n nomdelavm -i sonid -s 1

Pour le stockage, j’ai deux VG (voir : Stockage avancé avec KVM, LVM et Raid) donc, je choisis 0 ou 1 avec l’option -s. VG1 pour les Master, VG0 pour les Slaves.

 

A – Variables, checks et récapitulatif

Créez un fichier create.sh et commencez par mettre ce qui suit dedans :

#!/bin/bash

# Options
size_swap=1
size_var=3
size=4
ram=1024
cpu=1


# Setup KVM
kvmnet="intern-vlans"
vlandef="vlansadmin"
vol_kvm_main="lvm1"
vg_main="vg1"
vol_kvm_back="lvm0"
vg_back="vg0"
template="debugo.tar.gz"
os="debian10"
dir_mount="/mnt"
user="toto"

On commence par définir des variables.

  • size_swap=1 : la taille du Swap, en Go
  • size_var=3 : la taille de /var, en Go
  • size=4 : la taille de /, en Go
  • ram=1024 : ram en Mo
  • cpu=1 : le nombre de cpus

Concernant la partitionnement, vous remarquerez que je mets /var à part.

Ensuite, les options « hyperviseur » :

  • kvmnet= »intern-vlans » : le nom du pool dans kvm
  • vlandef= »vlansadmin » : le nom du vlan dans kvm
  • vol_kvm_main= »lvm1″ : le nom du LVM Main dans KVM
  • vg_main= »vg1″ : le VG Main
  • vol_kvm_back= »lvm0″ : le nom du LVM Back dans KVM
  • vg_back= »vg0″ : le VG Back
  • template= »debugo.tar.gz » : l’archive à utiliser, créée avec le script make.sh
  • os= »debian10″ : le nom de l’os (Oui, meme pour Debian 11)
  • dir_mount= »/mnt » : le point de montage où l’on va faire notre tambouille.
  • user= »toto » : l’user par défaut

Pour le réseau, je connecte mes bécanes qu’à un seul port au démarrage. Celui ci achemine deux VLANs. Côté KVM, ce trunk est identifié sur le portgroup vlansadmin dans le réseau intern-vlans.

Plus d’informations à ce sujet ici : Gestion d’un réseau complexe KVM et Openvswitch.

Au niveau stockage, deux pools, un pour les Master, un pour les Slave, j’explique le principe ici : Stockage avancé avec KVM, LVM et Raid.

Et je termine en définissant un user par défaut. Il sera utilisé par Ansible par la suite. Et comme dans Ansible pour le moment, je ne me suis pas penché sur le stockage de mdp, pour le moment, j’utilise ce compte, en passant le mdp du compte en argument de mes playbooks (-K) afin qu’il puisse élever ses privilèges.

Ensuite, quelques checks :

while getopts n:i:s: flag
do
  case "${flag}" in
    n) name=${OPTARG};;
    i) id=${OPTARG};;
    s) stock=${OPTARG};;
  esac
done

fonction info() {
  echo -e "\e[$2m $1 \e[39m \n"
}
function input_pwd() {
  ok=0
  empty=1
  while [ $ok -eq 0 -o $empty -eq 1 ]
  do
    echo "Password ?"; read -s pwd
    echo "Confirmation ?"; read -s pwdc
    if [ ! -z "$pwd" -o ! -z "$pwdc" ]; then empty=0
    else empty=1; fi
    if [ "$pwd" = "$pwdc" ] && [ $empty -eq 0 ]; then ok=1
    else echo "Les MDP doivent etre identiques et non nuls!"; fi
  done
}
# Checks params
re='^[0-9]+$'
if [ ! -n "$name" ];then echo 'Name (-n) manque à la pelle, oui on peut être drôle...' >&2;exit 1;fi
if [ ! -n "$stock" ];then echo 'Emplacement stockage (-s) manque' >&2;exit 1;fi
if [ ! -n "$id" ];then echo 'Id (-i) manque' >&2;exit 1;fi
if [ -n "$id" ] && ! [[ $id =~ $re ]];then echo 'ID (-i) non numerique' >&2;exit 1;fi
if ! [[ "$id" -ge 1 && "$id" -le 254 ]];then echo 'ID (-i) non correct (doit être entre 1 et 254)' >&2;exit 1;fi
if [ -n "$size" ] &&! [[ $size =~ $re ]];then echo 'Size non numerique' >&2;exit 1;fi
if [ -n "$cpu" ] && ! [[ $cpu =~ $re ]];then echo 'Cpu non numerique' >&2;exit 1;fi
if [ -n "$ram" ] && ! [[ $ram =~ $re ]];then echo 'RAM non numerique' >&2;exit 1;fi
if [ -n "$size_swap" ] && ! [[ $size_swap =~ $re ]];then echo 'Swap non numerique' >&2;exit 1;fi
if [ -n "$size_var" ] && ! [[ $size_var =~ $re ]];then echo 'Var non numerique' >&2;exit 1;fi

Puis, des checks côté KVM, un recap et la demandes des mots de passes :

# Choix du vg en fonction de l'option passée
if [[ $stock = 1 ]];then vol_kvm=${vol_kvm_main};vg=${vg_main}
else vol_kvm=${vol_kvm_back};vg=${vg_back}
fi

# Verif espace libre
free=$(virsh pool-info ${vol_kvm} | grep "Available" | awk '{ split($2,v,","); print v[1]; }')
if [ $(($size+$size_swap+$size_var)) -ge ${free%.*} ];then
  info "Va falloir faire du ménage avant..." 31; exit 1
fi
# Check nom vm et id
if virsh dominfo ${name}> /dev/null 2>&1;then info "Une VM porte deja ce joli nom..." 31;exit 1;fi
if grep -w "10.99.1.${id}" /etc/hosts;then info "ID deja pris..." 31;exit 1;fi
# Affichage recap :
info "Creation de la VM ${name} avec CPU: ${cpu} / RAM: ${ram}MB IP: 10.X.1.${id}" 32
info "Tailles: /:${size}G - /var: ${size_var}G - SWAP: ${size_swap}G -> vol :${vol_kvm}" 32
info "Entrée pour valider, CTRL-C pour quitter" 32
read

# Lecture des mdp
info "Saisir mot de passe root" 33
input_pwd
pass=$pwd
info "Saisir mot de passe $user" 33
input_pwd
puser=$pwd

On a terminé avec les inputs et les vérifications.

 

B – Création des volumes et mise en place du template

Bon, arrivé la, on sait que tout est bon, on peut donc enfin y aller.

# Prep Vol dans LVM
info "Préparations Disques" 32
if ! virsh vol-create-as ${vol_kvm} ${name} ${size}G ; then info "Erreur /" 31; exit 1; fi
if ! virsh vol-create-as ${vol_kvm} ${name}-var ${size_var}G ; then info "Erreur /var" 31; exit 1; fi
if [ $size_swap -ne 0 ]; then
  if ! virsh vol-create-as ${vol_kvm} ${name}-swap ${size_swap}G ; then info "Erreur Swap" 31; exit 1; fi
fi
# Partitionnement disque /
if ! parted --script /dev/${vg}/${name} 'mklabel msdos' ; then info "Oups: parted" 31;exit 1; fi
if ! parted --script /dev/${vg}/${name} 'mkpart primary ext4 1MiB 100%'; then info "Oups: parted" 31;exit 1; fi

# Generation nom partition xxx1 ou xxx1p1
if [[ ${name: -1} =~ $re ]]; then part="${vg}-${name}p1"; else part="${vg}-${name}1";fi

# Création FS
mkfs.ext4 /dev/mapper/${part} -q -F > /dev/null
mkfs.ext4 /dev/${vg}/${name}-var -q -F > /dev/null
mkswap /dev/${vg}/${name}-swap > /dev/null

# Montages
mount /dev/mapper/${part} ${dir_mount}
mkdir ${dir_mount}/var
mount /dev/mapper/${vg}-${name}--var ${dir_mount}/var

# Decompression template
info "Decompression template ${template}" 32
tar -C ${dir_mount} -xvf ${template} --strip-components=1 > /dev/null

# Montages
mount -t sysfs /sys ${dir_mount}/sys
mount -t proc /proc ${dir_mount}/proc
mount --bind /dev ${dir_mount}/dev
mount -t devpts /dev/pts ${dir_mount}/dev/pts
mount --bind /tmp ${dir_mount}/tmp
info "Mise à jour" 32
chroot ${dir_mount} /bin/bash -c "apt-get update && apt-get upgrade -y" > /dev/null

On créé les volumes, on colle dedans notre image préparée, le tout avec l’ensemble des montages qui va bien. On termine avec une petite update, ça ne mange pas de pain !

C – Copie Skelette

On va copier des fichiers templates dans nos VMs :

# Copie Squelette
cp skel/etc/* ${dir_mount}/etc/ -r

Et on va les créer de suite !

D’abord l’arborescences :

mkdir skel
mkdir skel/etc

Et ensuite créez chacun des fichiers suivants :

1 – skel/etc/default/grub

Les réglages pour Grub, je passe le nom des interfaces réseau en ethX et j’active la console.

GRUB_DEFAULT=0
GRUB_TIMEOUT=1
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200n8 net.ifnames=0 biosdevname=0"
GRUB_TERMINAL="console serial"
GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"
2 – skel/etc/fstab

La future définition des volumes

UUID=rootuuid / ext4 errors=remount-ro 0 1
UUID=varuuid /var ext4 errors=remount-ro 0 1
UUID=swapuuid none swap sw 0 0
3 – skel/etc/hostname
debian
4 – skel/etc/hosts
127.0.0.1 localhost
127.0.1.1 debian
5 – skel/etc/network/interfaces

Je déclare mes interfaces Vlan 99 et 250. IP sera remplacé lors de l’execution du script.

auto lo
iface lo inet loopback

auto eth0.99
iface eth0.99 inet static
 address 10.99.1.IP
 netmask 255.255.255.0

auto eth0.250
iface eth0.250 inet static
 address 10.250.1.IP
 netmask 255.255.255.0
6 – skel/etc/resolv.conf
nameserver IP.DNS

Pour le moment, le serveur DNS interne n’étant pas en production, on renseignera un DNS temporaire.

 

Voila qui termine les fichiers du template.

 

D – Setup VM

Revenons dans notre script et passons à la configuration de la VM :

# Configuration de GRUB
echo -e "\e[32mConfig GRUB\e[39m"
chroot ${dir_mount} /bin/bash -c "grub-mkconfig -o /boot/grub/grub.cfg" > /dev/null 2>&1
chroot ${dir_mount} /bin/bash -c "grub-install /dev/mapper/${vg}-${name}" > /dev/null 2>&1

# Configuration des fichiers template
echo -e "\e[32mSetup VM\e[39m"
sed -i "s/IP/${id}/g" ${dir_mount}/etc/network/interfaces
sed -i "s/debian/${name}/g" ${dir_mount}/etc/hostname
sed -i "s/debian/${name}/g" ${dir_mount}/etc/hosts
rootuuid=$(blkid -s UUID -o value "/dev/mapper/${part}")
varuuid=$(blkid -s UUID -o value "/dev/mapper/${vg}-${name}--var")
if [ $size_swap -ne 0 ]; then swapuuid=$(blkid -s UUID -o value "/dev/mapper/${vg}-${name}--swap"); fi
sed -i "s/rootuuid/$rootuuid/g" ${dir_mount}/etc/fstab
sed -i "s/varuuid/$varuuid/g" ${dir_mount}/etc/fstab
if [ $size_swap -ne 0 ]; then sed -i "s/swapuuid/$swapuuid/g" ${dir_mount}/etc/fstab
else sed -i '/swapuuid/d' ${dir_mount}/etc/fstab
fi

# Interdiction de connexion en root par ssh
sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin no/g" ${dir_mount}/etc/ssh/sshd_config

# Ajout user et changement pwd user et root
chroot ${dir_mount} sh -c "adduser --disabled-password --gecos \"\" ${user}" > /dev/null
chroot ${dir_mount} sh -c "usermod -aG sudo ${user}"
chroot ${dir_mount} sh -c "echo 'root:${pass}' | chpasswd"
chroot ${dir_mount} sh -c "echo '${user}:${puser}' | chpasswd"

# Clé SSH, config SSH, ntp, ipv6...
mkdir ${dir_mount}/home/${user}/.ssh
rm ${dir_mount}/etc/ssh/ssh_host_*
chroot ${dir_mount} /bin/bash -c "dpkg-reconfigure --frontend=noninteractive openssh-server" > /dev/null 2>&1
echo "net.ipv6.conf.all.disable_ipv6 = 1" >> ${dir_mount}/etc/sysctl.conf
echo "server 10.99.1.250" >> ${dir_mount}/etc/ntp.conf
cat /root/.ssh/id_rsa.pub > ${dir_mount}/home/${user}/.ssh/authorized_keys

# Recup fingerprint
rsa=$(ssh-keygen -l -f "${dir_mount}"/etc/ssh/ssh_host_ecdsa_key | cut -d' ' -f2) 

# Démontage final
umount ${dir_mount}/{sys,proc,dev/pts,dev,tmp};umount ${dir_mount}/var;umount ${dir_mount}

E – Enregistrement VM dans KVM

Ne reste plus qu’à enregistrer la VM dans KVM. Pour se faire, on va générer un fichier xml avec virt-install et le commutateur –print-xml. Ensuite, on le retouche pour ajouter le driver vhost et on utilise virsh define avec le xml pour générer la VM. On termine en ajoutant l’IP dans le VLAN 99 dans le fichier /etc/hosts de l’hyperviseur.

info "Enregistrement VM" 32

# Param Disques
if [ $size_swap -ne 0 ]; then virtswap=" --disk vol=${vol_kvm}/${name}-swap,bus=virtio"; fi
virtdisks="--disk vol=${vol_kvm}/${name},bus=virtio --disk vol=${vol_kvm}/${name}-var,bus=virtio${virtswap}"

# Param Nets
netdef="${kvmnet},portgroup=${vlandef}"
virtnets="--network network:${netdef},model=virtio,target=${name}.0"

# Generation XML
virt-install -n ${name} --vcpus ${cpu} --ram ${ram} --cpu host --import ${virtdisks} ${virtnets} \
--graphics none --console pty,target_type=serial --os-variant ${os} --print-xml > ${name}.xml

# Petite retouche
sed -i -e "/<\/interface>/i \\ \\<driver name=\"vhost\"/>" ${name}.xml

# Enregistrement dans KVM
virsh define ${name}.xml > /dev/null
rm ${name}.xml
info "Terminé !" 31
echo -e "VM prête à demarrer : virsh start ${name} --console \n"
info "Fingerprint: ${rsa}" 32
echo -e "10.99.1.${id}\t${name}" >> /etc/hosts

Et voila !

F – Chasse à la faute de frappe dans le fichier

On va enfin pourvoir admirer le résultat.

Avant tout, et si vous ne l’avez jamais fait sur votre serveur :

# ssh-keygen

et ce afin de générer les clés necessaire à la connexion depuis l’hyperviseur vers les VMs.

Et pour ENFIN créer une VM :

./create_vm.sh -n mavmquirox -i 69 -s 1

Ainsi, j’aurais une VM nommée mavmquirox avec des IPs en 69 (10.x.x.69), sur le VG Master. Bien évidement, on aura adapté CPU, RAM au besoin.

Et vous le voyez, le temps de création est minime ! Bon, en meme temps, vu le temps passé sur le script, faut bien le rentabiliser !

 

III – Suppression de VM

Clairement la, le script n’est pas terminé, n’est pas aussi « poussé » que create.sh. Il faut triturer les options dedans pour tapper le bon VG.

Ca ne checke pas non plus si la machine tourne… Je retravaillerai dessus à l’occasion.

#!/bin/bash

# A remplacer par le bon
pool="lvm0" # Ou "lvm1"
vg="vg0"    # ou "vg1"

while getopts n: flag
do
  case "${flag}" in
    n) name=${OPTARG};;
  esac
done
if [ ! -n "$name" ]; then echo 'Name (-n) manque' >&2;exit 1;fi
echo -e "Pas de regrets ?\nEntrée pour valider, CTRL-C pour quitter"
read

echo "Suppression de $name"
partprobe > /dev/null 2>&1
re='^[0-9]+$'
if [[ "${name: -1}" =~ $re ]]; then p="p";fi
wipefs -a -f /dev/${vg}/${name}
wipefs -a -f /dev/${vg}/${name}-swap
wipefs -a -f /dev/${vg}/${name}-var
dmsetup remove ${vg}-${name}${p}1
dmsetup remove ${vg}-${name}

virsh vol-delete ${name} ${pool} > /dev/null 2>&1
virsh vol-delete ${name}-swap ${pool} > /dev/null 2>&1
virsh vol-delete ${name}-var ${pool} > /dev/null 2>&1

virsh undefine $name > /dev/null 2>&1

sed -i "/${name}/d" /etc/hosts
ssh-keygen -f "/root/.ssh/known_hosts" -R "$name"

echo "VM ${name} supprimée..."

 

IV – Conclustion

Et voila, vous avez des scripts permettant de mettre en place vos VMs de maniere très rapide. Sur mon serveur, create.sh prend moins de 20 secondes pour me créer une VM fonctionnelle.

Pour information, je suis également en train d’en faire une version python que je vous soumettrais quand ce sera opérationnel 😉

 

 

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *