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:
parent
cb35bb360a
commit
075bee7f11
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
55
labs/.pylintrc
Normal 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
|
@ -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
1
labs/autostart
Symbolic link
@ -0,0 +1 @@
|
||||
osbash/autostart
|
1
labs/config
Symbolic link
1
labs/config
Symbolic link
@ -0,0 +1 @@
|
||||
osbash/config
|
@ -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
|
||||
|
@ -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
1
labs/scripts
Symbolic link
@ -0,0 +1 @@
|
||||
osbash/scripts
|
270
labs/st.py
Executable file
270
labs/st.py
Executable 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())
|
@ -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)
|
284
labs/stacktrain/batch_for_windows.py
Normal file
284
labs/stacktrain/batch_for_windows.py
Normal 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
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
0
labs/stacktrain/config/__init__.py
Normal file
0
labs/stacktrain/config/__init__.py
Normal file
323
labs/stacktrain/config/general.py
Normal file
323
labs/stacktrain/config/general.py
Normal 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
|
12
labs/stacktrain/config/virtualbox.py
Normal file
12
labs/stacktrain/config/virtualbox.py
Normal 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")
|
0
labs/stacktrain/core/__init__.py
Normal file
0
labs/stacktrain/core/__init__.py
Normal file
305
labs/stacktrain/core/autostart.py
Normal file
305
labs/stacktrain/core/autostart.py
Normal 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)
|
17
labs/stacktrain/core/cond_sleep.py
Normal file
17
labs/stacktrain/core/cond_sleep.py
Normal 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)
|
71
labs/stacktrain/core/download.py
Normal file
71
labs/stacktrain/core/download.py
Normal 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()
|
92
labs/stacktrain/core/functions_host.py
Normal file
92
labs/stacktrain/core/functions_host.py
Normal 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()
|
100
labs/stacktrain/core/helpers.py
Normal file
100
labs/stacktrain/core/helpers.py
Normal 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
|
91
labs/stacktrain/core/iso_image.py
Normal file
91
labs/stacktrain/core/iso_image.py
Normal 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
|
39
labs/stacktrain/core/keycodes.py
Normal file
39
labs/stacktrain/core/keycodes.py
Normal 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)
|
16
labs/stacktrain/core/node_builder.py
Normal file
16
labs/stacktrain/core/node_builder.py
Normal 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()
|
47
labs/stacktrain/core/report.py
Normal file
47
labs/stacktrain/core/report.py
Normal 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
146
labs/stacktrain/core/ssh.py
Normal 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)
|
0
labs/stacktrain/distros/__init__.py
Normal file
0
labs/stacktrain/distros/__init__.py
Normal file
26
labs/stacktrain/distros/distro.py
Normal file
26
labs/stacktrain/distros/distro.py
Normal 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)
|
115
labs/stacktrain/distros/ubuntu_14_04_server_amd64.py
Normal file
115
labs/stacktrain/distros/ubuntu_14_04_server_amd64.py
Normal 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)
|
115
labs/stacktrain/distros/ubuntu_14_04_server_i386.py
Normal file
115
labs/stacktrain/distros/ubuntu_14_04_server_i386.py
Normal 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)
|
134
labs/stacktrain/distros/ubuntu_16_04_server_amd64.py
Normal file
134
labs/stacktrain/distros/ubuntu_16_04_server_amd64.py
Normal 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)
|
0
labs/stacktrain/kvm/__init__.py
Normal file
0
labs/stacktrain/kvm/__init__.py
Normal file
132
labs/stacktrain/kvm/install_base.py
Normal file
132
labs/stacktrain/kvm/install_base.py
Normal 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.")
|
107
labs/stacktrain/kvm/install_node.py
Normal file
107
labs/stacktrain/kvm/install_node.py
Normal 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")
|
134
labs/stacktrain/kvm/keycodes.py
Normal file
134
labs/stacktrain/kvm/keycodes.py
Normal 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)
|
654
labs/stacktrain/kvm/vm_create.py
Normal file
654
labs/stacktrain/kvm/vm_create.py
Normal 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
19
labs/stacktrain/setup.py
Normal 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)
|
0
labs/stacktrain/tests/__init__.py
Normal file
0
labs/stacktrain/tests/__init__.py
Normal file
0
labs/stacktrain/virtualbox/__init__.py
Normal file
0
labs/stacktrain/virtualbox/__init__.py
Normal file
181
labs/stacktrain/virtualbox/install_base.py
Normal file
181
labs/stacktrain/virtualbox/install_base.py
Normal 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.")
|
81
labs/stacktrain/virtualbox/install_node.py
Normal file
81
labs/stacktrain/virtualbox/install_node.py
Normal 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)
|
134
labs/stacktrain/virtualbox/keycodes.py
Normal file
134
labs/stacktrain/virtualbox/keycodes.py
Normal 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)
|
663
labs/stacktrain/virtualbox/vm_create.py
Normal file
663
labs/stacktrain/virtualbox/vm_create.py
Normal 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)
|
@ -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
|
||||
|
@ -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
1
labs/tools
Symbolic link
@ -0,0 +1 @@
|
||||
osbash/tools
|
Loading…
x
Reference in New Issue
Block a user