training-labs/labs/stacktrain/virtualbox/vm_create.py

674 lines
21 KiB
Python

#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
from time import sleep
import logging
import os
import re
import subprocess
import sys
import stacktrain.config.general as conf
import stacktrain.core.helpers as hf
import stacktrain.core.cond_sleep as cs
import stacktrain.batch_for_windows as wb
logger = logging.getLogger(__name__)
vm_group = "labs"
conf.vbox_ostype = None
def init():
output = vbm("--version")
# We only get an output if we are actually building the cluster
if conf.do_build:
logger.debug("VBoxManage version: %s", output)
if re.search("kernel module is not load", output, flags=re.MULTILINE):
logger.error("Kernel module for VirtualBox is not loaded."
" Aborting.")
sys.exit(1)
def vbm_log(call_args, err_code=None):
log_file = os.path.join(conf.log_dir, "vboxmanage.log")
msg = ' '.join(call_args)
if err_code:
msg = "FAILURE ({}): ".format(err_code) + msg
with open(log_file, 'a') as logf:
if conf.do_build:
logf.write("%s\n" % msg)
else:
logf.write("(not executed) %s\n" % msg)
def vbm(*args, **kwargs):
# wbatch parameter can override conf.wbatch setting
wbatch = kwargs.pop('wbatch', conf.wbatch)
if wbatch:
wb.wbatch_log_vbm(args)
# FIXME caller expectations: where should stderr go (console, logfile)
show_err = kwargs.pop('show_err', True)
if show_err:
errout = subprocess.STDOUT
else:
errout = open(os.devnull, 'w')
vbm_exe = "VBoxManage"
call_args = [vbm_exe] + list(args)
vbm_log(call_args)
if not conf.do_build:
return
try:
output = subprocess.check_output(call_args, stderr=errout)
except subprocess.CalledProcessError as err:
if show_err:
vbm_log(call_args, err_code=err.returncode)
logger.warn("%s call failed.", vbm_exe)
logger.warn(' '.join(call_args))
logger.warn("call_args: %s", call_args)
logger.warn("rc: %s", err.returncode)
logger.warn("output:\n%s", err.output)
logger.exception("Exception")
logger.warn("--------------------------------------------------")
import traceback
traceback.print_exc(file=sys.stdout)
sys.exit(45)
else:
logger.debug("%s call failed.", vbm_exe)
logger.debug(' '.join(call_args))
logger.debug("call_args: %s", call_args)
logger.debug("rc: %s", err.returncode)
logger.debug("output:\n%s", err.output)
raise EnvironmentError
return output
# -----------------------------------------------------------------------------
# VM status
# -----------------------------------------------------------------------------
def vm_exists(vm_name):
output = vbm("list", "vms", wbatch=False)
return True if re.search('"' + vm_name + '"', output) else False
def get_vm_state(vm_name):
state = None
try:
output = vbm("showvminfo", "--machinereadable", vm_name, wbatch=False,
show_err=False)
except EnvironmentError:
# VBoxManage returns error status while the machine is changing
# state (e.g., shutting down)
logger.debug("Ignoring exceptions when checking for VM state.")
else:
ma = re.search(r'VMState="(.*)"', output)
if ma:
state = ma.group(1)
logger.debug("get_vm_vmstate: %s", state)
return state
def vm_is_running(vm_name):
vm_state = get_vm_state(vm_name)
if vm_state in ("running", "stopping"):
logger.debug("vm_is_running: ;%s;", vm_state)
return True
else:
return False
def vm_is_shut_down(vm_name):
vm_state = get_vm_state(vm_name)
if vm_state == "poweroff":
logger.debug("vm_is_shut_down: ;%s;", vm_state)
return True
else:
return False
# TODO move vm_wait_for_shutdown to functions_host
def vm_wait_for_shutdown(vm_name, timeout=None):
if conf.wbatch:
wb.wbatch_wait_poweroff(vm_name)
cs.conditional_sleep(1)
if not conf.do_build:
return
logger.info("Waiting for shutdown of VM %s.", vm_name)
sec = 0
while True:
if vm_is_shut_down(vm_name):
logger.info("Machine powered off.")
break
if timeout and sec > timeout:
logger.info("Timeout reached, giving up.")
break
print('.', end='')
sys.stdout.flush()
delay = 1
sleep(delay)
sec += delay
def vm_power_off(vm_name):
if vm_is_running(vm_name):
logger.info("Powering off VM %s", vm_name)
try:
vbm("controlvm", vm_name, "poweroff")
except EnvironmentError:
logger.debug("vm_power_off got an error, hoping for the best.")
# Give VirtualBox time to sort out whatever happened
sleep(5)
vm_wait_for_shutdown(vm_name, timeout=10)
if vm_is_running(vm_name):
logger.error("VM %s does not power off. Aborting.", vm_name)
sys.exit(1)
# VirtualBox VM needs a break before taking new commands
cs.conditional_sleep(1)
def vm_acpi_shutdown(vm_name):
logger.info("Shutting down VM %s.", vm_name)
vbm("controlvm", vm_name, "acpipowerbutton")
# VirtualBox VM needs a break before taking new commands
cs.conditional_sleep(1)
# Shut down all VMs in group VM_GROUP
# Note: This function must be called when no Windows batch file is open for
# writing (wbatch_write will ignore all these calls).
def stop_running_cluster_vms():
# Get VM ID from a line looking like this:
# "My VM" {0a13e26d-9543-460d-82d6-625fa657b7c4}
output = vbm("list", "runningvms")
if not output:
return
for runvm in output.splitlines():
mat = re.match(r'".*" {(\S+)}', runvm)
if mat:
vm_id = mat.group(1)
output = vbm("showvminfo", "--machinereadable", vm_id)
for line in output.splitlines():
if re.match('groups="/{}'.format(vm_group), line):
# We may have waited quite some time for other VMs
# to shut down
if vm_is_running(vm_id):
logger.info("Shutting down VM %s.", vm_id)
vm_acpi_shutdown(vm_id)
vm_wait_for_shutdown(vm_id, timeout=5)
if vm_is_running(vm_id):
logger.info("VM will not shut down, powering it"
" off.")
vm_power_off(vm_id)
# -----------------------------------------------------------------------------
# Host-only network functions
# -----------------------------------------------------------------------------
def hostonlyif_in_use(if_name):
output = vbm("list", "-l", "runningvms", wbatch=False)
return re.search("NIC.*Host-only Interface '{}'".format(if_name),
output, flags=re.MULTILINE)
def ip_to_hostonlyif(ip):
ip_net_address = hf.ip_to_net_address(ip)
if not conf.do_build:
# Add placeholders for wbatch code
for index, (net_name, net_address) in enumerate(
conf.networks.iteritems()):
if net_address == ip_net_address:
if_name = "vboxnet{}".format(index)
logger.debug("%s %s %s", net_address, net_name, if_name)
return if_name
output = vbm("list", "hostonlyifs", wbatch=False)
host_net_address = None
for line in output.splitlines():
ma = re.match(r"Name:\s+(\S+)", line)
if ma:
if_name = ma.group(1)
continue
ma = re.match(r"IPAddress:\s+(\S+)", line)
if ma:
host_ip = ma.group(1)
host_net_address = hf.ip_to_net_address(host_ip)
if host_net_address == ip_net_address:
return if_name
def create_hostonlyif():
output = vbm("hostonlyif", "create", wbatch=False)
# output is something like "Interface 'vboxnet3' was successfully created"
ma = re.search(r"^Interface '(\S+)' was successfully created",
output, flags=re.MULTILINE)
if ma:
if_name = ma.group(1)
else:
logger.error("Host-only interface creation failed.")
raise EnvironmentError
return if_name
def create_network(net_name, ip_address):
# The host-side interface is the default gateway of the network
if_name = ip_to_hostonlyif(ip_address)
if if_name:
if hostonlyif_in_use(if_name):
logger.info("Host-only interface %s (%s) in use. Using it, too.",
if_name, ip_address)
# else: TODO destroy network if not in use?
else:
logger.info("Creating host-only interface.")
if_name = create_hostonlyif()
logger.info("Configuring host-only network %s with gw address %s (%s).",
net_name, ip_address, if_name)
vbm("hostonlyif", "ipconfig", if_name,
"--ip", ip_address,
"--netmask", "255.255.255.0",
wbatch=False)
return if_name
# -----------------------------------------------------------------------------
# VM create and configure
# -----------------------------------------------------------------------------
def vm_mem(vm_config):
# Default RAM allocation is 512 MB per VM
mem = vm_config.vm_mem or 512
vbm("modifyvm", vm_config.vm_name, "--memory", str(mem))
def vm_cpus(vm_config):
# Default RAM allocation is 512 MB per VM
cpus = vm_config.vm_cpus or 1
vbm("modifyvm", vm_config.vm_name, "--cpus", str(cpus))
def vm_port(vm_name, desc, hostport, guestport):
natpf1_arg = "{},tcp,127.0.0.1,{},,{}".format(desc, hostport, guestport)
vbm("modifyvm", vm_name, "--natpf1", natpf1_arg)
def vm_nic_base(vm_name, index):
# We start counting interfaces at 0, but VirtualBox starts NICs at 1
nic = index + 1
vbm("modifyvm", vm_name,
"--nictype{}".format(nic), "virtio",
"--nic{}".format(nic), "nat")
def vm_nic_std(vm_name, iface, index):
# We start counting interfaces at 0, but VirtualBox starts NICs at 1
nic = index + 1
hostif = ip_to_hostonlyif(iface["ip"])
vbm("modifyvm", vm_name,
"--nictype{}".format(nic), "virtio",
"--nic{}".format(nic), "hostonly",
"--hostonlyadapter{}".format(nic), hostif,
"--nicpromisc{}".format(nic), "allow-all")
def vm_nic_set_boot_prio(vm_name, iface, index):
# We start counting interfaces at 0, but VirtualBox starts NICs at 1
nic = index + 1
vbm("modifyvm", vm_name,
"--nicbootprio{}".format(nic), str(iface["prio"]))
def vm_create(vm_config):
vm_name = vm_config.vm_name
if conf.wbatch:
wb.wbatch_abort_if_vm_exists(vm_name)
if conf.do_build:
wbatch_tmp = conf.wbatch
conf.wbatch = False
vm_delete(vm_name)
conf.wbatch = wbatch_tmp
vbm("createvm", "--name", vm_name, "--register",
"--ostype", conf.vbox_ostype, "--groups", "/" + vm_group)
if conf.do_build:
output = vbm("showvminfo", "--machinereadable", vm_name, wbatch=False)
if re.search(r'longmode="off"', output):
logger.info("Nodes run 32-bit OS, enabling PAE.")
vbm("modifyvm", vm_name, "--pae", "on")
vbm("modifyvm", vm_name, "--rtcuseutc", "on")
vbm("modifyvm", vm_name, "--biosbootmenu", "disabled")
vbm("modifyvm", vm_name, "--largepages", "on")
vbm("modifyvm", vm_name, "--boot1", "disk")
vbm("modifyvm", vm_name, "--boot3", "net")
# Enough ports for three disks
vbm("storagectl", vm_name, "--name", "SATA", "--add", "sata",
"--portcount", str(3))
vbm("storagectl", vm_name, "--name", "SATA", "--hostiocache", "on")
vbm("storagectl", vm_name, "--name", "IDE", "--add", "ide")
logger.info("Created VM %s.", vm_name)
# -----------------------------------------------------------------------------
# VM unregister, remove, delete
# -----------------------------------------------------------------------------
def vm_unregister_del(vm_name):
logger.info("Unregistering and deleting VM: %s", vm_name)
vbm("unregistervm", vm_name, "--delete")
def vm_delete(vm_name):
logger.info("Asked to delete VM %s ", vm_name)
if vm_exists(vm_name):
logger.info("\tfound")
vm_power_off(vm_name)
hd_path = vm_get_disk_path(vm_name)
if hd_path:
logger.info("\tDisk attached: %s", hd_path)
vm_detach_disk(vm_name)
disk_unregister(hd_path)
try:
os.remove(hd_path)
except OSError:
# File is probably gone already
pass
vm_unregister_del(vm_name)
else:
logger.info("\tnot found")
# -----------------------------------------------------------------------------
# VM shared folders
# -----------------------------------------------------------------------------
def vm_add_share_automount(vm_name, share_dir, share_name):
vbm("sharedfolder", "add", vm_name,
"--name", share_name,
"--hostpath", share_dir,
"--automount")
def vm_add_share(vm_name, share_dir, share_name):
vbm("sharedfolder", "add", vm_name,
"--name", share_name,
"--hostpath", share_dir)
# -----------------------------------------------------------------------------
# Disk functions
# -----------------------------------------------------------------------------
def get_next_child_disk_uuid(disk):
if not disk_registered(disk):
return
output = vbm("showhdinfo", disk, wbatch=False)
child_uuid = None
line = re.search(r'^Child UUIDs:\s+(\S+)$', output, flags=re.MULTILINE)
try:
child_uuid = line.group(1)
except AttributeError:
# No more child UUIDs
pass
return child_uuid
def disk_to_vm(disk):
output = vbm("showhdinfo", disk, wbatch=False)
line = re.search(r'^In use by VMs:\s+(\S+)', output, flags=re.MULTILINE)
try:
vm_name = line.group(1)
except AttributeError:
# No VM attached to disk
return None
return vm_name
def disk_to_path(disk):
output = vbm("showhdinfo", disk, wbatch=False)
# Note: path may contain whitespace
line = re.search(r'^Location:\s+(\S.*)$', output, flags=re.MULTILINE)
try:
disk_path = line.group(1)
except AttributeError:
logger.error("No disk path found for disk %s.", disk)
raise
return disk_path
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Creating, registering and unregistering disk images with VirtualBox
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def disk_registered(disk):
"""disk can be either a path or a disk UUID"""
output = vbm("list", "hdds", wbatch=False)
return re.search(disk, output)
def disk_unregister(disk):
logger.info("Unregistering disk\n\t%s", disk)
vbm("closemedium", "disk", disk)
def create_vdi(path, size):
# Make sure target directory exists
hf.create_dir(os.path.dirname(path))
logger.info("Creating disk (size: %s MB):\n\t%s", size, path)
vbm("createhd",
"--format", "VDI",
"--filename", path,
"--size", str(size))
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Attaching and detaching disks from VMs
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def vm_get_disk_path(vm_name):
output = vbm("showvminfo", "--machinereadable", vm_name, wbatch=False)
line = re.search(r'^"SATA-0-0"="(.*vdi)"$', output, flags=re.MULTILINE)
try:
path = line.group(1)
except AttributeError:
logger.info("No disk path found for VM %s.", vm_name)
path = None
return path
def vm_detach_disk(vm_name, port=0):
logger.info("Detaching disk from VM %s.", vm_name)
vbm("storageattach", vm_name,
"--storagectl", "SATA",
"--port", str(port),
"--device", "0",
"--type", "hdd",
"--medium", "none")
# VirtualBox VM needs a break before taking new commands
cs.conditional_sleep(1)
def vm_attach_dvd(vm_name, iso, port=0):
logger.info("Attaching to VM %s:\n\t%s", vm_name, iso)
vbm("storageattach", vm_name,
"--storagectl", "IDE",
"--port", str(port),
"--device", "0",
"--type", "dvddrive",
"--medium", iso)
def vm_attach_disk(vm_name, disk, port=0):
"""disk can be either a path or a disk UUID"""
logger.info("Attaching to VM %s:\n\t%s", vm_name, disk)
vbm("storageattach", vm_name,
"--storagectl", "SATA",
"--port", str(port),
"--device", "0",
"--type", "hdd",
"--medium", disk)
# disk can be either a path or a disk UUID
def vm_disk_is_multiattach(disk):
output = vbm("showmediuminfo", disk, wbatch=False)
regex = re.compile(r"^Type:.*multiattach", re.MULTILINE)
return True if re.search(regex, output) else False
# disk can be either a path or a disk UUID
def vm_attach_disk_multi(vm_name, disk, port=0):
vbm("modifyhd", "--type", "multiattach", disk)
logger.info("Attaching to VM %s (multi):\n\t%s", vm_name, disk)
vbm("storageattach", vm_name,
"--storagectl", "SATA",
"--port", str(port),
"--device", "0",
"--type", "hdd",
"--medium", disk)
# -----------------------------------------------------------------------------
# VirtualBox guest add-ons
# -----------------------------------------------------------------------------
def vm_attach_guestadd_iso(vm_name):
if conf.wbatch:
# Record the calls for wbatch (this should always work because the
# Windows VirtualBox always comes with the guest additions)
# TODO better way of disabling do_build temporarily
tmp_do_build = conf.do_build
conf.do_build = False
# An existing drive is needed to make additions shortcut work
# (at least VirtualBox 4.3.12 and below)
vm_attach_dvd(vm_name, "emptydrive", port=1)
vm_attach_dvd(vm_name, "additions", port=1)
conf.do_build = tmp_do_build
# If we are just faking it for wbatch, we are already done here
if not conf.do_build:
return
if not hasattr(conf, "guestadd_iso") or not conf.guestadd_iso:
# No location configured, asking VirtualBox for one
tmp_wbatch = conf.wbatch
conf.wbatch = False
# An existing drive is needed to make additions shortcut work
# (at least VirtualBox 4.3.12 and below)
vm_attach_dvd(vm_name, "emptydrive", port=1)
try:
vm_attach_dvd(vm_name, "additions", port=1)
except Exception:
# TODO Implement search and guessing if still needed.
# We only need it on Linux if the VirtualBox package does not
# include the guest additions, the user has not provided an ISO,
# and the cluster must be built using shared folders (i.e. only
# for wbatch testing on Linux)
logger.error("VirtualBox guest additions not found.")
sys.exit(1)
conf.wbatch = tmp_wbatch
# -----------------------------------------------------------------------------
# Snapshots
# -----------------------------------------------------------------------------
def vm_snapshot_list(vm_name):
output = None
if vm_exists(vm_name):
try:
output = vbm("snapshot", vm_name, "list", "--machinereadable",
show_err=False)
except EnvironmentError:
# No snapshots
pass
return output
def vm_snapshot_exists(vm_name, shot_name):
snap_list = vm_snapshot_list(vm_name)
if snap_list:
return re.search('SnapshotName.*="{}"'.format(shot_name), snap_list)
else:
return False
def vm_snapshot(vm_name, shot_name):
vbm("snapshot", vm_name, "take", shot_name)
# VirtualBox VM needs a break before taking new commands
cs.conditional_sleep(1)
# -----------------------------------------------------------------------------
# Booting a VM
# -----------------------------------------------------------------------------
def vm_boot(vm_name):
log_str = "Starting VM {}".format(vm_name)
if conf.do_build:
# Save latest VM config before booting
output = vbm("showvminfo", "--machinereadable", vm_name, wbatch=False)
log_file = os.path.join(conf.log_dir, "vm_{}.cfg".format(vm_name))
with open(log_file, 'w') as logf:
logf.write(output)
if conf.vm_ui:
if conf.wbatch and conf.vm_ui == "headless":
# With VirtualBox 5.1.6, console type "headless" often gives no
# access to the VM console which on Windows is the main method for
# interacting with the cluster. Use "separate" which works at least
# on 5.0.26 and 5.1.6.
logger.warning('Overriding UI type "headless" with "separate" for '
'Windows batch files.')
conf.vm_ui = "separate"
log_str += " with {} GUI".format(conf.vm_ui)
logger.info(log_str)
vbm("startvm", vm_name, "--type", conf.vm_ui)
else:
logger.info(log_str)
vbm("startvm", vm_name)