Python port of osbash

This is a pretty direct port of osbash to Python.

The entry point is st.py; use ./st.py --help for help.

osbash.sh should work as before.

Implements: blueprint labs-python-port
Change-Id: Ifcccc420d58cbe907ce29542e4200803fa39e134
This commit is contained in:
Roger Luethi 2016-08-07 20:36:34 +02:00
parent cb35bb360a
commit 075bee7f11
55 changed files with 4633 additions and 58 deletions

5
.gitignore vendored
View File

@ -64,3 +64,8 @@ labs/osbash/log/
labs/osbash/wbatch/
labs/osbash/lib/vagrant-ssh-keys/
labs/osbash/test_tmp/
labs/autostart/
labs/img/
labs/log/
labs/wbatch/

55
labs/.pylintrc Normal file
View File

@ -0,0 +1,55 @@
# The format of this file isn't really documented; just use --generate-rcfile
[MASTER]
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=.git,tests
[Messages Control]
# NOTE(justinsb): We might want to have a 2nd strict pylintrc in future
# C0111: Don't require docstrings on every method
# W0511: TODOs in code comments are fine.
# W0142: *args and **kwargs are fine.
# W0622: Redefining id is fine.
disable=C0111,W0511,W0142,W0622
[Basic]
# Variable names can be 1 to 31 characters long, with lowercase and underscores
variable-rgx=[a-z_][a-z0-9_]{0,30}$
# Argument names can be 2 to 31 characters long, with lowercase and underscores
argument-rgx=[a-z_][a-z0-9_]{1,30}$
# Method names should be at least 3 characters long
# and be lowercased with underscores
method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$
# Module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Don't require docstrings on tests.
no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$
[Miscellaneous]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME
[Format]
# Maximum number of characters on a single line.
max-line-length=79
[Design]
max-public-methods=100
min-public-methods=0
max-args=6
[Variables]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
# _ is used by our localization
additional-builtins=_
[REPORTS]
# Tells whether to display a full report or only the messages
reports=no

View File

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pbr.version
__version__ = pbr.version.VersionInfo(
'openstack-labs').version_string()

1
labs/autostart Symbolic link
View File

@ -0,0 +1 @@
osbash/autostart

1
labs/config Symbolic link
View File

@ -0,0 +1 @@
osbash/config

1
labs/lib Symbolic link
View File

@ -0,0 +1 @@
osbash/lib

View File

@ -2,7 +2,7 @@ cmd boot -n pxeserver
#==============================================================================
# Scripts for controller node
cmd create_pxe_node -n controller
cmd boot_set_tmp_node_ip
cmd boot_set_tmp_node_ip -n controller
cmd queue_renamed -n controller osbash/init_xxx_node.sh
cmd queue etc_hosts.sh
@ -72,7 +72,7 @@ cmd boot -n controller
#==============================================================================
# Scripts for compute1 node
cmd create_pxe_node -n compute1
cmd boot_set_tmp_node_ip
cmd boot_set_tmp_node_ip -n compute1
cmd queue_renamed -n compute1 osbash/init_xxx_node.sh
cmd queue etc_hosts.sh

View File

@ -7,7 +7,19 @@ source "$CONFIG_DIR/deploy.osbash"
source "$OSBASH_LIB_DIR/functions-host.sh"
source "$OSBASH_LIB_DIR/$PROVIDER-functions.sh"
OSBASH=exec_cmd
if [ -f "$TOP_DIR/osbash.sh" ]; then
BUILD_EXE=$TOP_DIR/osbash.sh
OSBASH=exec_cmd
elif [ -f "$TOP_DIR/st.py" ]; then
BUILD_EXE=$TOP_DIR/st.py
# Stacktrain options
ST_OPT=""
else
echo "No build exe found."
exit 1
fi
echo "Using $BUILD_EXE"
RESULTS_ROOT=$LOG_DIR/test-results
@ -20,14 +32,15 @@ function usage {
echo ""
echo "-h Help"
echo "-c Restore node VMs to current snapshot for each test"
echo "-q Disable snapshot cycles during build"
echo "-t SNAP Restore cluster to target snapshot for each test"
echo "-r REP Number of repetitions (default: endless loop)"
echo "-s NODES Start each named node VM after restoring the cluster"
echo "-b Rebuild cluster for each test, from scratch or snapshot"
echo " (osbash.sh -b cluster [...])"
echo " ($(basename $BUILD_EXE) -b cluster [...])"
}
while getopts :bchr:s:t: opt; do
while getopts :bchqr:s:t: opt; do
case $opt in
b)
REBUILD=yes
@ -39,6 +52,13 @@ while getopts :bchr:s:t: opt; do
usage
exit 0
;;
q)
if [ -f "$TOP_DIR/osbash.sh" ]; then
export SNAP_CYCLE=no
else
ST_OPT="$ST_OPT -q"
fi
;;
r)
REP=$OPTARG
;;
@ -136,9 +156,9 @@ until [ $cnt -eq $REP ]; do
rc=0
if [ -n "${REBUILD:-}" ]; then
if [ -n "${TARGET_SNAPSHOT:-}" ]; then
LEAVE_VMS_RUNNING=yes "$TOP_DIR/osbash.sh" -t "$TARGET_SNAPSHOT" -b cluster || rc=$?
LEAVE_VMS_RUNNING=yes "$BUILD_EXE" ${ST_OPT:-} -t "$TARGET_SNAPSHOT" -b cluster || rc=$?
else
"$TOP_DIR/osbash.sh" -b cluster || rc=$?
"$BUILD_EXE" ${ST_OPT:-} -b cluster || rc=$?
fi
fi
echo "####################################################################"
@ -156,7 +176,7 @@ until [ $cnt -eq $REP ]; do
echo "Copying osbash and test log files into $dir."
(
cd "$LOG_DIR"
cp -a *.auto *.log *.xml *.db "$dir" || rc=$?
cp -a *.auto *.log *.xml *.db *.cfg "$dir" || rc=$?
)
echo "Copying upstart log files into $dir."

1
labs/scripts Symbolic link
View File

@ -0,0 +1 @@
osbash/scripts

270
labs/st.py Executable file
View File

@ -0,0 +1,270 @@
#!/usr/bin/env python
"""
Main program for stacktrain.
"""
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import argparse
import importlib
import logging
import os
import sys
import time
import stacktrain.core.helpers as hf
import stacktrain.config.general as conf
import stacktrain.core.report as report
import stacktrain.batch_for_windows as wbatch
# -----------------------------------------------------------------------------
def enable_verbose_console():
'''Replace our default console log handler with a more verbose version'''
logger = logging.getLogger()
for x in logger.handlers:
if type(x) is logging.StreamHandler:
logger.removeHandler(x)
console_log_handler = logging.StreamHandler()
console_log_handler.setLevel(logging.DEBUG)
# All console messages are the same color (except with colored level names)
console_formatter = logging.Formatter('%(asctime)s %(process)s'
' \x1b[0;32m%(levelname)s'
'\t%(message)s\x1b[0m', datefmt="%H:%M:%S")
console_log_handler.setFormatter(console_formatter)
logger.addHandler(console_log_handler)
def configure_logging():
"""Configure root logger"""
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# Level name colored differently (both console and file)
logging.addLevelName(logging.WARNING, '\x1b[0;33m%s\x1b[0m' %
logging.getLevelName(logging.WARNING))
logging.addLevelName(logging.ERROR, '\x1b[0;31m%s\x1b[0m' %
logging.getLevelName(logging.ERROR))
# Configure console logging
console_log_handler = logging.StreamHandler()
console_log_handler.setLevel(logging.INFO)
# All console messages are the same color (except with colored level names)
console_formatter = logging.Formatter('\x1b[0;32m%(levelname)s'
'\t%(message)s\x1b[0m')
console_log_handler.setFormatter(console_formatter)
logger.addHandler(console_log_handler)
# Configure log file
hf.clean_dir(conf.log_dir)
log_file = os.path.join(conf.log_dir, 'stacktrain.log')
file_log_handler = logging.FileHandler(log_file)
file_log_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(process)s %(asctime)s.%(msecs)03d'
' %(name)s %(levelname)s %(message)s',
datefmt="%H:%M:%S")
file_log_handler.setFormatter(file_formatter)
logger.addHandler(file_log_handler)
logger.debug("Root logger configured.")
def parse_args():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(description="stacktrain main program.")
parser.add_argument('-w', '--wbatch', action='store_true',
help='Create Windows batch files')
parser.add_argument('--verbose-console', action='store_true',
help='Include time, PID, and DEBUG level messages')
parser.add_argument('-b', '--build', action='store_true',
help='Build cluster on local machine')
parser.add_argument('-q', '--quick', action='store_true',
help='Disable snapshot cycles during build')
parser.add_argument('-t', '--jump-snapshot', metavar='TARGET_SNAPSHOT',
help='Jump to target snapshot and continue build')
parser.add_argument('-g', '--gui', metavar='GUI_TYPE',
help=('GUI type during build (headless, '
'vnc [KVM only], '
'separate|gui [VirtualBox only]'))
parser.add_argument('target', metavar='TARGET',
help="usually basedisk or cluster")
parser.add_argument('-p', '--provider', metavar='PROVIDER', nargs='?',
help='Either virtualbox (VirtualBox) or kvm (KVM)')
parser.add_argument('--verbose', action='store_true')
return parser.parse_args()
def set_conf_vars(args):
"""Store command line args in configuration variables"""
logger = logging.getLogger(__name__)
conf.verbose_console = args.verbose_console
if conf.verbose_console:
enable_verbose_console()
if not args.wbatch and not args.build:
logger.error("Neither -b nor -w given, nothing to do. Exiting.")
sys.exit(1)
conf.do_build = args.build
conf.wbatch = args.wbatch
# Arguments override configuration
logger.debug("Provider: %s (config), %s (args)", conf.provider,
args.provider)
conf.provider = args.provider or conf.provider
conf.check_provider()
logger.info("Using provider %s.", conf.provider)
gui_opts = ["headless"]
if conf.provider == "virtualbox":
gui_opts.extend(("separate", "gui"))
# For VirtualBox, default to headless...
conf.vm_ui = args.gui or "headless"
# ...unless it it is for Windows batch files
if conf.wbatch:
# 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" for Windows
# batch files, which works at least on 5.0.26 and 5.1.6.
if conf.vm_ui == "headless":
conf.vm_ui = "separate"
if args.gui == "headless":
# headless was set by user, let them know
logger.warning('Overriding UI type "headless" with '
'"separate" for Windows batch files.')
elif conf.provider == "kvm":
gui_opts.append("vnc")
# For kvm, default to vnc
conf.vm_ui = args.gui or "vnc"
if conf.vm_ui not in gui_opts:
logger.warning('Valid options for provider %s: %s.', conf.provider,
", ".join(gui_opts))
logger.error('Invalid gui option: "%s". Aborting.', args.gui)
sys.exit(1)
if os.environ.get('SNAP_CYCLE') == 'no':
logger.info("Picked up SNAP_CYCLE=no from environment.")
conf.snapshot_cycle = False
if args.quick:
conf.snapshot_cycle = False
if args.jump_snapshot:
conf.jump_snapshot = args.jump_snapshot
conf.leave_vms_running = bool(os.environ.get('LEAVE_VMS_RUNNING') == 'yes')
wbatch.init()
def abort_if_root_user():
if not os.geteuid():
print("Please run this program as a regular user, not as root or"
" with sudo. Aborting.")
sys.exit(1)
def main():
abort_if_root_user()
configure_logging()
logger = logging.getLogger(__name__)
logger.debug("Call args: %s", sys.argv)
args = parse_args()
set_conf_vars(args)
import stacktrain.core.autostart as autostart
import stacktrain.core.node_builder as node_builder
import stacktrain.core.functions_host as host
# W0612: variable defined but not used
# pylint_: disable=W0612
# Only for the benefit of sfood
# import stacktrain.virtualbox.install_base
logger.debug("importing stacktrain.%s.install_base", conf.provider)
install_base = importlib.import_module("stacktrain.%s.install_base" %
conf.provider)
logger.debug("importing stacktrain.%s.vm_create", conf.provider)
vm = importlib.import_module("stacktrain.%s.vm_create" %
conf.provider)
vm.init()
logger.info("stacktrain start at %s", time.strftime("%c"))
# OS X sets LC_CTYPE to UTF-8 which results in errors when exported to
# (remote) environments
if "LC_CTYPE" in os.environ:
logger.debug("Removing LC_CTYPE from environment.")
del os.environ["LC_CTYPE"]
# To be on the safe side, ensure a sane locale
os.environ["LC_ALL"] = "C"
logger.debug("Environment %s", os.environ)
autostart.autostart_reset()
if conf.wbatch:
wbatch.wbatch_reset()
if conf.do_build and install_base.base_disk_exists():
if args.target == "basedisk":
print("Basedisk exists: %s" % conf.get_base_disk_name())
print("\tDestroy and recreate? [y/N] ", end='')
ans = raw_input().lower()
if ans == 'y':
logger.info("Deleting existing basedisk.")
start_time = time.time()
install_base.vm_install_base()
logger.info("Basedisk build took %s seconds",
hf.fmt_time_diff(start_time))
elif conf.wbatch:
logger.info("Windows batch file build only.")
tmp_do_build = conf.do_build
conf.do_build = False
install_base.vm_install_base()
conf.do_build = tmp_do_build
else:
print("Nothing to do.")
print("Done, returning now.")
return
elif conf.wbatch:
logger.info("Windows batch file build only.")
tmp_do_build = conf.do_build
conf.do_build = False
install_base.vm_install_base()
conf.do_build = tmp_do_build
else:
start_time = time.time()
install_base.vm_install_base()
logger.info("Basedisk build took %s seconds",
hf.fmt_time_diff(start_time))
if args.target == "basedisk":
print("We are done.")
return
host.create_host_networks()
start_time = time.time()
node_builder.build_nodes(args.target)
logger.info("Cluster build took %s seconds", hf.fmt_time_diff(start_time))
report.print_summary()
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,12 @@
# # Set default logging handler to avoid "No handler found" warnings.
# import logging
# try: # Python 2.7+
# from logging import NullHandler
# except ImportError:
# class NullHandler(logging.Handler):
# def emit(self, record):
# pass
#
# logging.getLogger(__name__).addHandler(NullHandler())
#import logging
#logging.getLogger(__name__).setLevel(logging.DEBUG)

View File

@ -0,0 +1,284 @@
"""
This library contains the functions that allow stacktrain to produce
Windows batch files.
"""
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import logging
import io
import ntpath
import os
import re
from string import Template
import stacktrain.config.general as conf
import stacktrain.core.helpers as hf
logger = logging.getLogger(__name__)
WBATCH_OUT_DIR = os.path.join(conf.top_dir, "wbatch")
# wbatch template dir
TPLT_DIR = os.path.join(conf.top_dir, "stacktrain/batch_for_windows_templates")
OUT_FILE = None
def wbatch_reset():
"""Clean Windows batch directory"""
hf.clean_dir(WBATCH_OUT_DIR)
def init():
"""Initialize variables and directory for Windows batch script creation"""
if conf.wbatch:
if hasattr(conf, 'vm_access') and conf.vm_access == "all":
logging.info("Already configured for shared folder access.")
else:
logging.info("Setting vm_access method to shared folder.")
conf.vm_access = "shared_folder"
else:
logging.debug("Not building Windows batch files.")
wbatch_reset()
def wbatch_new_file(file_name):
"""Create new Windows batch file"""
global OUT_FILE
hf.create_dir(WBATCH_OUT_DIR)
OUT_FILE = os.path.join(WBATCH_OUT_DIR, file_name)
open(OUT_FILE, "a").close()
def wbatch_close_file():
global OUT_FILE
OUT_FILE = None
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class WbatchTemplate(Template):
# Default delimiter "$" occurs directly after backslash (in Windows paths)
delimiter = '#'
idpattern = r'[A-Z][_A-Z0-9]*'
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Note: Windows batch scripts with LF may seem to work, but (for instance) jump
# labels don't work properly
def wbatch_write(*args):
if OUT_FILE:
with io.open(OUT_FILE, 'a', newline='\r\n') as out:
try:
string = unicode(*args).rstrip()
out.write(string + "\n")
except TypeError:
logging.error("wbatch can't print %s: %s", type(str(*args)),
str(*args))
logging.exception("Exception")
import sys
sys.exit(1)
def wbatch_write_template(template, replace=None):
if replace is None:
replace = {}
with open(os.path.join(TPLT_DIR, template)) as tf:
for line in tf:
te = WbatchTemplate(line)
wbatch_write(te.substitute(replace))
# -----------------------------------------------------------------------------
# Batch function calls
# -----------------------------------------------------------------------------
def wbatch_abort_if_vm_exists(vm_name):
te = WbatchTemplate(u"CALL :vm_exists #VM_NAME")
wbatch_write(te.substitute(VM_NAME=vm_name))
def wbatch_wait_poweroff(vm_name):
te = WbatchTemplate(u"""ECHO %time% Waiting for VM #VM_NAME to power off.
CALL :wait_poweroff #VM_NAME
ECHO %time% VM #VM_NAME powered off.
""")
wbatch_write(te.substitute(VM_NAME=vm_name))
def wbatch_wait_auto():
te = WbatchTemplate(u"""ECHO %time% Waiting for autostart files to execute.
CALL :wait_auto
ECHO %time% All autostart files executed.
""")
wbatch_write(te.substitute())
# -----------------------------------------------------------------------------
# Batch commands
# -----------------------------------------------------------------------------
def wbatch_delete_disk(disk_path):
disk_name = os.path.basename(disk_path)
te = WbatchTemplate(r"IF EXIST %IMGDIR%\#DISK DEL %IMGDIR%\#DISK")
wbatch_write(te.substitute(DISK=disk_name))
def wbatch_rename_disk(src_name, target_name):
te = WbatchTemplate(r"MOVE /y %IMGDIR%\#SRC %IMGDIR%\#TARGET")
wbatch_write(te.substitute(SRC=src_name, TARGET=target_name))
def wbatch_cp_auto(src_path, target_path):
src = wbatch_path_to_windows(src_path)
target = os.path.basename(target_path)
te = WbatchTemplate(r"COPY %TOPDIR%\#SRC %AUTODIR%\#TARGET")
wbatch_write(te.substitute(SRC=src, TARGET=target))
def wbatch_sleep(seconds):
te = WbatchTemplate(r"TIMEOUT /T #SECONDS /NOBREAK")
wbatch_write(te.substitute(SECONDS=seconds))
# -----------------------------------------------------------------------------
# Templated parts
# -----------------------------------------------------------------------------
def wbatch_file_header(product):
replace = {"PRODUCT": product}
wbatch_write_template("template-file_header_bat", replace)
def wbatch_end_file():
wbatch_write_template("template-end_file_bat")
wbatch_close_file()
def wbatch_elevate_privileges():
wbatch_write_template("template-elevate_privs_bat")
def wbatch_find_vbm():
wbatch_write_template("template-find_vbm_bat")
def wbatch_mkdirs():
autodir = wbatch_path_to_windows(conf.autostart_dir)
imgdir = wbatch_path_to_windows(conf.img_dir)
logdir = wbatch_path_to_windows(conf.log_dir)
statusdir = wbatch_path_to_windows(conf.status_dir)
replace = {"AUTODIR": autodir,
"IMGDIR": imgdir,
"LOGDIR": logdir,
"STATUSDIR": statusdir}
wbatch_write_template("template-mkdirs_bat", replace)
def wbatch_begin_hostnet():
wbatch_new_file("create_hostnet.bat")
wbatch_file_header("host-only networks")
# Creating networks requires elevated privileges
wbatch_elevate_privileges()
wbatch_find_vbm()
def wbatch_create_hostnet(if_ip, adapter):
adapter = vboxnet_to_win_adapter_num(adapter)
replace = {"IFNAME": adapter,
"IFIP": if_ip}
wbatch_write_template("template-create_hostnet_bat", replace)
def wbatch_begin_base():
# Disable E1101 (no-member) in pylint
iso_name = os.path.basename(conf.iso_image.name) # pylint: disable=E1101
if not iso_name:
logging.error("Windows batch file needs install ISO URL.")
raise ValueError
wbatch_new_file("create_base.bat")
wbatch_file_header("base disk")
wbatch_find_vbm()
wbatch_mkdirs()
replace = {"INSTALLFILE": conf.iso_image.name, # pylint: disable=no-member
"ISOURL": conf.iso_image.url} # pylint: disable=no-member
wbatch_write_template("template-begin_base_bat", replace)
def wbatch_begin_node(node_name):
wbatch_new_file("create_{}_node.bat".format(node_name))
wbatch_file_header("{} VM".format(node_name))
wbatch_find_vbm()
wbatch_mkdirs()
basedisk = "{}.vdi".format(conf.get_base_disk_name())
replace = {"BASEDISK": basedisk}
wbatch_write_template("template-begin_node_bat", replace)
# -----------------------------------------------------------------------------
# VBoxManage call handling
# -----------------------------------------------------------------------------
def wbatch_log_vbm(*args):
argl = list(*args)
for index, arg in enumerate(argl):
if re.match("--hostonlyadapter", arg):
# The next arg is the host-only interface name -> change it
argl[index+1] = '"' + vboxnet_to_win_adapter_num(argl[index+1]) + \
'"'
elif re.match("--hostpath", arg):
# The next arg is the shared dir -> change it
argl[index+1] = r'%SHAREDIR%'
elif re.search(r"\.(iso|vdi)$", arg):
# Fix path of ISO or VDI image
img_name = os.path.basename(arg)
argl[index] = ntpath.join("%IMGDIR%", img_name)
# Have Windows echo what we are about to do
wbatch_write("ECHO VBoxManage " + " ". join(argl))
wbatch_write("VBoxManage " + " ". join(argl))
# Abort if VBoxManage call raised errorlevel
wbatch_write("IF %errorlevel% NEQ 0 GOTO :vbm_error")
# Blank line for readability
wbatch_write()
# -----------------------------------------------------------------------------
# Windows path name helpers
# -----------------------------------------------------------------------------
def vboxnet_to_win_adapter_num(vboxname):
win_if = "VirtualBox Host-Only Ethernet Adapter"
# Remove leading "vboxnet" to get interface number
if_num = int(vboxname.replace("vboxnet", ""))
if if_num > 0:
# The first numbered "VirtualBox Host-Only Ethernet Adapter" is #2
win_if += " #{}".format(str(if_num + 1))
logger.debug("vboxnet_to_win_adapter_num returns: %s", win_if)
return win_if
def wbatch_path_to_windows(full_path):
rel_path = hf.strip_top_dir(conf.top_dir, full_path)
# Convert path to backslash-type as expected by Windows batch files
rel_path = ntpath.normpath(rel_path)
return rel_path

View File

@ -0,0 +1,37 @@
ECHO %time% Cleaning up autostart and log directories
DEL /S /Q %AUTODIR%
DEL /S /Q %LOGDIR%
ECHO %time% Looking for %IMGDIR%\#INSTALLFILE
IF EXIST %IMGDIR%\#INSTALLFILE goto got_install_iso
ECHO.
ECHO #INSTALLFILE not found in %IMGDIR%.
ECHO.
ECHO Trying to download the install ISO from
ECHO #ISOURL
ECHO.
ECHO Expect this to take several minutes or longer, depending on your
ECHO Internet connection.
ECHO.
cscript /nologo %TOOLSDIR%\downloader.js #ISOURL
RENAME downloaded.bin #INSTALLFILE
MOVE #INSTALLFILE %IMGDIR%
IF EXIST %IMGDIR%\#INSTALLFILE goto got_install_iso
ECHO.
ECHO #INSTALLFILE still not found in %IMGDIR%.
ECHO Aborting.
ECHO.
goto :terminate
:got_install_iso
ECHO.
ECHO %time% Found %IMGDIR%\#INSTALLFILE
ECHO.
ECHO %time% Initialization done. Hit any key to continue.
ECHO.
PAUSE
REM vim: set ai ts=4 sw=4 et ft=dosbatch:

View File

@ -0,0 +1,23 @@
ECHO %time% Cleaning up autostart and log directories
DEL /S /Q %AUTODIR%
DEL /S /Q %LOGDIR%
ECHO %time% Looking for %IMGDIR%\#BASEDISK
IF EXIST %IMGDIR%\#BASEDISK goto got_base_disk
ECHO.
ECHO #BASEDISK not found in %IMGDIR%.
ECHO.
ECHO You need to build a base disk before you can create node VMs.
ECHO.
goto :terminate
:got_base_disk
ECHO.
ECHO %time% Found %IMGDIR%\#BASEDISK
ECHO.
ECHO %time% Initialization done. Hit any key to continue.
ECHO.
PAUSE
REM vim: set ai ts=4 sw=4 et ft=dosbatch:

View File

@ -0,0 +1,9 @@
ECHO VBoxManage hostonlyif create
VBoxManage hostonlyif create
IF %errorlevel% NEQ 0 GOTO :vbm_error
ECHO VBoxManage hostonlyif ipconfig "#IFNAME" --ip #IFIP --netmask 255.255.255.0
VBoxManage hostonlyif ipconfig "#IFNAME" --ip #IFIP --netmask 255.255.255.0
IF %errorlevel% NEQ 0 GOTO :vbm_error
REM vim: set ai ts=4 sw=4 et ft=dosbatch:

View File

@ -0,0 +1,26 @@
REM Elevate credentials, code courtesy of Matthew Newton
REM http://blog.mnewton.com/articles/Windows-Installer-Batch-Script-Revisited.html
REM Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
REM If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
echo Requesting administrative privileges...
goto UACPrompt
) else ( goto gotAdmin )
:UACPrompt
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"
"%temp%\getadmin.vbs"
REM we are done, exiting recursive call
exit /B
:gotAdmin
if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
echo We have admin privileges, proceeding...
REM vim: set ai ts=4 sw=4 et ft=dosbatch:

View File

@ -0,0 +1,59 @@
ECHO.
ECHO %time% Batch script seems to have succeeded.
ECHO.
GOTO :terminate
REM Note: vbm_error falls through to terminate
:vbm_error
ECHO.
ECHO %time% VBoxManage returned with an error. Aborting.
ECHO.
:terminate
ENDLOCAL
PAUSE
EXIT
GOTO :eof
REM ============================================================================
REM
REM End of program, function definitions follow
REM
REM ============================================================================
:wait_auto
IF EXIST %STATUSDIR%\done (
DEL %STATUSDIR%\done
GOTO :eof
)
IF EXIST %STATUSDIR%\error (
ECHO.
ECHO %time% ERROR Script returned error:
ECHO.
TYPE %STATUSDIR%\error
ECHO.
ECHO %time% Aborting.
ECHO.
DEL %STATUSDIR%\error
GOTO :terminate
)
TIMEOUT /T 5 /NOBREAK
GOTO :wait_auto
REM - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
:wait_poweroff
VBoxManage showvminfo %~1 --machinereadable|findstr poweroff
IF %errorlevel% EQU 0 GOTO :eof
TIMEOUT /T 2 /NOBREAK
GOTO :wait_poweroff
REM - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
:vm_exists
VBoxManage list vms|findstr %~1
IF %errorlevel% NEQ 0 GOTO :eof
ECHO.
ECHO %time% VM %~1 already exists. Aborting.
ECHO.
GOTO :terminate
REM - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
REM vim: set ai ts=4 sw=4 et ft=dosbatch:

View File

@ -0,0 +1,16 @@
@ECHO OFF
REM This is an automatically generated Windows batch file. It creates the
REM #PRODUCT for an OpenStack training-labs setup.
SETLOCAL ENABLEDELAYEDEXPANSION
ECHO.
ECHO OpenStack labs for VirtualBox on Windows
ECHO Generated by osbash
ECHO.
ECHO Create #PRODUCT
ECHO.
REM vim: set ai ts=4 sw=4 et ft=dosbatch:

View File

@ -0,0 +1,30 @@
REM VBoxManage is not in PATH, but this is a good guess
IF EXIST %ProgramFiles%\Oracle\VirtualBox\VBoxManage.exe (
SET PATH=%PATH%;%ProgramFiles%\Oracle\VirtualBox
ECHO.
ECHO %time% Found %ProgramFiles%\Oracle\VirtualBox\VBoxManage.exe
ECHO.
GOTO :vbm_found
)
ECHO.
ECHO %time% Searching %SystemDrive% for VBoxManage, this may take a while
ECHO.
FOR /r %SystemDrive% %%a IN (*) DO (
IF "%%~nxa"=="VBoxManage.exe" (
SET PATH=%PATH%;%%~dpa
ECHO %time% Found %%~dpnxa
GOTO :vbm_found
)
)
ECHO.
ECHO %time% Cannot find VBoxManage.exe (part of VirtualBox) on %SystemDrive%.
ECHO %time% Program stops.
ECHO.
GOTO :terminate
:vbm_found
REM vim: set ai ts=4 sw=4 et ft=dosbatch:

View File

@ -0,0 +1,20 @@
SET BATDIR=%~dp0
PUSHD %BATDIR%..
SET TOPDIR=%cd%
POPD
SET AUTODIR=%TOPDIR%\#AUTODIR
SET IMGDIR=%TOPDIR%\#IMGDIR
SET LOGDIR=%TOPDIR%\#LOGDIR
SET STATUSDIR=%TOPDIR%\#STATUSDIR
SET SHAREDIR=%TOPDIR%
SET TOOLSDIR=%TOPDIR%\tools
ECHO %time% Creating directories (if needed)
IF NOT EXIST %AUTODIR% mkdir %AUTODIR%
IF NOT EXIST %IMGDIR% mkdir %IMGDIR%
IF NOT EXIST %LOGDIR% mkdir %LOGDIR%
IF NOT EXIST %SHAREDIR% mkdir %SHAREDIR%
REM vim: set ai ts=4 sw=4 et ft=dosbatch:

View File

View File

@ -0,0 +1,323 @@
from __future__ import print_function
import collections
import logging
from os.path import dirname, join, realpath
import os
import platform
import re
import sys
import stacktrain.core.download as dl
import stacktrain.core.helpers as hf
logger = logging.getLogger(__name__)
do_build = False
wbatch = False
vm = {}
vm_ui = "gui"
# Default access method is ssh; Windows batch files use shared folders instead
vm_access = "ssh"
# If set, scripts before this snapshot are skipped
jump_snapshot = None
# -----------------------------------------------------------------------------
def check_provider():
if provider == "virtualbox":
if do_build and not hf.test_exe("VBoxManage", "-v"):
logger.error("VBoxManage not found. Is VirtualBox installed?")
logger.error("Aborting.")
sys.exit(1)
elif provider == "kvm":
if platform.uname()[0] != "Linux":
logger.error("Provider kvm only supported on Linux. Aborting.")
sys.exit(1)
if not hf.test_exe("virsh", "-v"):
logger.error("virsh not found. Aborting.")
sys.exit(1)
if wbatch:
logger.error("Cannot build Windows batch files with provider kvm."
"Aborting.")
sys.exit(1)
else:
logger.error("Unknown provider: %s", provider)
logger.error("Aborting.")
sys.exit(1)
# -----------------------------------------------------------------------------
def remove_quotation_marks(line):
"""Removes single or double quotation marks"""
# Remove quotation marks (if any)
ma = re.search(r"(?P<quote>['\"])(.+)(?P=quote)", line)
if ma:
line = ma.group(2)
return line
class CfgFileParser(object):
def __init__(self, cfg_file):
self.file_path = os.path.join(config_dir, cfg_file)
self.cfg_vars = {}
with open(self.file_path) as cfg:
for line in cfg:
if re.match(r"^\s*$", line):
# Line contains only white space
continue
ma = re.match(r"^: \${(\S+):=(.*)}$", line)
if ma:
# Special bash config style syntax
# e.g., ": ${OPENSTACK_RELEASE=mitaka}"
key = ma.group(1)
value = remove_quotation_marks(ma.group(2))
self.cfg_vars[key] = value
continue
if re.match(r"^(#|:)", line):
# Line starts with comment
continue
ma = re.match(r"^(\S+)=(.*)$", line)
if ma:
key = ma.group(1)
value = remove_quotation_marks(ma.group(2))
self.cfg_vars[key] = value
def get_value(self, var_name):
"""Return value for given key (or None if it does not exist)"""
try:
return self.cfg_vars[var_name]
except KeyError:
return None
def get_numbered_value(self, var_name_root):
"""Return dictionary of key:value pairs where key starts with arg"""
pairs = {}
for key in self.cfg_vars:
if key.startswith(var_name_root):
key_id = key.replace(var_name_root, "")
value = self.cfg_vars[key]
pairs[key_id] = value
return pairs
# -----------------------------------------------------------------------------
# top_dir is training-labs/labs
top_dir = dirname(dirname(dirname(realpath(__file__))))
# Parsing osbash's config/paths is not worth it; neither is having these
# paths in a config file for users to change
img_dir = join(top_dir, "img")
log_dir = join(top_dir, "log")
status_dir = join(log_dir, "status")
osbash_dir = join(top_dir, "osbash")
config_dir = join(top_dir, "config")
scripts_dir = join(top_dir, "scripts")
autostart_dir = join(top_dir, "autostart")
lib_dir = join(top_dir, "lib")
# -----------------------------------------------------------------------------
# config/localrc
cfg_localrc = CfgFileParser("localrc")
vm_proxy = cfg_localrc.get_value("VM_PROXY")
dl.downloader.vm_proxy = vm_proxy
provider = cfg_localrc.get_value("PROVIDER")
logger.debug("Checking provider given by config/localarc: %s", provider)
check_provider()
distro_full = cfg_localrc.get_value("DISTRO")
# ubuntu-14.04-server-amd64 -> ubuntu_14_04_server_amd64
distro_full = re.sub(r'[-.]', '_', distro_full)
# -----------------------------------------------------------------------------
# config/deploy.osbash
cfg_deploy_osbash = CfgFileParser("deploy.osbash")
vm_shell_user = cfg_deploy_osbash.get_value("VM_SHELL_USER")
# -----------------------------------------------------------------------------
# config/credentials
cfg_credentials = CfgFileParser("credentials")
admin_user = cfg_credentials.get_value("ADMIN_USER_NAME")
admin_password = cfg_credentials.get_value("ADMIN_PASS")
demo_user = cfg_credentials.get_value("DEMO_USER_NAME")
demo_password = cfg_credentials.get_value("DEMO_PASS")
# -----------------------------------------------------------------------------
# config/openstack
cfg_openstack = CfgFileParser("openstack")
openstack_release = cfg_openstack.get_value("OPENSTACK_RELEASE")
pxe_initial_node_ip = cfg_openstack.get_value("PXE_INITIAL_NODE_IP")
# Get all variables starting with NETWORK_
networks_cfg = cfg_openstack.get_numbered_value("NETWORK_")
# Network order matters (the cluster should work either way, but the
# networks may end up assigned to different interfaces)
networks = collections.OrderedDict()
for index, value in networks_cfg.items():
ma = re.match(r'(\S+)\s+([\.\d]+)', value)
if ma:
name = ma.group(1)
address = ma.group(2)
networks[name] = address
else:
logger.error("Syntax error in NETWORK_%s: %s", index, value)
sys.exit(1)
# -----------------------------------------------------------------------------
snapshot_cycle = True
# Base disk size in MB
base_disk_size = 10000
distro = ""
def get_base_disk_name():
return "base-{}-{}-{}".format(vm_access, openstack_release,
iso_image.release_name)
class VMconfig(object):
def __init__(self, vm_name):
self.vm_name = vm_name
self.disks = []
self._ssh_ip = None
self._ssh_port = None
self.http_port = None
self.get_config_from_file()
self.pxe_tmp_ip = None
if provider == "virtualbox":
# TODO is IPaddress class worth using?
# self._ssh_ip = IPaddress("127.0.0.1")
self._ssh_ip = "127.0.0.1"
elif provider == "kvm":
# Override whatever we got from config file
self._ssh_port = 22
else:
logger.error("No provider defined. Aborting.")
sys.exit(1)
logger.debug(self.__repr__())
def __repr__(self):
repr = "<VMconfig: vm_name=%r" % self.vm_name
repr += " disks=%r" % self.disks
repr += " ssh_ip=%r" % self.ssh_ip
repr += " ssh_port=%r" % self.ssh_port
if self.pxe_tmp_ip:
repr += " pxe_tmp_ip=%r" % self.pxe_tmp_ip
repr += " _ssh_port=%r" % self._ssh_port
repr += " _ssh_ip=%r" % self._ssh_ip
repr += " http_port=%r" % self.http_port
repr += " vm_mem=%r" % self.vm_mem
repr += " vm_cpus=%r" % self.vm_cpus
repr += " net_ifs=%r" % self.net_ifs
repr += ">"
return repr
def get_config_from_file(self):
cfg_vm = CfgFileParser("config." + self.vm_name)
if provider == "virtualbox":
# Port forwarding only on VirtualBox
self._ssh_port = cfg_vm.get_value("VM_SSH_PORT")
if self._ssh_port:
logger.debug("Port forwarding ssh: %s", self._ssh_port)
self.http_port = cfg_vm.get_value("VM_WWW_PORT")
if self.http_port:
logger.debug("Port forwarding http: %s", self.http_port)
self.vm_mem = cfg_vm.get_value("VM_MEM") or 512
self.vm_cpus = cfg_vm.get_value("VM_CPUS") or 1
net_if_cfg = cfg_vm.get_numbered_value("NET_IF_")
# Create array of required size
self.net_ifs = [{} for _ in range(len(net_if_cfg))]
for key in net_if_cfg:
self._parse_net_line(int(key), net_if_cfg[key])
# If the size of the first disk is given, it's not using the base
# disk (i.e. probably building via PXE booting)
self.disks.append(cfg_vm.get_value("FIRST_DISK_SIZE") or "base")
logger.debug("Disks: %s", self.disks[-1])
self.disks.append(cfg_vm.get_value("SECOND_DISK_SIZE"))
if self.disks[1]:
logger.debug(" %s", self.disks[1])
self.disks.append(cfg_vm.get_value("THIRD_DISK_SIZE"))
if self.disks[2]:
logger.debug(" %s", self.disks[2])
def _parse_net_line(self, index, line):
args = re.split(r'\s+', line)
self.net_ifs[index]["typ"] = args[0]
if len(args) > 1:
self.net_ifs[index]["ip"] = args[1]
if len(args) > 2:
self.net_ifs[index]["prio"] = args[2]
else:
self.net_ifs[index]["prio"] = 0
@property
def ssh_ip(self):
return self.pxe_tmp_ip or self._ssh_ip
@ssh_ip.setter
def ssh_ip(self, value):
self._ssh_ip = value
# TODO make all callers expect int, not str
@property
def ssh_port(self):
return "22" if self.pxe_tmp_ip else self._ssh_port
@ssh_port.setter
def ssh_port(self, value):
self._ssh_port = int(value)
class IPaddress(object):
def __init__(self, ip):
self.ip = ip
def __repr__(self):
return "<IPaddress ip=%s>" % self.ip
def __str__(self):
return self.ip
@property
def network(self):
"""Return /24 subnet address"""
return self.remove_last_octet(self.ip) + '0'
# @c_class_network.setter
# def c_class_network(self, network):
# self._ssh_ip = ssh_ip
def same_c_class_network(self, ip):
return self.remove_last_octet(self.ip) == self.remove_last_octet(ip)
@staticmethod
def remove_last_octet(ip):
ma = re.match(r'(\d+\.\d+.\d+\.)\d+', ip)
if ma:
return ma.group(1)
else:
raise ValueError

View File

@ -0,0 +1,12 @@
import os
import stacktrain.config.general as conf
conf.provider = "virtualbox"
conf.share_name = "osbash"
conf.share_dir = conf.top_dir
conf.vm_ui = "headless"
def get_base_disk_path():
return os.path.join(conf.img_dir, conf.get_base_disk_name() + ".vdi")

View File

View File

@ -0,0 +1,305 @@
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
from importlib import import_module
import logging
import os
from os.path import basename, isfile, join
import re
import sys
import time
from glob import glob
import stacktrain.config.general as conf
import stacktrain.core.ssh as ssh
import stacktrain.batch_for_windows as wbatch
import stacktrain.core.functions_host as host
import stacktrain.core.helpers as hf
import stacktrain.core.iso_image as iso_image
# import stacktrain.kvm.install_node as inst_node
# import stacktrain.virtualbox.vm_create as vm
inst_node = import_module("stacktrain.%s.install_node" % conf.provider)
vm = import_module("stacktrain.%s.vm_create" % conf.provider)
logger = logging.getLogger(__name__)
def ssh_exec_script(vm_name, script_path):
ssh.vm_scp_to_vm(vm_name, script_path)
remote_path = hf.strip_top_dir(conf.top_dir, script_path)
logger.info("Start %s", remote_path)
script_name = os.path.splitext(os.path.basename(script_path))[0]
prefix = host.get_next_prefix(conf.log_dir, "auto")
log_name = "{}_{}.auto".format(prefix, script_name)
log_path = os.path.join(conf.log_dir, log_name)
try:
ssh.vm_ssh(vm_name,
"bash {} && rm -vf {}".format(remote_path, remote_path),
log_file=log_path)
except EnvironmentError:
logger.error("Script failure: %s", script_name)
sys.exit(1)
logger.info(" done")
def ssh_process_autostart(vm_name):
# If a KVM VM has been created by an earlier script run, its IP address
# is not known
if not conf.vm[vm_name].ssh_ip:
vm.node_to_ip(vm_name)
logger.info("Waiting for ssh server in VM %s to respond at %s:%s.",
vm_name, conf.vm[vm_name].ssh_ip, conf.vm[vm_name].ssh_port)
ssh.wait_for_ssh(vm_name)
logger.info(" Connected to ssh server.")
sys.stdout.flush()
ssh.vm_ssh(vm_name, "rm -rf osbash lib config autostart")
ssh.vm_scp_to_vm(vm_name, conf.lib_dir, conf.config_dir)
for script_path in sorted(glob(join(conf.autostart_dir, "*.sh"))):
ssh_exec_script(vm_name, script_path)
os.remove(script_path)
open(join(conf.status_dir, "done"), 'a').close()
# -----------------------------------------------------------------------------
# Autostart mechanism
# -----------------------------------------------------------------------------
def autostart_reset():
logger.debug("Resetting autostart directories.")
hf.clean_dir(conf.autostart_dir)
hf.clean_dir(conf.status_dir)
def process_begin_files():
for begin_file in sorted(glob(join(conf.status_dir, "*.sh.begin"))):
match = re.match(r'.*/(.*).begin', begin_file)
os.remove(begin_file)
logger.info("\nVM processing %s.", match.group(1))
def autofiles_processing_done():
err_path = join(conf.status_dir, "error")
done_path = join(conf.status_dir, "done")
return isfile(done_path) or isfile(err_path)
def wait_for_autofiles():
if conf.wbatch:
wbatch.wbatch_wait_auto()
if not conf.do_build:
# Remove autostart files and return if we are just faking it for wbatch
autostart_reset()
return
while not autofiles_processing_done():
if conf.wbatch:
# wbatch uses begin files (ssh method does not need them)
process_begin_files()
print('D' if conf.verbose_console else '.', end='')
sys.stdout.flush()
time.sleep(1)
# Check for remaining *.sh.begin files
if conf.wbatch:
process_begin_files()
if isfile(join(conf.status_dir, "done")):
os.remove(join(conf.status_dir, "done"))
else:
logger.error("Script failed. Exiting.")
sys.exit(1)
logger.info("Processing of scripts successful.")
def autostart_and_wait(vm_name):
sys.stdout.flush()
if not conf.wbatch:
import multiprocessing
# TODO multiprocessing logging to file
# mlogger = multiprocessing.get_logger()
try:
sshp = multiprocessing.Process(target=ssh_process_autostart,
args=(vm_name,))
sshp.start()
except Exception:
logger.exception("ssh_process_autostart")
raise
wait_for_autofiles()
if not conf.wbatch:
sshp.join()
logger.debug("sshp exit code: %s", sshp.exitcode)
if sshp.exitcode:
logger.error("sshp returned error!")
raise ValueError
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def _autostart_queue(src_rel_path, target_name=None):
src_path = join(conf.scripts_dir, src_rel_path)
src_name = basename(src_path)
if not target_name:
target_name = src_name
if target_name.endswith(".sh"):
prefix = host.get_next_prefix(conf.autostart_dir, "sh", 2)
target_name = "{}_{}".format(prefix, target_name)
if src_name == target_name:
logger.info("\t%s", src_name)
else:
logger.info("\t%s -> %s", src_name, target_name)
from shutil import copyfile
copyfile(src_path, join(conf.autostart_dir, target_name))
if conf.wbatch:
wbatch.wbatch_cp_auto(src_path, join(conf.autostart_dir, target_name))
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def autostart_queue_and_rename(src_dir, src_file, target_file):
_autostart_queue(join(src_dir, src_file), target_file)
def autostart_queue(*args):
for script in args:
_autostart_queue(script)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def syntax_error_abort(line):
logger.error("Syntax error: %s", line)
sys.exit(1)
def get_vmname_arg(line, args):
if len(args) == 3 and args[1] == "-n":
vm_name = args[2]
if vm_name not in conf.vm:
conf.vm[vm_name] = conf.VMconfig(vm_name)
else:
syntax_error_abort(line)
return vm_name
def get_two_args(line, args):
if len(args) == 4 and args[1] == "-n":
vm_name = args[2]
arg2 = args[3]
if vm_name not in conf.vm:
conf.vm[vm_name] = conf.VMconfig(vm_name)
else:
syntax_error_abort(line)
return vm_name, arg2
def command_from_config(line):
# Drop trailing whitespace and newline
line = line.rstrip()
# Drop first argument ("cmd")
args = line.split(" ")[1:]
if args[0] == "boot":
vm_name = get_vmname_arg(line, args)
vm.vm_boot(vm_name)
autostart_and_wait(vm_name)
conf.vm[vm_name].pxe_tmp_ip = None
elif args[0] == "snapshot":
vm_name, shot_name = get_two_args(line, args)
host.vm_conditional_snapshot(vm_name, shot_name)
elif args[0] == "shutdown":
vm_name = get_vmname_arg(line, args)
vm.vm_acpi_shutdown(vm_name)
vm.vm_wait_for_shutdown(vm_name)
elif args[0] == "wait_for_shutdown":
vm_name = get_vmname_arg(line, args)
vm.vm_wait_for_shutdown(vm_name)
elif args[0] == "snapshot_cycle":
if not conf.snapshot_cycle:
return
vm_name, shot_name = get_two_args(line, args)
_autostart_queue("shutdown.sh")
vm.vm_boot(vm_name)
autostart_and_wait(vm_name)
conf.vm[vm_name].pxe_tmp_ip = None
vm.vm_wait_for_shutdown(vm_name)
host.vm_conditional_snapshot(vm_name, shot_name)
elif args[0] == "boot_set_tmp_node_ip":
vm_name = get_vmname_arg(line, args)
logger.info("Setting temporary IP for PXE booting to %s.",
conf.pxe_initial_node_ip)
conf.vm[vm_name].pxe_tmp_ip = conf.pxe_initial_node_ip
elif args[0] == "create_node":
vm_name = get_vmname_arg(line, args)
inst_node.vm_create_node(vm_name)
logger.info("Node %s created.", vm_name)
elif args[0] == "create_pxe_node":
vm_name = get_vmname_arg(line, args)
conf.vm[vm_name].disks[0] = 10000
inst_node.vm_create_node(vm_name)
logger.info("PXE node %s created.", vm_name)
elif args[0] == "queue_renamed":
vm_name, script_rel_path = get_two_args(line, args)
template_name = os.path.basename(script_rel_path)
new_name = template_name.replace("xxx", vm_name)
_autostart_queue(script_rel_path, new_name)
elif args[0] == "queue":
script_rel_path = args[1]
_autostart_queue(script_rel_path)
elif args[0] == "cp_iso":
vm_name = get_vmname_arg(line, args)
iso_path = iso_image.find_install_iso()
ssh.vm_scp_to_vm(vm_name, iso_path)
else:
syntax_error_abort(line)
# Parse config/scripts.* configuration files
def autostart_from_config(cfg_file):
cfg_path = join(conf.config_dir, cfg_file)
if not isfile(cfg_path):
logger.error("Config file not found:\n\t%s", cfg_path)
raise Exception
with open(cfg_path) as cfg:
for line in cfg:
if re.match('#', line):
continue
if re.match(r"\s?$", line):
continue
if not re.match(r"cmd\s", line):
logger.error("Syntax error in line:\n\t%s", line)
raise Exception
if conf.jump_snapshot:
ma = re.match(r"cmd\s+snapshot.*\s+(\S)$", line)
if ma:
logger.info("Skipped forward to snapshot %s.",
conf.jump_snapshot)
del conf.jump_snapshot
continue
command_from_config(line)

View File

@ -0,0 +1,17 @@
import time
import stacktrain.config.general as conf
import stacktrain.batch_for_windows as wbatch
# -----------------------------------------------------------------------------
# Conditional sleeping
# -----------------------------------------------------------------------------
def conditional_sleep(seconds):
# Don't sleep if we are just faking it for wbatch
if conf.do_build:
time.sleep(seconds)
if conf.wbatch:
wbatch.wbatch_sleep(seconds)

View File

@ -0,0 +1,71 @@
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import httplib
import logging
import os
import urllib2
import stacktrain.core.helpers as hf
logger = logging.getLogger(__name__)
class Downloader(object):
def __init__(self):
self._vm_proxy = None
def get_urllib_proxy(self):
res = None
if urllib2._opener: # pylint: disable=protected-access
for handler in urllib2._opener.handlers: # pylint: disable=W0212
if isinstance(handler, urllib2.ProxyHandler):
logger.debug("get_urllib_proxy proxies: %s",
handler.proxies)
res = handler.proxies
logger.debug("get_urllib_proxy proxies: %s", res)
return res
@property
def vm_proxy(self):
logger.debug("Downloader getter vm_proxy %s (proxy: %s)",
self._vm_proxy, self.get_urllib_proxy())
return self._vm_proxy
@vm_proxy.setter
def vm_proxy(self, value):
if self._vm_proxy:
proxy_handler = urllib2.ProxyHandler({'http': self._vm_proxy})
self._vm_proxy = value
else:
# Remove existing proxy setting
logger.debug("Downloader unsetting vm_proxy.")
proxy_handler = urllib2.ProxyHandler({})
self._vm_proxy = None
urllib2.install_opener(urllib2.build_opener(proxy_handler))
logger.debug("Proxy now: %s", self.get_urllib_proxy())
def download(self, url, target_path=None):
try:
logger.debug("Trying to download: %s to %s", url, target_path)
logger.debug("Proxy: %s", self.get_urllib_proxy())
response = urllib2.urlopen(url)
if target_path:
# Make sure target directory exits
hf.create_dir(os.path.dirname(target_path))
with open(target_path, 'wb') as out:
try:
out.write(response.read())
except urllib2.URLError as err:
# Download failed, remove empty file
os.remove(target_path)
else:
return response.read()
except (urllib2.URLError, httplib.BadStatusLine) as err:
logger.debug("download() failed, %s for %s", type(err), url)
raise EnvironmentError
downloader = Downloader()

View File

@ -0,0 +1,92 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import importlib
import logging
import re
import os
import os.path
import subprocess
import time
import stacktrain.config.general as conf
import stacktrain.core.helpers as hf
import stacktrain.batch_for_windows as wbatch
logger = logging.getLogger(__name__)
vm = importlib.import_module("stacktrain.%s.vm_create" % conf.provider)
# Wrapper around vm_snapshot to deal with collisions with cluster rebuilds
# starting from snapshot. We could delete the existing snapshot first,
# rename the new one, or just skip the snapshot.
def vm_conditional_snapshot(vm_name, shot_name):
if conf.wbatch:
# We need to record the proper command for wbatch; if a snapshot
# exists, something is wrong and the program will abort
vm.vm_snapshot(vm_name, shot_name)
# It is not wbatch, so it must be do_build
elif not vm.vm_snapshot_exists(vm_name, shot_name):
vm.vm_snapshot(vm_name, shot_name)
# -----------------------------------------------------------------------------
# Files
# -----------------------------------------------------------------------------
def get_next_file_number(dir_path, suffix=None):
# Get number of files in directory
entries = os.listdir(dir_path)
cnt = 0
for entry in entries:
if not os.path.isfile(os.path.join(dir_path, entry)):
continue
if suffix and not re.match(r'.*\.' + suffix, entry):
continue
cnt += 1
return cnt
def get_next_prefix(dir_path, suffix, digits=3):
cnt = get_next_file_number(dir_path, suffix)
return ('{:0' + str(digits) + 'd}').format(cnt)
# -----------------------------------------------------------------------------
# Networking
# -----------------------------------------------------------------------------
def create_host_networks():
if conf.do_build and not conf.leave_vms_running:
vm.stop_running_cluster_vms()
if conf.wbatch:
wbatch.wbatch_begin_hostnet()
cnt = 0
# Iterate over values (IP addresses)
for net_name, net_address in conf.networks.items():
logger.info("Creating %s network: %s.", net_name, net_address)
gw_address = hf.ip_to_gateway(net_address)
if conf.do_build:
iface = vm.create_network(net_name, gw_address)
else:
# TODO use a generator (yield) here
# If we are here only for wbatch, ignore actual network interfaces;
# just return a vboxnetX identifier (so it can be replaced with the
# interface name used by Windows).
iface = "vboxnet{}".format(cnt)
cnt += 1
if conf.wbatch:
wbatch.wbatch_create_hostnet(gw_address, iface)
if conf.wbatch:
wbatch.wbatch_end_file()

View File

@ -0,0 +1,100 @@
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import errno
import logging
import os
import re
import subprocess
import sys
import time
logger = logging.getLogger(__name__)
def strip_top_dir(root_path_to_remove, full_path):
if re.match(root_path_to_remove, full_path):
return os.path.relpath(full_path, root_path_to_remove)
else:
# TODO error handling
logger.error("Cannot strip path\n\t%s\n\tfrom\n\t%s", full_path,
root_path_to_remove)
sys.exit(1)
def create_dir(dir_path):
"""Create directory (including parents if necessary)."""
try:
os.makedirs(dir_path)
except OSError as err:
if err.errno == errno.EEXIST and os.path.isdir(dir_path):
pass
else:
raise
def clean_dir(dir_path):
"""Non-recursive removal of all files except README.*"""
if not os.path.exists(dir_path):
create_dir(dir_path)
elif not os.path.isdir(dir_path):
logging.error("This is not a directory: %s", dir_path)
# TODO error handling
raise Exception
dir_entries = os.listdir(dir_path)
for dir_entry in dir_entries:
path = os.path.join(dir_path, dir_entry)
if os.path.isfile(path):
if not re.match(r'README.', dir_entry):
os.remove(path)
# files = [ f for f in os.listdir(dir_path) if
# os.path.isfile(os.path.join(dir_path, f))]
def fmt_time_diff(start, stop=None):
stop = stop or time.time()
return "%3d" % round(stop - start)
def wait_for_ping(ip):
logger.debug("Waiting for ping returning from %s.", ip)
devnull = open(os.devnull, 'w')
while True:
try:
subprocess.call(["ping", "-c1", ip],
stdout=devnull, stderr=devnull)
break
except subprocess.CalledProcessError:
time.sleep(1)
print(".")
logger.debug("Got ping reply from %s.", ip)
def ip_to_gateway(ip):
return remove_last_octet(ip) + '1'
def ip_to_net_address(ip):
return remove_last_octet(ip) + '0'
def remove_last_octet(ip):
ma = re.match(r'(\d+\.\d+.\d+\.)\d+', ip)
if ma:
return ma.group(1)
else:
raise ValueError
def test_exe(*args):
devnull = open(os.devnull, 'w')
try:
# subprocess.call(args.split(' '), stdout=devnull, stderr=devnull)
subprocess.call(args, stdout=devnull, stderr=devnull)
except OSError:
return False
return True

View File

@ -0,0 +1,91 @@
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import importlib
import logging
import os
import sys
import stacktrain.core.helpers as hf
import stacktrain.core.download as dl
import stacktrain.config.general as conf
distro_boot = importlib.import_module("stacktrain.distros.%s" %
conf.distro_full)
logger = logging.getLogger(__name__)
conf.iso_image = distro_boot.ISOImage()
# -----------------------------------------------------------------------------
# Functions to get install ISO images
# -----------------------------------------------------------------------------
def iso_image_okay(iso_path):
if os.path.isfile(iso_path):
logger.debug("There is a file at given path:\n\t%s", iso_path)
if md5_match(iso_path, conf.iso_image.md5):
return True
else:
logger.warning("ISO image corrupt, removing:\n\t%s", iso_path)
os.remove(iso_path)
else:
logger.warning("There is no file at given path:\n\t%s", iso_path)
return False
def download_and_check(iso_path):
if iso_image_okay(iso_path):
logger.info("ISO image okay.")
return True
logger.info("Downloading\n\t%s\n\tto %s", conf.iso_image.url, iso_path)
logger.info("This may take a while.")
try:
dl.downloader.download(conf.iso_image.url, iso_path)
logger.info("Download succeeded.")
except EnvironmentError:
logger.warning("Download failed.")
return False
return iso_image_okay(iso_path)
def find_install_iso():
iso_path = os.path.join(conf.img_dir, conf.iso_image.name)
if download_and_check(iso_path):
return iso_path
logger.warn("Unable to get ISO image, trying to update URL.")
conf.iso_image.update_iso_image_variables()
iso_path = os.path.join(conf.img_dir, conf.iso_image.name)
if download_and_check(iso_path):
return iso_path
logger.error("Download failed for:\n\t%s", conf.iso_image.url)
sys.exit(1)
def md5_match(path, correct_md5):
import hashlib
with open(path, 'rb') as ff:
hasher = hashlib.md5()
while True:
buf = ff.read(2**24)
if not buf:
break
hasher.update(buf)
actual_md5 = hasher.hexdigest()
logger.debug("MD5 correct %s, actual %s", correct_md5, actual_md5)
if correct_md5 == actual_md5:
logger.debug("MD5 sum matched.")
return True
else:
logger.warn("MD5 sum did not match.")
return False

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import importlib
import stacktrain.config.general as conf
kc = importlib.import_module("stacktrain.%s.keycodes" % conf.provider)
# -----------------------------------------------------------------------------
# Virtual VM keyboard using keycodes
# -----------------------------------------------------------------------------
def keyboard_send_escape(vm_name):
kc.keyboard_push_scancode(vm_name, kc.esc2scancode())
def keyboard_send_enter(vm_name):
kc.keyboard_push_scancode(vm_name, kc.enter2scancode())
def keyboard_send_backspace(vm_name):
kc.keyboard_push_scancode(vm_name, kc.backspace2scancode())
def keyboard_send_f6(vm_name):
kc.keyboard_push_scancode(vm_name, kc.f6_2scancode())
# Turn strings into keycodes and send them to target VM
def keyboard_send_string(vm_name, string):
# This loop is inefficient enough that we don't overrun the keyboard input
# buffer when pushing scancodes to the VM.
for letter in string:
scancode = kc.char2scancode(letter)
kc.keyboard_push_scancode(vm_name, scancode)

View File

@ -0,0 +1,16 @@
import stacktrain.config.general as conf
import stacktrain.core.autostart as autostart
import stacktrain.batch_for_windows as wbatch
def build_nodes(cluster_cfg):
config_name = "{}_{}".format(conf.distro, cluster_cfg)
if conf.wbatch:
wbatch.wbatch_begin_node(config_name)
autostart.autostart_reset()
autostart.autostart_from_config("scripts." + config_name)
if conf.wbatch:
wbatch.wbatch_end_file()

View File

@ -0,0 +1,47 @@
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import logging
import sys
import time
from urlparse import urlparse, urlunparse
import stacktrain.config.general as conf
import stacktrain.core.download as dl
logger = logging.getLogger(__name__)
def print_summary():
print("Your cluster nodes:")
for vm_name, vmc in conf.vm.items():
logger.info("VM name: %s", vm_name)
if vmc.ssh_port != 22:
port_opt = " -p {}".format(vmc.ssh_port)
else:
port_opt = ""
logger.info("\tSSH login: ssh%s %s@%s", port_opt, conf.vm_shell_user,
vmc.ssh_ip)
logger.info("\t (password: %s)", "osbash")
if vm_name == "controller":
if vmc.http_port:
port_opt = ":{}".format(vmc.http_port)
else:
port_opt = ""
dashboard_url = "http://{}{}/horizon/".format(vmc.ssh_ip, port_opt)
logger.info("\tDashboard: Assuming horizon is on %s VM.",
vmc.vm_name)
logger.info("\t %s", dashboard_url)
logger.info("\t User : %s (password: %s)",
conf.demo_user, conf.demo_password)
logger.info("\t User : %s (password: %s)",
conf.admin_user, conf.admin_password)
for name, address in conf.networks.items():
logger.info("Network: %s", name)
logger.info(" Network address: %s", address)

146
labs/stacktrain/core/ssh.py Normal file
View File

@ -0,0 +1,146 @@
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import logging
import os
import time
import subprocess
import sys
import traceback
import stacktrain.config.general as conf
import stacktrain.core.helpers as hf
logger = logging.getLogger(__name__)
def get_osbash_private_key():
key_path = os.path.join(conf.lib_dir, "osbash-ssh-keys", "osbash_key")
if os.path.isfile(key_path):
mode = os.stat(key_path).st_mode & 0o777
if mode != 0o400:
logger.warning("Adjusting permissions for key file (0400):\n\t%s",
key_path)
os.chmod(key_path, 0o400)
else:
logger.error("Key file not found at:\n\t%s", key_path)
sys.exit(1)
return key_path
def vm_scp_to_vm(vm_name, *args):
"""
Copy files or directories (incl. implied directories) to a VM's osbash
home directory
"""
key_path = get_osbash_private_key()
for src_path in args:
target_path = hf.strip_top_dir(conf.top_dir, src_path)
target_dir = os.path.dirname(target_path)
if not target_dir:
target_dir = '.'
vm_ssh(vm_name, "mkdir", "-p", target_dir)
target_port = str(conf.vm[vm_name].ssh_port)
try:
full_target = "{}@{}:{}".format(conf.vm_shell_user,
conf.vm[vm_name].ssh_ip,
target_path)
logger.debug("Copying from\n\t%s\n\tto\n\t%s (port: %s)",
src_path, full_target, target_port)
# To avoid getting stuck on broken ssh connection, disable
# connection sharing (ControlPath) and use a timeout when
# connecting.
subprocess.check_output(["scp", "-q", "-r",
"-i", key_path,
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=10",
"-o", "ControlPath=none",
"-P", target_port,
src_path, full_target])
except subprocess.CalledProcessError as err:
logger.error("Copying from\n\t%s\n\tto\n\t%s",
src_path, full_target)
logger.error("\trc=%s: %s", err.returncode, err.output)
sys.exit(1)
def vm_ssh(vm_name, *args, **kwargs):
key_path = get_osbash_private_key()
live_log = kwargs.pop('log_file', None)
show_err = kwargs.pop('show_err', True)
try:
target = "{}@{}".format(conf.vm_shell_user, conf.vm[vm_name].ssh_ip)
target_port = str(conf.vm[vm_name].ssh_port)
# To avoid getting stuck on broken ssh connection, disable
# connection sharing (ControlPath) and use a timeout when connecting.
full_args = ["ssh", "-q",
"-i", key_path,
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=10",
"-o", "ControlPath=none",
"-p", target_port,
target] + list(args)
logger.debug("vm_ssh: %s", ' '.join(full_args))
ssh_log = os.path.join(conf.log_dir, "ssh.log")
with open(ssh_log, 'a') as logf:
print(' '.join(full_args), file=logf)
if live_log:
logger.debug("Writing live log for ssh call at %s.", live_log)
hf.create_dir(os.path.dirname(live_log))
# Unbuffered log file
with open(live_log, 'a', 0) as live_logf:
ret = subprocess.call(full_args,
stderr=subprocess.STDOUT,
stdout=live_logf)
if ret:
err_msg = "ssh returned status {}.".format(ret)
logger.error("%s", err_msg)
# Indicate error in status dir
open(os.path.join(conf.status_dir, "error"), 'a').close()
raise EnvironmentError
output = None
else:
try:
output = subprocess.check_output(full_args,
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
if show_err:
logger.exception("vm_ssh: Aborting.")
traceback.print_exc(file=sys.stdout)
sys.exit(1)
raise EnvironmentError
except subprocess.CalledProcessError as err:
logger.debug("ERROR ssh %s", full_args)
logger.debug("ERROR rc %s", err.returncode)
logger.debug("ERROR output %s", err.output)
raise EnvironmentError
return output
def wait_for_ssh(vm_name):
while True:
try:
vm_ssh(vm_name, "exit", show_err=False)
break
except EnvironmentError:
time.sleep(1)

View File

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import os
class GenericISOImage(object):
"""Base class for ISO images"""
def __init__(self):
self.url_base = None
self.name = None
self.md5 = None
@property
def url(self):
""""Return path to ISO image"""
return os.path.join(self.url_base, self.name)
@url.setter
def url(self, url):
"""Update url_base and name based on new URL"""
self.url_base = os.path.dirname(url)
self.name = os.path.basename(url)

View File

@ -0,0 +1,115 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import logging
import os
import re
import sys
import stacktrain.core.cond_sleep as cs
import stacktrain.core.download as dl
import stacktrain.core.keycodes as kc
import stacktrain.config.general as conf
import stacktrain.distros.distro as distro
logger = logging.getLogger(__name__)
conf.base_install_scripts = "scripts.ubuntu_base"
conf.distro = "ubuntu"
# -----------------------------------------------------------------------------
# Installation from ISO image
# -----------------------------------------------------------------------------
class ISOImage(distro.GenericISOImage):
def __init__(self):
super(ISOImage, self).__init__()
self.release_name = "ubuntu-14.04-amd64"
self.url = ("http://releases.ubuntu.com/14.04/"
"ubuntu-14.04.4-server-amd64.iso")
self.md5 = "2ac1f3e0de626e54d05065d6f549fa3a"
# Fallback function to find current ISO image in case the file in ISO_URL
# is neither on the disk nor at the configured URL. This mechanism was
# added because old Ubuntu ISOs are removed from the server as soon as a
# new ISO appears.
def update_iso_image_variables(self):
# Get matching line from distro repo's MD5SUMS file, e.g.
# "9e5fecc94b3925bededed0fdca1bd417 *ubuntu-14.04.3-server-amd64.iso"
md5_url = os.path.join(self.url_base, "MD5SUMS")
logger.debug("About to download MD5SUM from %s", md5_url)
try:
txt = dl.downloader.download(md5_url)
except EnvironmentError:
logger.error("Can't find newer ISO image. Aborting.")
sys.exit(1)
ma = re.search(r"(.*) \*{0,1}(.*server-amd64.iso)", txt)
if ma:
self.md5 = ma.group(1)
self.name = ma.group(2)
logger.info("Using new ISO image:\n\t%s\n\t%s", self.name,
self.md5)
else:
logger.error("Failed to update ISO location. Exiting.")
sys.exit(1)
logger.info("New ISO URL:\n\t%s", self.url)
PRESEED_HOST_DIR = ("http://git.openstack.org/cgit/openstack/training-labs/"
"plain/labs/osbash/lib/osbash/netboot/")
PRESEED_URL = {}
PRESEED_URL['ssh'] = PRESEED_HOST_DIR + "preseed-ssh-v4.cfg"
PRESEED_URL['shared_folder'] = PRESEED_HOST_DIR + "preseed-vbadd.cfg"
PRESEED_URL['all'] = PRESEED_HOST_DIR + "preseed-all-v2.cfg"
# Arguments for ISO image installer
_BOOT_ARGS = ("/install/vmlinuz"
" noapic"
" preseed/url=%s"
" debian-installer=en_US"
" auto=true"
" locale=en_US"
" hostname=osbash"
" fb=false"
" debconf/frontend=noninteractive"
" keyboard-configuration/modelcode=SKIP"
" initrd=/install/initrd.gz"
" console-setup/ask_detect=false")
# ostype used by VirtualBox to choose icon and flags (64-bit, IOAPIC)
conf.vbox_ostype = "Ubuntu_64"
def distro_start_installer(config):
"""Boot the ISO image operating system installer"""
preseed = PRESEED_URL[conf.vm_access]
logger.debug("Using %s", preseed)
boot_args = _BOOT_ARGS % preseed
if conf.vm_proxy:
boot_args += " mirror/http/proxy=%s http_proxy=%s" % (conf.vm_proxy,
conf.vm_proxy)
kc.keyboard_send_escape(config.vm_name)
kc.keyboard_send_escape(config.vm_name)
kc.keyboard_send_enter(config.vm_name)
cs.conditional_sleep(1)
logger.debug("Pushing boot command line: %s", boot_args)
kc.keyboard_send_string(config.vm_name, boot_args)
logger.info("Initiating boot sequence for %s.", config.vm_name)
kc.keyboard_send_enter(config.vm_name)

View File

@ -0,0 +1,115 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import logging
import os
import re
import sys
import stacktrain.core.cond_sleep as cs
import stacktrain.core.download as dl
import stacktrain.core.keycodes as kc
import stacktrain.config.general as conf
import stacktrain.distros.distro as distro
logger = logging.getLogger(__name__)
conf.base_install_scripts = "scripts.ubuntu_base"
conf.distro = "ubuntu"
# -----------------------------------------------------------------------------
# Installation from ISO image
# -----------------------------------------------------------------------------
class ISOImage(distro.GenericISOImage):
def __init__(self):
super(ISOImage, self).__init__()
self.release_name = "ubuntu-14.04-i386"
self.url = ("http://releases.ubuntu.com/14.04/"
"ubuntu-14.04.3-server-i386.iso")
self.md5 = "352009d5b44f0e97c9558919f0147c0c"
# Fallback function to find current ISO image in case the file in ISO_URL
# is neither on the disk nor at the configured URL. This mechanism was
# added because old Ubuntu ISOs are removed from the server as soon as a
# new ISO appears.
def update_iso_image_variables(self):
# Get matching line from distro repo's MD5SUMS file, e.g.
# "9e5fecc94b3925bededed0fdca1bd417 *ubuntu-14.04.3-server-amd64.iso"
md5_url = os.path.join(self.url_base, "MD5SUMS")
logger.debug("About to download MD5SUM from %s", md5_url)
try:
txt = dl.downloader.download(md5_url)
except EnvironmentError:
logger.error("Can't find newer ISO image. Aborting.")
sys.exit(1)
ma = re.search(r"(.*) \*{0,1}(.*server-i386.iso)", txt)
if ma:
self.md5 = ma.group(1)
self.name = ma.group(2)
logger.info("Using new ISO image:\n\t%s\n\t%s", self.name,
self.md5)
else:
logger.error("Failed to update ISO location. Exiting.")
sys.exit(1)
logger.info("New ISO URL:\n\t%s", self.url)
PRESEED_HOST_DIR = ("http://git.openstack.org/cgit/openstack/training-labs/"
"plain/labs/osbash/lib/osbash/netboot/")
PRESEED_URL = {}
PRESEED_URL['ssh'] = PRESEED_HOST_DIR + "preseed-ssh-v4.cfg"
PRESEED_URL['shared_folder'] = PRESEED_HOST_DIR + "preseed-vbadd.cfg"
PRESEED_URL['all'] = PRESEED_HOST_DIR + "preseed-all-v2.cfg"
# Arguments for ISO image installer
_BOOT_ARGS = ("/install/vmlinuz"
" noapic"
" preseed/url=%s"
" debian-installer=en_US"
" auto=true"
" locale=en_US"
" hostname=osbash"
" fb=false"
" debconf/frontend=noninteractive"
" keyboard-configuration/modelcode=SKIP"
" initrd=/install/initrd.gz"
" console-setup/ask_detect=false")
# ostype used by VirtualBox to choose icon and flags (64-bit, IOAPIC)
conf.vbox_ostype = "Ubuntu"
def distro_start_installer(config):
"""Boot the ISO image operating system installer"""
preseed = PRESEED_URL[conf.vm_access]
logger.debug("Using %s", preseed)
boot_args = _BOOT_ARGS % preseed
if conf.vm_proxy:
boot_args += " mirror/http/proxy=%s http_proxy=%s" % (conf.vm_proxy,
conf.vm_proxy)
kc.keyboard_send_escape(config.vm_name)
kc.keyboard_send_escape(config.vm_name)
kc.keyboard_send_enter(config.vm_name)
cs.conditional_sleep(1)
logger.debug("Pushing boot command line: %s" , boot_args)
kc.keyboard_send_string(config.vm_name, boot_args)
logger.info("Initiating boot sequence for %s.", config.vm_name)
kc.keyboard_send_enter(config.vm_name)

View File

@ -0,0 +1,134 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import logging
import os
import re
import sys
import stacktrain.core.cond_sleep as cs
import stacktrain.core.download as dl
import stacktrain.core.keycodes as kc
import stacktrain.config.general as conf
import stacktrain.distros.distro as distro
logger = logging.getLogger(__name__)
conf.base_install_scripts = "scripts.ubuntu_base"
conf.distro = "ubuntu"
# -----------------------------------------------------------------------------
# Installation from ISO image
# -----------------------------------------------------------------------------
class ISOImage(distro.GenericISOImage):
def __init__(self, arch="amd64"):
super(ISOImage, self).__init__()
self.arch = arch
if arch == "amd64":
self.release_name = "ubuntu-16.04-amd64"
self.url = ("http://releases.ubuntu.com/16.04/"
"ubuntu-16.04.1-server-amd64.iso")
self.md5 = "d2d939ca0e65816790375f6826e4032f"
# ostype used by VirtualBox to choose icon and flags (64-bit,
# IOAPIC)
conf.vbox_ostype = "Ubuntu_64"
elif arch == "i386":
self.url = ("http://releases.ubuntu.com/16.04/"
"ubuntu-16.04.1-server-i386.iso")
self.release_name = "ubuntu-16.04-i386"
self.md5 = "352009d5b44f0e97c9558919f0147c0c"
conf.vbox_ostype = "Ubuntu"
else:
logger.error("Unknown arch: %s. Aborting.", arch)
sys.exit(1)
# Fallback function to find current ISO image in case the file in ISO_URL
# is neither on the disk nor at the configured URL. This mechanism was
# added because old Ubuntu ISOs are removed from the server as soon as a
# new ISO appears.
def update_iso_image_variables(self):
# Get matching line from distro repo's MD5SUMS file, e.g.
# "9e5fecc94b3925bededed0fdca1bd417 *ubuntu-14.04.3-server-amd64.iso"
md5_url = os.path.join(self.url_base, "MD5SUMS")
logger.debug("About to download MD5SUM from %s", md5_url)
try:
txt = dl.downloader.download(md5_url)
except EnvironmentError:
logger.error("Can't find newer ISO image. Aborting.")
sys.exit(1)
if self.arch == "amd64":
ma = re.search(r"(.*) \*{0,1}(.*server-amd64.iso)", txt)
else:
ma = re.search(r"(.*) \*{0,1}(.*server-i386.iso)", txt)
if ma:
self.md5 = ma.group(1)
self.name = ma.group(2)
logger.info("Using new ISO image:\n\t%s\n\t%s", self.name,
self.md5)
else:
logger.error("Failed to update ISO location. Exiting.")
sys.exit(1)
logger.info("New ISO URL:\n\t%s", self.url)
PRESEED_HOST_DIR = ("http://git.openstack.org/cgit/openstack/training-labs/"
"plain/labs/osbash/lib/osbash/netboot/")
PRESEED_URL = {}
PRESEED_URL['ssh'] = PRESEED_HOST_DIR + "preseed-ssh-v4.cfg"
PRESEED_URL['shared_folder'] = PRESEED_HOST_DIR + "preseed-vbadd-v3.cfg"
PRESEED_URL['all'] = PRESEED_HOST_DIR + "preseed-all-v2.cfg"
# Arguments for ISO image installer
_BOOT_ARGS = ("/install/vmlinuz"
" noapic"
" preseed/url=%s"
" debian-installer=en_US"
" auto=true"
" locale=en_US"
" hostname=osbash"
" fb=false"
" debconf/frontend=noninteractive"
" keyboard-configuration/modelcode=SKIP"
" initrd=/install/initrd.gz"
" console-setup/ask_detect=false")
def distro_start_installer(config):
"""Boot the ISO image operating system installer"""
preseed = PRESEED_URL[conf.vm_access]
logger.debug("Using %s", preseed)
boot_args = _BOOT_ARGS % preseed
if conf.vm_proxy:
boot_args += " mirror/http/proxy=%s http_proxy=%s" % (conf.vm_proxy,
conf.vm_proxy)
logger.debug("Choosing installer expert mode.")
kc.keyboard_send_enter(config.vm_name)
kc.keyboard_send_f6(config.vm_name)
kc.keyboard_send_escape(config.vm_name)
logger.debug("Clearing default boot arguments.")
for _ in range(83):
kc.keyboard_send_backspace(config.vm_name)
logger.debug("Pushing boot command line: %s" , boot_args)
kc.keyboard_send_string(config.vm_name, boot_args)
logger.info("Initiating boot sequence for %s.", config.vm_name)
kc.keyboard_send_enter(config.vm_name)

View File

View File

@ -0,0 +1,132 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import importlib
import logging
import os
import time
import stacktrain.config.general as conf
import stacktrain.kvm.vm_create as vm
import stacktrain.core.iso_image as iso_image
import stacktrain.core.autostart as autostart
import stacktrain.core.cond_sleep as cs
import stacktrain.core.helpers as hf
distro_boot = importlib.import_module("stacktrain.distros.%s" %
conf.distro_full)
logger = logging.getLogger(__name__)
def base_disk_exists():
return vm.disk_exists(conf.get_base_disk_name())
def vm_install_base():
vm_name = "base"
conf.vm[vm_name] = conf.VMconfig(vm_name)
base_disk_name = conf.get_base_disk_name()
vm.vm_delete(vm_name)
vm.disk_delete(base_disk_name)
vm_config = conf.vm[vm_name]
if conf.do_build:
install_iso = iso_image.find_install_iso()
else:
install_iso = os.path.join(conf.img_dir, conf.iso_image.name)
logger.info("Install ISO:\n\t%s", install_iso)
autostart.autostart_reset()
autostart.autostart_queue("osbash/base_fixups.sh")
autostart.autostart_from_config(conf.base_install_scripts)
autostart.autostart_queue("zero_empty.sh", "shutdown.sh")
base_disk_size = 10000
vm.disk_create(base_disk_name, base_disk_size)
libvirt_connect_uri = "qemu:///system"
virt_install_call = ["sudo", "virt-install",
"--connect={}".format(libvirt_connect_uri)]
vm_base_mem = 512
call_args = virt_install_call
call_args.extend(["--name", vm_name])
call_args.extend(["--ram", str(vm_base_mem)])
call_args.extend(["--vcpus", str(1)])
call_args.extend(["--os-type", "linux"])
call_args.extend(["--cdrom", install_iso])
call_args.extend(["--disk", "vol={}/{},cache=none".format(vm.kvm_vol_pool,
base_disk_name)])
if conf.vm_ui == "headless":
call_args.extend(("--graphics", "none", "--noautoconsole"))
elif conf.vm_ui == "vnc":
call_args.extend(("--graphics", "vnc,listen=127.0.0.1"))
# Default (no extra argument) is gui option: should open a console viewer
call_args.append("--wait=-1")
import subprocess
errout = subprocess.STDOUT
logger.debug("virt-install call: %s", ' '.join(call_args))
logger.debug("virt-install call: %s", call_args)
vm.virsh_log(call_args)
subprocess.Popen(call_args, stderr=errout)
while True:
if vm.vm_is_running(vm_name):
break
print('.', end='')
time.sleep(1)
delay = 5
logger.info("\nWaiting %d seconds for VM %s to come up.", delay, vm_name)
cs.conditional_sleep(delay)
logger.info("Booting into distribution installer.")
distro_boot.distro_start_installer(vm_config)
# Prevent "time stamp from the future" due to race between two sudos for
# virt-install (background) above and virsh below
time.sleep(1)
logger.info("Waiting for VM %s to be defined.", vm_name)
while True:
if vm.vm_is_running(vm_name):
break
time.sleep(1)
print(".")
ssh_ip = vm.node_to_ip(vm_name)
conf.vm[vm_name].ssh_ip = ssh_ip
logger.info("Waiting for ping returning from %s.", ssh_ip)
hf.wait_for_ping(ssh_ip)
autostart.autostart_and_wait(vm_name)
vm.vm_wait_for_shutdown(vm_name)
logger.info("Compacting %s.", base_disk_name)
vm.disk_compress(base_disk_name)
vm.virsh("undefine", vm_name)
del conf.vm[vm_name]
logger.info("Base disk created.")
logger.info("stacktrain base disk build ends.")

View File

@ -0,0 +1,107 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import logging
import sys
import stacktrain.config.general as conf
import stacktrain.kvm.vm_create as vm
import stacktrain.core.functions_host as host
logger = logging.getLogger(__name__)
# TODO could vm_create_code become generic enough for base_disk install?
def vm_create_node(vm_name):
try:
vm_config = conf.vm[vm_name]
except Exception:
logger.exception("Failed to import VM configuration config.vm_%s.",
vm_name)
raise
base_disk_name = conf.get_base_disk_name()
vm.vm_delete(vm_name)
libvirt_connect_uri = "qemu:///system"
virt_install_call = ["sudo", "virt-install",
"--connect={}".format(libvirt_connect_uri)]
call_args = virt_install_call + ["--name", vm_name,
"--ram", str(vm_config.vm_mem),
"--vcpus", str(vm_config.vm_cpus),
"--os-type={}".format("linux"),
"--import"]
for index, iface in enumerate(conf.vm[vm_name].net_ifs):
if iface["typ"] == "dhcp":
call_args.extend(["--network", "bridge=virbr0"])
elif iface["typ"] == "manual":
net_name = "labs-{}".format(vm.ip_to_netname(iface["ip"]))
call_args.extend(["--network", "network={}".format(net_name)])
elif iface["typ"] == "static":
net_name = "labs-{}".format(vm.ip_to_netname(iface["ip"]))
call_args.extend(["--network", "network={}".format(net_name)])
else:
logger.error("Unknown interface type: %s", iface.typ)
sys.exit(1)
for index, disk in enumerate(conf.vm[vm_name].disks):
# Turn number into letter (0->a, 1->b, etc.)
disk_letter = chr(index + ord('a'))
if disk is None:
continue
disk_name = "{}-sd{}".format(vm_name, disk_letter)
if disk == "base":
logger.info("Creating copy-on-write VM disk.")
vm.disk_create_cow(disk_name, base_disk_name)
else:
size = disk
logger.info("Adding empty disk to %s: %s", vm_name, disk_name)
vm.disk_create(disk_name, size)
call_args.extend(["--disk",
"vol={}/{},cache=none".format(vm.kvm_vol_pool,
disk_name)])
if conf.vm_ui == "headless":
call_args.extend(("--graphics", "none", "--noautoconsole"))
call_args.append("--noreboot")
elif conf.vm_ui == "vnc":
# Only local connections allowed (0.0.0.0 would allow external
# connections as well)
call_args.extend(("--graphics", "vnc,listen=127.0.0.1"))
call_args.append("--noreboot")
# Default UI uses virt-viewer which doesn't fly with --noreboot
import subprocess
errout = subprocess.STDOUT
logger.debug("virt-install call: %s", call_args)
vm.virsh_log(call_args)
subprocess.Popen(call_args, stderr=errout)
# Prevent "time stamp from the future" due to race between two sudos for
# virt-install (background) above and virsh below
import time
time.sleep(1)
logger.info("Waiting for VM %s to be defined.", vm_name)
while True:
if vm.virsh_vm_defined(vm_name):
logger.debug("VM %s is defined now.", vm_name)
vm.log_xml_dump(vm_name, "defined")
break
time.sleep(1)
print(".", end='')
sys.stdout.flush()
# Set VM group in description so we know which VMs are ours
# (not with virt-install because older versions give an error for
# --metadata title=TITLE)
vm.set_vm_group(vm_name)
vm.log_xml_dump(vm_name, "in_group")

View File

@ -0,0 +1,134 @@
#!/usr/bin/env python
# The functions in this library are used to get scancode strings for virsh
# keyboard input (send-key).
#
# It is based on:
# http://libvirt.org/git/?p=libvirt.git;a=blob_plain;f=src/util/keymaps.csv
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import stacktrain.kvm.vm_create as vm
def char2scancode(key):
keycodes = {
'a': "KEY_A",
'b': "KEY_B",
'c': "KEY_C",
'd': "KEY_D",
'e': "KEY_E",
'f': "KEY_F",
'g': "KEY_G",
'h': "KEY_H",
'i': "KEY_I",
'j': "KEY_J",
'k': "KEY_K",
'l': "KEY_L",
'm': "KEY_M",
'n': "KEY_N",
'o': "KEY_O",
'p': "KEY_P",
'q': "KEY_Q",
'r': "KEY_R",
's': "KEY_S",
't': "KEY_T",
'u': "KEY_U",
'v': "KEY_V",
'w': "KEY_W",
'x': "KEY_X",
'y': "KEY_Y",
'z': "KEY_Z",
'A': "KEY_SHIFT KEY_A",
'B': "KEY_SHIFT KEY_B",
'C': "KEY_SHIFT KEY_C",
'D': "KEY_SHIFT KEY_D",
'E': "KEY_SHIFT KEY_E",
'F': "KEY_SHIFT KEY_F",
'G': "KEY_SHIFT KEY_G",
'H': "KEY_SHIFT KEY_H",
'I': "KEY_SHIFT KEY_I",
'J': "KEY_SHIFT KEY_J",
'K': "KEY_SHIFT KEY_K",
'L': "KEY_SHIFT KEY_L",
'M': "KEY_SHIFT KEY_M",
'N': "KEY_SHIFT KEY_N",
'O': "KEY_SHIFT KEY_O",
'P': "KEY_SHIFT KEY_P",
'Q': "KEY_SHIFT KEY_Q",
'R': "KEY_SHIFT KEY_R",
'S': "KEY_SHIFT KEY_S",
'T': "KEY_SHIFT KEY_T",
'U': "KEY_SHIFT KEY_U",
'V': "KEY_SHIFT KEY_V",
'W': "KEY_SHIFT KEY_W",
'X': "KEY_SHIFT KEY_X",
'Y': "KEY_SHIFT KEY_Y",
'Z': "KEY_SHIFT KEY_Z",
'1': "KEY_1",
'2': "KEY_2",
'3': "KEY_3",
'4': "KEY_4",
'5': "KEY_5",
'6': "KEY_6",
'7': "KEY_7",
'8': "KEY_8",
'9': "KEY_9",
'0': "KEY_0",
'!': "KEY_SHIFT KEY_1",
'@': "KEY_SHIFT KEY_2",
'#': "KEY_SHIFT KEY_3",
'$': "KEY_SHIFT KEY_4",
'%': "KEY_SHIFT KEY_5",
'^': "KEY_SHIFT KEY_6",
'&': "KEY_SHIFT KEY_7",
'*': "KEY_SHIFT KEY_8",
'(': "KEY_SHIFT KEY_9",
')': "KEY_SHIFT KEY_0",
'-': "KEY_MINUS",
'_': "KEY_SHIFT KEY_MINUS",
'=': "KEY_EQUAL",
'+': "KEY_SHIFT KEY_EQUAL",
' ': "KEY_SPACE",
'[': "KEY_LEFTBRACE",
']': "KEY_RIGHTBRACE",
'{': "KEY_SHIFT KEY_LEFTBRACE",
'}': "KEY_SHIFT KEY_RIGHTBRACE",
';': "KEY_SEMICOLON",
':': "KEY_SHIFT KEY_SEMICOLON",
',': "KEY_COMMA",
'.': "KEY_DOT",
'/': "KEY_SLASH",
'\\': "KEY_BACKSLASH",
'|': "KEY_SHIFT KEY_BACKSLASH",
'?': "KEY_SHIFT KEY_SLASH",
'"': "KEY_SHIFT KEY_APOSTROPHE",
"'": "KEY_APOSTROPHE",
">": "KEY_SHIFT KEY_DOT",
"<": "KEY_SHIFT KEY_COMMA"
}
return keycodes[key]
def esc2scancode():
return "KEY_ESC"
def enter2scancode():
return "KEY_ENTER"
def backspace2scancode():
return "KEY_BACKSPACE"
def f6_2scancode():
return "KEY_F6"
def keyboard_push_scancode(vm_name, code_string):
code = code_string.split(' ')
vm.virsh("send-key", vm_name, "--codeset", "linux", *code)

View File

@ -0,0 +1,654 @@
#!/usr/bin/env python
# TODO rename vm_create
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import logging
import os
import re
import subprocess
import sys
import time
import stacktrain.config.general as conf
import stacktrain.core.cond_sleep as cs
import stacktrain.core.helpers as hf
logger = logging.getLogger(__name__)
kvm_vol_pool = "default"
vm_group = "OpenStack training-labs"
def init():
output = virsh("--version")
logger.debug("Virsh version: %s", output)
try:
output = virsh("list", show_err=False)
except EnvironmentError:
logger.error("Failed to connect to libvirt/KVM. Is service running?"
" Aborting.")
sys.exit(1)
def virsh_log(call_args, err_code=None):
log_file = os.path.join(conf.log_dir, "virsh.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)
return
def virsh(*args, **kwargs):
show_err = kwargs.pop('show_err', True)
virsh_exe = "virsh"
libvirt_connect_uri = "qemu:///system"
virsh_call = ["sudo", virsh_exe,
"--connect={}".format(libvirt_connect_uri)]
call_args = virsh_call + list(args)
virsh_log(call_args)
try:
output = subprocess.check_output(call_args, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as err:
virsh_log(call_args, err_code=err.returncode)
if show_err:
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("virsh: Aborting.")
logger.warn("-----------------------------------------------")
sys.exit(1)
else:
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):
try:
virsh("domstate", vm_name, show_err=False)
except EnvironmentError:
return False
return True
def vm_is_running(vm_name):
try:
output = virsh("domstate", vm_name, show_err=False)
except EnvironmentError:
# VM probably does not exist
return False
return True if output and re.search(r'running', output) else False
def vm_is_shut_down(vm_name):
# cond = re.compile(r'(running|in shutdown)')
cond = re.compile(r'(shut off)')
output = virsh("domstate", vm_name)
return True if cond.search(output) else False
def vm_wait_for_shutdown(vm_name, timeout=None):
logger.info("Waiting for shutdown of VM %s.", vm_name)
cnt = 0
while True:
if vm_is_shut_down(vm_name):
logger.debug("Machine powered off.")
break
if timeout and cnt > timeout:
logger.debug("Timeout reached, giving up.")
break
print('W' if conf.verbose_console else '.', end='')
sys.stdout.flush()
time.sleep(1)
cnt += 1
def vm_power_off(vm_name):
# TODO check: may need to check for "shut off" instead of "running"
if vm_is_running(vm_name):
logger.debug("Powering off VM %s", vm_name)
virsh("destroy", vm_name)
else:
logger.debug("vm_power_off: VM %s not running", vm_name)
def vm_acpi_shutdown(vm_name):
if vm_is_running(vm_name):
logger.info("Shutting down VM %s.", vm_name)
virsh("shutdown", vm_name)
else:
logger.debug("vm_acpi_shutdown: VM %s not running", vm_name)
def set_vm_group(vm_name):
logger.debug("Setting VM group (title, description) for %s.", vm_name)
# We are not changing a running VM here; to do that, add "--live" (which
# produces an error if the VM is not running)
virsh("desc", vm_name, "--config", "--title",
"--new-desc", "{}: {}".format(vm_name, vm_group))
long_desc = "All VMs with '{}' in their description title get shut down" \
"when a new cluster build starts."
virsh("desc", vm_name, "--config",
"--new-desc", long_desc.format(vm_group))
def get_vm_group(vm_name):
return virsh("desc", vm_name, "--title")
# TODO move this to functions_host, call get_running_vms_list, shutdown,
# poweroff, wait_for_shutdown
def stop_running_cluster_vms():
output = virsh("list", "--uuid")
if output == "\n":
return
for vm_id in output.splitlines():
if vm_id == "":
continue
logger.debug("Candidate for shutdown vm_id:%s:", vm_id)
if re.match(".*{}".format(vm_group), get_vm_group(vm_id)):
logger.info("Shutting down VM %s.", vm_id)
vm_acpi_shutdown(vm_id)
vm_wait_for_shutdown(vm_id, timeout=5)
if not vm_is_shut_down(vm_id):
logger.info("VM will not shut down, powering it off.")
vm_power_off(vm_id)
# -----------------------------------------------------------------------------
# Network functions
# -----------------------------------------------------------------------------
def log_xml_dump(vm_name, desc, xml=None):
if not xml:
# No XML dump provided, so get it now
xml = virsh("dumpxml", vm_name)
fpath = os.path.join(conf.log_dir, "vm_{}_{}.xml".format(vm_name, desc))
with open(fpath, 'w') as xf:
xf.write(xml)
# Get the MAC address from a node name (default network)
def node_to_mac(node):
logger.info("Waiting for MAC address.")
while True:
dump = virsh("dumpxml", node)
ma = re.search(r'([a-z0-9:]{17})', dump)
if ma:
# FIXME what if there are two matching lines?
mac = ma.group(1)
break
time.sleep(1)
print('M' if conf.verbose_console else '.', end='')
sys.stdout.flush()
return mac
# Get the IP address from a MAC address (default network)
def mac_to_ip(mac):
logger.info("Waiting for IP address.")
while True:
lines = subprocess.check_output(["sudo", "arp", "-n"])
ma = re.search(r"(\S+).*{}".format(mac), lines)
if ma:
ip = ma.group(1)
logger.debug("mac_to_ip: %s -> %s", mac, ip)
return ip
time.sleep(1)
print('I' if conf.verbose_console else '.', end='')
sys.stdout.flush()
def node_to_ip(vm_name):
# TODO refactor node_to_ip()
# Store vm_name, IP address, and MAC address in text file for later use
# by shell script tools (e.g., tools/test-once.sh)
node_ip_db = os.path.join(conf.log_dir, "node_ip.db")
logger.debug("node_to_ip %s.", vm_name)
if conf.vm[vm_name].pxe_tmp_ip:
ip = conf.vm[vm_name].pxe_tmp_ip
logger.debug("Using IP address %s for PXE booting.", ip)
# Return IP address, but don't cache in conf or file
return ip
elif conf.vm[vm_name].ssh_ip:
# Return IP address cached in conf
return conf.vm[vm_name].ssh_ip
mac = node_to_mac(vm_name)
logger.debug("MAC address for %s: %s", vm_name, mac)
if os.path.exists(node_ip_db):
with open(node_ip_db, 'r') as dbfile:
for line in dbfile:
ma = re.match(r"{} ([0-9\.]+) ".format(mac), line)
if ma:
ip = ma.group(1)
logger.debug("IP address for %s: %s (cached)", vm_name,
ip)
#conf.vm[vm_name].ssh_ip = ip
return ip
ip = mac_to_ip(mac)
logger.debug("IP address for %s: %s", vm_name, ip)
# Update cache file
with open(node_ip_db, 'a') as out:
out.write("{} {} {}\n".format(mac, ip, vm_name))
conf.vm[vm_name].ssh_ip = ip
# Return IP address to caller
return ip
def log_iptables(tables, desc=""):
if not hasattr(log_iptables, "cnt"):
log_iptables.cnt = 0
log_name = "kvm-iptables-save_{}_{}".format(log_iptables.cnt, desc)
with open(os.path.join(conf.log_dir, log_name), 'w') as logf:
logf.write(tables)
log_iptables.cnt += 1
def get_iptables(desc=""):
errout = subprocess.STDOUT
output = subprocess.check_output(("sudo", "iptables-save"), stderr=errout)
log_iptables(output, desc=desc)
return output
def set_iptables(tables):
errout = subprocess.STDOUT
log_iptables(tables, desc="done")
p1 = subprocess.Popen(["sudo", "iptables-restore"],
stdin=subprocess.PIPE, stderr=errout)
p1.communicate(input=tables)
def virsh_destroy_network(net_name):
if virsh_network_defined(net_name) and virsh_network_active(net_name):
network = "labs-{}".format(net_name)
virsh("net-destroy", network)
def virsh_stop_network(net_name):
# Undo our changes to iptables before letting libvirt deal with it
iptables_forward_new_connections(False)
logger.debug("Stopping network %s.", net_name)
virsh_destroy_network(net_name)
def iptables_forward_new_connections(forward_new_conns):
# Save, update, and restore iptables configuration made by libvirt
replace = [" RELATED", " NEW,RELATED"]
if forward_new_conns:
fnc_desc = "add_forwarding"
else:
fnc_desc = "remove_forwarding"
replace[0], replace[1] = replace[1], replace[0]
logger.debug("Replacing %s with %s.", replace[0], replace[1])
output = get_iptables(desc=fnc_desc)
changed = ""
# Keep "\n", we will need them
for line in output.splitlines(True):
if re.search("FORWARD.*virbr[^0]", line):
changed += re.sub(replace[0], replace[1], line)
else:
changed += line
set_iptables(changed)
def virsh_start_network(net_name):
network = "labs-{}".format(net_name)
if virsh_network_active(net_name):
return
logger.debug("Starting network %s.", net_name)
network = "labs-{}".format(net_name)
virsh("net-start", network)
# Forward new connections, too (except on virbr0); this allows our
# NAT networks to talk to each other
iptables_forward_new_connections(True)
def virsh_undefine_network(net_name):
net = "labs-{}".format(net_name)
if virsh_network_defined(net_name):
logger.debug("Undefining network %s.", net_name)
virsh("net-undefine", net)
def virsh_vm_defined(vm_name):
try:
virsh("domstate", vm_name, show_err=False)
except EnvironmentError:
return False
return True
def virsh_network_defined(net_name):
net = "labs-{}".format(net_name)
try:
virsh("net-info", net, show_err=False)
except EnvironmentError:
# logger.error("virsh_network_defined %s", type(err))
# logger.exception("Exception")
return False
return True
def virsh_network_active(net_name):
network = "labs-{}".format(net_name)
# Returns exception if network does not exist
output = virsh("net-info", network, show_err=False)
ma = re.search("Active:.*yes", output)
return True if ma else False
def virsh_define_network(net_name, ip_address):
network = "labs-{}".format(net_name)
if not virsh_network_defined(net_name):
logger.debug("Defining network %s (%s)", network, ip_address)
xml_content = ("<network>\n"
" <name>%s</name>\n"
" <forward mode='nat'/>\n"
" <ip address='%s' netmask='255.255.255.0'>\n"
" </ip>\n"
"</network>\n")
xml_content = xml_content % (network, ip_address)
xml_file = os.path.join(conf.log_dir, "kvm-net-{}.xml".format(network))
with open(xml_file, 'w') as xf:
xf.write(xml_content)
virsh("net-define", xml_file)
def create_network(net_name, ip_address):
logger.debug("Creating network %s (%s)", net_name, ip_address)
virsh_stop_network(net_name)
virsh_undefine_network(net_name)
virsh_define_network(net_name, ip_address)
virsh_start_network(net_name)
# -----------------------------------------------------------------------------
# VM create and configure
# -----------------------------------------------------------------------------
def ip_to_netname(ip):
ip_net = hf.ip_to_net_address(ip)
for net_name, net_address in conf.networks.items():
if net_address == ip_net:
logger.debug("ip_to_netname %s -> %s", ip, net_name)
return net_name
logger.error("ip_to_netname: no netname found for %s.", ip)
raise ValueError
# virt-xml(1) looks like it was made for this, but version 1.4.0 has issues:
# changing the boot device from hd to network works, but completely removing it
# did not. To set the order at the device level, we could then do:
# virt-xml controller --edit 2 --disk boot_order=1
# virt-xml controller --edit 1 --network boot_order=2
# But for now, we have to edit the XML file here.
# TODO make vm_boot_order_pxe more configurable
def vm_boot_order_pxe(vm_name):
output = virsh("dumpxml", vm_name)
logger.debug("vm_boot_order_pxe: Editing XML dump.")
changed = ""
# TODO boot_network should not be defined here
boot_network = "labs-mgmt"
# Track if every replace pattern matches exactly once
sanity_check = 0
# Keep "\n", we will need them
for line in output.splitlines(True):
if re.search(r"<boot dev='hd'/>", line):
# Example: <boot dev='hd'/>
logger.debug("Found boot order line, dropping it.")
sanity_check += 1
elif re.search(r" <source network=['\"]{}['\"]/>".format(boot_network),
line):
# <source network='labs-mgmt'/>
logger.debug("Found network interface, adding boot order.")
changed += line
changed += " <boot order='2'/>\n"
sanity_check += 10
elif re.search(r"<target dev=['\"]hda['\"].*/>", line):
# Example: <target dev='hda' bus='ide'/>
logger.debug("Found hd interface, adding boot order.")
changed += line
changed += " <boot order='1'/>\n"
sanity_check += 100
else:
changed += line
if sanity_check != 111:
logger.error("vm_boot_order_pxe failed (%s). Aborting.", sanity_check)
logger.debug("vm_boot_order_pxe original XML file:\n%s.", output)
sys.exit(1)
# Create file we use to redefine the VM to use PXE booting
xml_file = os.path.join(conf.log_dir,
"vm_{}_inject_pxe.xml".format(vm_name))
with open(xml_file, 'w') as xf:
xf.write(changed)
virsh("define", xml_file)
# -----------------------------------------------------------------------------
# VM unregister, remove, 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)
# virt-install restarts VM after poweroff, we may need to power off
# twice
cs.conditional_sleep(1)
vm_power_off(vm_name)
# Take a break before undefining the VM
cs.conditional_sleep(1)
virsh("undefine", "--snapshots-metadata", "--remove-all-storage",
vm_name)
else:
logger.info("\tnot found")
# -----------------------------------------------------------------------------
# Disk functions
# -----------------------------------------------------------------------------
def disk_exists(disk):
try:
virsh("vol-info", "--pool", kvm_vol_pool, disk, show_err=False)
except EnvironmentError:
return False
return True
def disk_create_cow(disk, base_disk):
# size in MB
if not disk_exists(disk):
virsh("vol-create-as", kvm_vol_pool, disk,
"{}M".format(conf.base_disk_size),
"--format", "qcow2",
"--backing-vol", base_disk,
"--backing-vol-format", "qcow2")
def disk_create(disk, size):
# size in MB
if not disk_exists(disk):
virsh("vol-create-as", kvm_vol_pool, disk, "{}M".format(size),
"--format", "qcow2")
def disk_delete(disk):
if disk_exists(disk):
logger.debug("Deleting disk %s.", disk)
virsh("vol-delete", "--pool", kvm_vol_pool, disk)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Attaching and detaching disks from VMs
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def disk_compress(disk_name):
spexe = "virt-sparsify"
if not hf.test_exe(spexe):
logger.warn("No virt-sparsify executable found.")
logger.warn("Consider installing libguestfs-tools.")
return
disk_path = get_disk_path(disk_name)
pool_dir = os.path.dirname(disk_path)
logger.info("Compressing disk image, input file:\n\t%s", disk_path)
stat = os.stat(disk_path)
mode = stat.st_mode
logger.debug("mode\t%s", oct(mode))
uid = stat.st_uid
logger.debug("uid\t%s", uid)
gid = stat.st_gid
logger.debug("gid\t%s", gid)
size = stat.st_size
logger.debug("size\t%s", size)
tmp_file = os.path.join(pool_dir, ".{}".format(disk_name))
subprocess.call(["sudo", spexe, "--compress", disk_path, tmp_file])
logger.info("Restoring owner.")
# No root wrapper, so use sudo with shell commands
subprocess.call(["sudo", "chown", "-v", "--reference={}".format(disk_path),
tmp_file])
logger.info("Restoring permissions.")
subprocess.call(["sudo", "chmod", "-v", "--reference={}".format(disk_path),
tmp_file])
logger.info("Moving temporary file into final location.")
subprocess.call(["sudo", "mv", "-v", "-f", tmp_file, disk_path])
# os.chown(tmp_file, uid, gid)
# os.chmod(tmp_file, mode)
# import shutil
# shutil.move(tmp_file, disk_path)
stat = os.stat(disk_path)
mode = stat.st_mode
logger.debug("mode\t%s", oct(mode))
uid = stat.st_uid
logger.debug("uid\t%s", uid)
gid = stat.st_gid
logger.debug("gid\t%s", gid)
new_size = stat.st_size
logger.debug("size\t%s", new_size)
compression = "%0.0f" % round((1-new_size/size)*100) + "%"
# logger.info("size\t%s (compressed by %s%)", new_size, compression)
logger.info("size\t%s (compressed by %s)", new_size, compression)
def get_disk_path(disk_name):
# Result comes with trailing newlines
return virsh("vol-path", "--pool", kvm_vol_pool, disk_name).rstrip()
# -----------------------------------------------------------------------------
# Snapshots
# -----------------------------------------------------------------------------
def vm_snapshot_list(vm_name):
if vm_exists(vm_name):
# try:
output = virsh("snapshot-list", vm_name, show_err=False)
# except EnvironmentError:
# # No snapshots
# # output = None
return output
def vm_snapshot_exists(vm_name, shot_name):
snap_list = vm_snapshot_list(vm_name)
if snap_list:
return re.search('^ {}'.format(shot_name), snap_list)
else:
return False
def vm_snapshot(vm_name, shot_name):
virsh("snapshot-create-as", vm_name, shot_name,
"{}: {}".format(vm_name, shot_name))
# -----------------------------------------------------------------------------
# Booting a VM
# -----------------------------------------------------------------------------
def vm_boot(vm_name):
if conf.vm[vm_name].pxe_tmp_ip:
logger.warn("Patching XML dump to enable PXE booting with KVM on %s.",
vm_name)
logger.error("Configuring PXE booting.")
vm_boot_order_pxe(vm_name)
# Log the current configuration of the VM
log_xml_dump(vm_name, "pxe_enabled")
logger.info("Starting VM %s", vm_name)
virsh("start", vm_name)
logger.info("Waiting for VM %s to run.", vm_name)
while not vm_is_running(vm_name):
time.sleep(1)
print('R' if conf.verbose_console else '.', end='')
sys.stdout.flush()
# Our caller assumes that conf.vm[vm_name].ssh_ip is set
node_to_ip(vm_name)

19
labs/stacktrain/setup.py Normal file
View File

@ -0,0 +1,19 @@
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
config = {
'description': 'My Project',
'author': 'Roger Luethi',
'url': 'URL to get it at.',
'download_url': 'Where to download it.',
'author_email': 'rl@patchworkscience.org',
'version': '0.1',
'install_requires': ['pytest'],
'packages': ['NAME'],
'scripts': [],
'name': 'projectname'
}
setup(**config)

View File

View File

View File

@ -0,0 +1,181 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import importlib
import os
import errno
import logging
import stacktrain.config.general as conf
import stacktrain.config.virtualbox as cvb
import stacktrain.virtualbox.vm_create as vm
import stacktrain.core.iso_image as iso_image
import stacktrain.batch_for_windows as wbatch
import stacktrain.core.autostart as autostart
import stacktrain.core.cond_sleep as cs
distro_boot = importlib.import_module("stacktrain.distros.%s" %
conf.distro_full)
logger = logging.getLogger(__name__)
def base_disk_exists():
return os.path.isfile(cvb.get_base_disk_path())
def disk_delete_child_vms(disk):
if not vm.disk_registered(disk):
logger.warn("Disk not registered with VirtualBox:\n\t%s", disk)
return 0
while True:
child_disk_uuid = vm.get_next_child_disk_uuid(disk)
if not child_disk_uuid:
break
child_disk_path = vm.disk_to_path(child_disk_uuid)
vm_name = vm.disk_to_vm(child_disk_uuid)
if vm_name:
logger.info("Deleting VM %s.", vm_name)
vm.vm_delete(vm_name)
else:
logger.info("Unregistering and deleting:\n\t%s", child_disk_path)
vm.disk_unregister(child_disk_uuid)
os.remove(child_disk_path)
def base_disk_delete():
base_disk_path = cvb.get_base_disk_path()
if vm.disk_registered(base_disk_path):
# Remove users of base disk
logger.info("Unregistering and removing all disks attached to"
" base disk path.")
disk_delete_child_vms(base_disk_path)
logger.info("Unregistering old base disk.")
vm.disk_unregister(base_disk_path)
logger.info("Removing old base disk.")
try:
os.remove(base_disk_path)
except OSError as err:
if err.errno != errno.ENOENT:
raise
# File doesn't exist, that's fine.
def vm_install_base():
vm_name = "base"
conf.vm[vm_name] = conf.VMconfig(vm_name)
base_disk_path = cvb.get_base_disk_path()
base_build_disk = os.path.join(conf.img_dir, "tmp-disk.vdi")
logger.info("Creating\n\t%s.", base_disk_path)
if conf.wbatch:
wbatch.wbatch_begin_base()
wbatch.wbatch_delete_disk(base_build_disk)
if conf.do_build:
if base_disk_exists():
logger.info("Deleting existing basedisk.")
base_disk_delete()
try:
os.remove(base_build_disk)
except OSError as err:
if err.errno != errno.ENOENT:
raise
# File doesn't exist, that's fine.
vm_config = conf.vm[vm_name]
if conf.do_build:
install_iso = iso_image.find_install_iso()
else:
install_iso = os.path.join(conf.img_dir, conf.iso_image.name)
logger.info("Install ISO:\n\t%s", install_iso)
vm.vm_create(vm_config)
vm.vm_mem(vm_config)
vm.vm_attach_dvd(vm_name, install_iso)
if conf.wbatch:
vm.vm_attach_guestadd_iso(vm_name)
vm.create_vdi(base_build_disk, conf.base_disk_size)
vm.vm_attach_disk(vm_name, base_build_disk)
if conf.wbatch:
# Automounted on /media/sf_bootstrap for first boot
vm.vm_add_share_automount(vm_name, conf.share_dir, "bootstrap")
# Mounted on /conf.share_name after first boot
vm.vm_add_share(vm_name, conf.share_dir, conf.share_name)
else:
vm.vm_port(vm_name, "ssh", conf.vm[vm_name].ssh_port, 22)
vm.vbm("modifyvm", vm_name, "--boot1", "dvd")
vm.vbm("modifyvm", vm_name, "--boot2", "disk")
autostart.autostart_reset()
if conf.wbatch:
autostart.autostart_queue("osbash/activate_autostart.sh")
autostart.autostart_queue("osbash/base_fixups.sh")
autostart.autostart_from_config(conf.base_install_scripts)
autostart.autostart_queue("zero_empty.sh", "shutdown.sh")
logger.info("Booting VM %s.", vm_name)
vm.vm_boot(vm_name)
# Note: It takes about 5 seconds for the installer in the VM to be ready
# on a fairly typical laptop. If we don't wait long enough, the
# installation will fail. Ideally, we would have a different method
# of making sure the installer is ready. For now, we just have to
# try and err on the side of caution.
delay = 10
logger.info("Waiting %d seconds for VM %s to come up.", delay, vm_name)
cs.conditional_sleep(delay)
logger.info("Booting into distribution installer.")
distro_boot.distro_start_installer(vm_config)
autostart.autostart_and_wait(vm_name)
vm.vm_wait_for_shutdown(vm_name)
# Detach disk from VM now or it will be deleted by vm_unregister_del
vm.vm_detach_disk(vm_name)
vm.vm_unregister_del(vm_name)
del conf.vm[vm_name]
logger.info("Compacting %s.", base_build_disk)
vm.vbm("modifyhd", base_build_disk, "--compact")
# This disk will be moved to a new name, and this name will be used for
# a new disk next time the script runs.
vm.disk_unregister(base_build_disk)
logger.info("Base disk created.")
logger.info("Moving base disk to:\n\t%s", base_disk_path)
if conf.do_build:
import shutil
shutil.move(base_build_disk, base_disk_path)
if conf.wbatch:
wbatch.wbatch_rename_disk(os.path.basename(base_build_disk),
os.path.basename(base_disk_path))
wbatch.wbatch_end_file()
logger.info("Base disk build ends.")

View File

@ -0,0 +1,81 @@
#!/usr/bin/env python
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import logging
import os
import stacktrain.config.general as conf
import stacktrain.config.virtualbox as cvb
import stacktrain.virtualbox.vm_create as vm
import stacktrain.core.functions_host as host
logger = logging.getLogger(__name__)
# TODO could vm_create_code become generic enough for base_disk install?
def configure_node_netifs(vm_name):
for index, iface in enumerate(conf.vm[vm_name].net_ifs):
if iface["typ"] == "dhcp":
vm.vm_nic_base(vm_name, index)
elif iface["typ"] == "manual":
vm.vm_nic_std(vm_name, iface, index)
elif iface["typ"] == "static":
vm.vm_nic_std(vm_name, iface, index)
else:
logger.error("Unknown interface type: %s", iface.typ)
raise ValueError
if iface["prio"]:
# Elevate boot prio so this particular NIC is used for PXE booting
# Set whether or not we use PXE booting (disk has always priority
# if it contains a bootable image)
vm.vm_nic_set_boot_prio(vm_name, iface, index)
def vm_create_node(vm_name):
try:
vm_config = conf.vm[vm_name]
except Exception:
logger.error("Failed to import VM configuration config.vm_%s.",
vm_name)
raise
vm.vm_create(vm_config)
vm.vm_mem(vm_config)
vm.vm_cpus(vm_config)
configure_node_netifs(vm_name)
if conf.vm[vm_name].ssh_port:
vm.vm_port(vm_name, "ssh", conf.vm[vm_name].ssh_port, 22)
if conf.vm[vm_name].http_port:
vm.vm_port(vm_name, "http", conf.vm[vm_name].http_port, 80)
if conf.wbatch:
vm.vm_add_share(vm_name, conf.share_dir, conf.share_name)
for index, disk in enumerate(conf.vm[vm_name].disks):
# Turn number into letter (0->a, 1->b, etc.)
disk_letter = chr(index + ord('a'))
port = index
if disk is None:
continue
elif disk == "base":
vm.vm_attach_disk_multi(vm_name, cvb.get_base_disk_path())
else:
size = disk
disk_name = "{}-sd{}.vdi".format(vm_name, disk_letter)
disk_path = os.path.join(conf.img_dir, disk_name)
# print("Adding additional disk to {}:\n\t{}".format(vm_name,
# disk_path))
vm.create_vdi(disk_path, size)
vm.vm_attach_disk(vm_name, disk_path, port)

View File

@ -0,0 +1,134 @@
#!/usr/bin/env python
# The functions in this library are used to get scancode strings for VirtualBox
# keyboard input (keyboardputscancode).
#
# It was generated mostly from output of Cameron Kerr's scancodes.l:
# http://humbledown.org/keyboard-scancodes.xhtml
# Force Python 2 to use float division even for ints
from __future__ import division
from __future__ import print_function
import stacktrain.virtualbox.vm_create as vm
def char2scancode(key):
keycodes = {
'a': "1e 9e",
'b': "30 b0",
'c': "2e ae",
'd': "20 a0",
'e': "12 92",
'f': "21 a1",
'g': "22 a2",
'h': "23 a3",
'i': "17 97",
'j': "24 a4",
'k': "25 a5",
'l': "26 a6",
'm': "32 b2",
'n': "31 b1",
'o': "18 98",
'p': "19 99",
'q': "10 90",
'r': "13 93",
's': "1f 9f",
't': "14 94",
'u': "16 96",
'v': "2f af",
'w': "11 91",
'x': "2d ad",
'y': "15 95",
'z': "2c ac",
'A': "2a 1e 9e aa",
'B': "2a 30 b0 aa",
'C': "2a 2e ae aa",
'D': "2a 20 a0 aa",
'E': "2a 12 92 aa",
'F': "2a 21 a1 aa",
'G': "2a 22 a2 aa",
'H': "2a 23 a3 aa",
'I': "2a 17 97 aa",
'J': "2a 24 a4 aa",
'K': "2a 25 a5 aa",
'L': "2a 26 a6 aa",
'M': "2a 32 b2 aa",
'N': "2a 31 b1 aa",
'O': "2a 18 98 aa",
'P': "2a 19 99 aa",
'Q': "2a 10 90 aa",
'R': "2a 13 93 aa",
'S': "2a 1f 9f aa",
'T': "2a 14 94 aa",
'U': "2a 16 96 aa",
'V': "2a 2f af aa",
'W': "2a 11 91 aa",
'X': "2a 2d ad aa",
'Y': "2a 15 95 aa",
'Z': "2a 2c ac aa",
'1': "02 82",
'2': "03 83",
'3': "04 84",
'4': "05 85",
'5': "06 86",
'6': "07 87",
'7': "08 88",
'8': "09 89",
'9': "0a 8a",
'0': "0b 8b",
'!': "2a 02 82 aa",
'@': "2a 03 83 aa",
'#': "2a 04 84 aa",
'$': "2a 05 85 aa",
'%': "2a 06 86 aa",
'^': "2a 07 87 aa",
'&': "2a 08 88 aa",
'*': "2a 09 89 aa",
'(': "2a 0a 8a aa",
')': "2a 0b 8b aa",
'-': "0c 8c",
'_': "2a 0c 8c aa",
'=': "0d 8d",
'+': "2a 0d 8d aa",
' ': "39 b9",
'[': "1a 9a",
']': "1b 9b",
'{': "2a 1a 9a aa",
'}': "2a 1b 9b aa",
';': "27 a7",
':': "2a 27 a7 aa",
',': "33 b3",
'.': "34 b4",
'/': "35 b5",
'\\': "2b ab",
'|': "2a 2b ab aa",
'?': "2a 35 b5 aa",
'"': "2a 28 a8 aa",
"'": "28 a8",
">": "2a 34 b4 aa",
"<": "2a 33 b3 aa"
}
return keycodes[key]
def esc2scancode():
return "01 81"
def enter2scancode():
return "1c 9c"
def backspace2scancode():
return "0e 8e"
def f6_2scancode():
return "40 c0"
def keyboard_push_scancode(vm_name, code_string):
code = code_string.split(' ')
vm.vbm("controlvm", vm_name, "keyboardputscancode", *code)

View File

@ -0,0 +1,663 @@
#!/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
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_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):
if vm_exists(vm_name):
try:
output = vbm("snapshot", vm_name, "list", "--machinereadable",
show_err=False)
except EnvironmentError:
# No snapshots
output = None
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)

View File

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2010-2011 OpenStack Foundation
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Test StackTrain!
----------------------------------
Tests for `stacktrain` module.
"""
from labs.tests import base
class TestLabs(base.TestCase):
def test_something(self):
pass

1
labs/tools Symbolic link
View File

@ -0,0 +1 @@
osbash/tools