Premier Training & Business Partner Red Hat

RHEV: deploy automatico tramite API

Daniele Mazzochio
Ti piacerebbe diventare anche tu uno di noi e
pubblicare i tuoi articoli nel blog degli RHCE italiani?

L’obiettivo dell’articolo è fare una rapida panoramica sull’API Python di RHEV, creando un semplice script per la creazione di nuove macchine virtuali; un simile script può essere usato per creare più rapidamente le macchine virtuali (bypassando completamente l’interfaccia web di RHEV-M) o per automatizzare il processo di deploy.

La prima scelta che si presenta è fra l’installazione da template oppure via rete (tramite kickstart); sebbene la scelta più ovvia, per le macchine virtuali, possa sembrare il template, io preferisco la seconda: i sistemi installati da kickstart sono già aggiornati, registrati su RHN e si possono utilizzare gli stessi script di installazione per tutte le macchine, fisiche o virtuali, semplificando la manutenzione (è più agevole aggiornare uno script piuttosto che uno o più template).

Una volta deciso di installare via rete, ho preferito però evitare di usare PXE/DHCP per avviare il boot: al di là dei potenziali problemi di sicurezza e affidabilità di PXE, il DHCP richiede un certo overhead di configurazione in caso di reti segmentate (DHCP relay agent o più server DHCP) o con diversi sistemi operativi, per non parlare della manutenzione della configurazione della reservation!

RHEV permette di mantenere tutti i pregi del boot da PXE/DHCP (ma non i difetti) passando direttamente il kernel (vmlinuz), l’immagine initrd (initrd.img) e i parametri di boot alla macchina virtuale. I file da passare si trovano nella directory “/images/pxeboot” dell’immagine “boot.iso”, scaricabile dal sito di RedHat, e possono essere uploadati sull’ISO domain con il comando rhevm-iso-uploader:

# rhevm-iso-uploader --iso-domain=<ISODomain> upload initrd.img vmlinuz
Please provide the REST API password for the admin@internal oVirt Engine user
(CTRL+D to abort): <password>
Uploading, please wait...
INFO: Start uploading initrd.img
INFO: initrd.img uploaded successfully
INFO: Start uploading vmlinuz
INFO: vmlinuz uploaded successfully

Questi file non saranno visibili listando il contenuto dell’ISO domain (a meno di aggiungere un’estensione “.iso”), ma saranno ugualmente utilizzabili (vedremo fra poco come).

Ma veniamo all’API RHEV; la prima cosa da fare, in uno script, è importare i moduli necessari e creare un’istanza della classe API, che rappresenta l’entry point per accedere a qualunque aspetto della configurazione di RHEV:

#!/usr/bin/env python

from ovirtsdk.api import API
from ovirtsdk.xml import params

URL      = "https://rhevm.my.domain/api"
USERNAME = "admin@internal"
PASSWORD = "password"
CA_FILE  = "/etc/pki/ovirt-engine/ca.pem"

api = API(url=URL, username=USERNAME, password=PASSWORD, ca_file=CA_FILE)

Il certificato (CA_FILE) può essere recuperato dalla macchina RHEV-M (https://<rhevm.my.domain>/ca.crt).
E’ quindi giunto il momento di creare la prima macchina virtuale:

VM_NAME = "my_vm"
CLUSTER_NAME = "my_cluster"
SOCKETS = 2
CORES = 2
GB = 1024**3

cpu_params = params.CPU(topology=params.CpuTopology(sockets=SOCKETS,
                                                    cores=CORES))
api.vms.add(params.VM(name=VM_NAME,
                      cluster=api.clusters.get(CLUSTER_NAME),
                      template=api.templates.get("Blank"),
                      cpu=cpu_params, memory=2*GB,
                      display=params.Display(type_="SPICE")))

Prima di proseguire con le operazioni successive, dobbiamo aspettare che la macchina raggiunga lo stato “down”; si può usare una funzione del tipo:

import time

def wait_vm_state(vm_name, state):
    while api.vms.get(vm_name).status.state != state:
        time.sleep(1)

wait_vm_state(VM_NAME, "down")

Una funzione simile può essere usata per monitorare lo stato dei dischi virtuali:

def wait_disk_state(disk_name, state):
    while api.disks.get(disk_name).status.state != state:
        time.sleep(1)

Una volta che la macchina è creata e in stato “down”, possiamo aggiungere uno o più dischi e schede di rete; in questo esempio, aggiungeremo un disco da 20GB thin-provisioned (Copy-On-Write) e una NIC:

STG_DOMAIN = "my_data_domain"
DSK_NAME = "disk1"
NIC_NAME = "nic1"
NET_NAME = "my_network"

vm = api.vms.get(VM_NAME)
stg_domain = api.storagedomains.get(STG_DOMAIN)
stg_parms = params.StorageDomains(storage_domain=[stg_domain])
# Boot disk
vm.disks.add(params.Disk(name=DSK_NAME,
                         storage_domains=stg_parms,
                         size=20*GB,
                         status=None,
                         interface='virtio',
                         format='cow',
                         sparse=False,
                         bootable=True))
wait_disk_state(DSK_NAME, "ok")

# Boot NIC
vm.nics.add(params.NIC(name=NIC_NAME,
                       network=params.Network(name=NET_NAME),
                       interface='virtio'))
boot_if = vm.nics.get(NIC_NAME).mac.address

# Aggiungere altri dischi e NIC a piacere...

Come si vede ho conservato, nella variabile ‘boot_if’, il MAC address dell’interfaccia di boot, in modo da passarlo al kernel per identificare la scheda di boot (parametro ksdevice); ovviamente questo ha senso solo se la macchina ha più interfacce di rete.
A questo punto possiamo settare i parametri di boot (kernel, init ramdisk e riga di comando); quest’operazione può essere già fatta in fase di creazione della macchina se non si deve specificare il MAC address come ksdevice.

boot_params = {"ks": "http://satellite/ks",
               "ksdevice": boot_if,
               "dns": "1.2.3.4",
               "ip": "10.9.8.7",
               "netmask": "255.255.255.0",
               "gateway": "10.9.8.1",
               "hostname": "{0}.my.domain".format(VM_NAME)}
cmdline = " ".join(map("{0[0]}={0[1]}".format, d.iteritems()))
vm.set_os(params.OperatingSystem(kernel="iso://vmlinuz",
                                 initrd="iso://initrd.img",
                                 cmdline=cmdline))
vm.update()

Non resta che avviare la macchina per far partire l’installazione:

vm.start()

Possiamo quindi togliere i parametri di boot per avviare la macchina da hard disk al prossimo reboot.

L’unico problema è che, se il kickstart contiene il parametro “reboot”, la macchina si riavvierà dopo l’installazione senza rileggere la configurazione, ricominciando da capo l’installazione; io preferisco quindi usare il parametro “poweroff” nel kickstart e riaccendere la macchina tramite l’API, quando avrà raggiunto lo stato “down”.

# Aspetta il poweroff a fine installazione
wait_vm_state(VM_NAME, "down")
# Togli i parametri di boot specifici per l'installazione
vm.set_os(params.OperatingSystem(kernel="", initrd="", cmdline=""))
vm.update()
# Avvia la macchina dopo l'installazione
vm.start()
api.disconnect()

Un’ultima osservazione sui parametri di boot; un modo per personalizzare la parte di post-installazione è passare parametri arbitrari nella riga di boot, in modo che gli script di post-install compiano o meno determinate azioni in base a questi parametri.

Ad esempio, se aggiungiamo il parametro “do_stuff=y” alla riga di boot, il kernel si limiterà ad ignorarlo perché non lo conosce; in compenso, nella sezione di post-install del file di kickstart, è possibile leggere la riga di boot ed effettuare determinate azioni in base a questo parametro; ad esempio:

%post --log /root/post-ks.log --interpreter /bin/bash
    eval $(cat /proc/cmdline)
    if [[ "$do_stuff" = "y" ]]; then
        # ... do stuff here ...
    fi
%end

[:en][ … ]
La prima scelta che si presenta è fra l’installazione da template oppure via rete (tramite kickstart); sebbene la scelta più ovvia, per le macchine virtuali, possa sembrare il template, io preferisco la seconda: i sistemi installati da kickstart sono già aggiornati, registrati su RHN e si possono utilizzare gli stessi script di installazione per tutte le macchine, fisiche o virtuali, semplificando la manutenzione (è più agevole aggiornare uno script piuttosto che uno o più template).
Una volta deciso di installare via rete, ho preferito però evitare di usare PXE/DHCP per avviare il boot: al di là dei potenziali problemi di sicurezza e affidabilità di PXE, il DHCP richiede un certo overhead di configurazione in caso di reti segmentate (DHCP relay agent o più server DHCP) o con diversi sistemi operativi, per non parlare della manutenzione della configurazione della reservation!
RHEV permette di mantenere tutti i pregi del boot da PXE/DHCP (ma non i difetti) passando direttamente il kernel (vmlinuz), l’immagine initrd (initrd.img) e i parametri di boot alla macchina virtuale. I file da passare si trovano nella directory “/images/pxeboot” dell’immagine “boot.iso”, scaricabile dal sito di RedHat, e possono essere uploadati sull’ISO domain con il comando rhevm-iso-uploader:

# rhevm-iso-uploader --iso-domain=<ISODomain> upload initrd.img vmlinuz
Please provide the REST API password for the admin@internal oVirt Engine user
(CTRL+D to abort): <password>
Uploading, please wait...
INFO: Start uploading initrd.img
INFO: initrd.img uploaded successfully
INFO: Start uploading vmlinuz
INFO: vmlinuz uploaded successfully

Questi file non saranno visibili listando il contenuto dell’ISO domain (a meno di aggiungere un’estensione “.iso”), ma saranno ugualmente utilizzabili (vedremo fra poco come).
Ma veniamo all’API RHEV; la prima cosa da fare, in uno script, è importare i moduli necessari e creare un’istanza della classe API, che rappresenta l’entry point per accedere a qualunque aspetto della configurazione di RHEV:

#!/usr/bin/env python

from ovirtsdk.api import API
from ovirtsdk.xml import params

URL      = "https://rhevm.my.domain/api"
USERNAME = "admin@internal"
PASSWORD = "password"
CA_FILE  = "/etc/pki/ovirt-engine/ca.pem"

api = API(url=URL, username=USERNAME, password=PASSWORD, ca_file=CA_FILE)

Il certificato (CA_FILE) può essere recuperato da https://rhevm.my.domain/ca.crt.
E’ quindi giunto il momento di creare la prima macchina virtuale:

VM_NAME = "my_vm"
CLUSTER_NAME = "my_cluster"
SOCKETS = 2
CORES = 2
GB = 1024**3

cpu_params = params.CPU(topology=params.CpuTopology(sockets=SOCKETS,
                                                    cores=CORES))
api.vms.add(params.VM(name=VM_NAME,
                      cluster=api.clusters.get(CLUSTER_NAME),
                      template=api.templates.get("Blank"),
                      cpu=cpu_params, memory=2*GB,
                      display=params.Display(type_="SPICE")))

Prima di proseguire con le operazioni successive, dobbiamo aspettare che la macchina raggiunga lo stato “down”; si può usare una funzione semplice del tipo:

def wait_state(vm_name, state):
    while api.vms.get(vm_name).status.state != state:
        time.sleep(1)

wait_state(VM_NAME, "down")

Possiamo quindi aggiungere alla macchina uno o più dischi e schede di rete:

STG_DOMAIN = "my_data_domain"
NIC_NAME = "nic1"
NET_NAME = "my_network"

vm = api.vms.get(VM_NAME)
stg_domain = api.storagedomains.get(STG_DOMAIN)
stg_parms = params.StorageDomains(storage_domain=[stg_domain])
# Boot disk
vm.disks.add(params.Disk(storage_domains=stg_parms,
                         size=20*GB,
                         status=None,
                         interface='virtio',
                         format='cow',
                         sparse=False,
                         bootable=True))
# Boot NIC
vm.nics.add(params.NIC(name=NIC_NAME,
                       network=params.Network(name=NET_NAME),
                       interface='virtio'))
boot_if = vm.nics.get(NIC_NAME).mac.address

Come si vede ho conservato, nella variabile ‘boot_if’, il MAC address dell’interfaccia di boot, in modo da passarlo al kernel per identificare la scheda di boot (parametro ksdevice); ovviamente questo ha senso solo se la macchina ha più interfacce di rete.
A questo punto possiamo settare i parametri di boot (kernel, init ramdisk e riga di comando); quest’operazione può essere già fatta in fase di creazione della macchina se non si deve specificare il MAC address come ksdevice.

boot_params = {"ks": "http://satellite/ks",
               "ksdevice": boot_if,
               "dns": "1.2.3.4",
               "ip": "10.9.8.7",
               "netmask": "255.255.255.0",
               "gateway": "10.9.8.1",
               "hostname": "{0}.my.domain".format(VM_NAME)}
cmdline = " ".join(map("{0[0]}={0[1]}".format, d.iteritems()))
vm.set_os(params.OperatingSystem(kernel="iso://vmlinuz",
                                 initrd="iso://initrd.img",
                                 cmdline=cmdline))
vm.update()

Non resta che avviare la macchina per far partire l’installazione:

vm.start()

Possiamo quindi togliere i parametri di boot per avviare la macchina da hard disk al prossimo reboot. L’unico problema è che, se il kickstart contiene il parametro “reboot”, la macchina si riavvierà dopo l’installazione senza rileggere la configurazione, ricominciando da capo l’installazione; io preferisco quindi usare il parametro “poweroff” nel kickstart e riaccendere la macchina tramite l’API, quando avrà raggiunto lo stato “down”.

wait_state(VM_NAME, "up")
# Togli i parametri di boot specifici per l'installazione
vm.set_os(params.OperatingSystem(kernel="", initrd="", cmdline=""))
vm.update()
# Aspetta il poweroff a fine installazione
wait_state(VM_NAME, "down")
# Avvia la macchina dopo l'installazione
vm.start()
api.disconnect()

[ … ]

Info about author

Daniele Mazzochio