Scripts I use for unattended installs of CentOS in a Parallels VM.
Packer+Vagrant
I use Packer to build Vagrant boxes with these packer-vagrant scripts. To spin up an instance of a box, you first
install the Vagrant Parallels Provider, then add a vagrantfile into a directory, and finally run vagrant up.
Vagrant.configure("2") do |config|
config.vm.box = 'JessThrysoee/centos-7-parallels'
config.vm.provider "parallels" do |parallels|
parallels.memory = 512
parallels.cpus = 1
parallels.update_guest_tools = true
parallels.optimize_power_consumption = false
parallels.customize ["set", :id, "--on-shutdown", "close"]
end
config.vm.provision "ansible" do |ansible|
ansible.playbook = "/opt/ansible/playbook.yml"
ansible.groups = {
"avahi" => ["machine1", "machine2"]
}
end
config.vm.define "machine1" do |machine|
machine.vm.hostname = "machine1"
machine.vm.provider "parallels" do |parallels|
parallels.memory = 2048
parallels.cpus = 1
end
end
config.vm.define "machine2" do |machine|
machine.vm.hostname = "machine2"
end
end
# vi: set ft=ruby :
download:
vagrantfileParallelsVM
A Vagrant like shell script for managing CentOS VMs in Parallels.
$ parallelsvm
usage: parallelsvm ACTION [OPTIONS]
ACTIONS:
init
up [vm_name]
halt [vm_name] [--force]
suspend [vm_name]
resume [vm_name]
destroy [vm_name]
info [vm_name]
status [vm_name]
global-status
shell [vm_name]
box add
box list
box saveas <new_box_name> [--force]
box remove <box_name>
box rename <box_name> <new_box_name>
To get started, create a directory and initialize the VM configuration
$ mkdir vmtest $ cd vmtest $ parallelsvm init
If a box hasn't already been created, create one and spin it up
$ parallelsvm box add $ parallelsvm up
Log in to the newly created VM:
$ ssh vmtest -l rootor
$ parallelsvm shell
The current VM can be saved as a box,
$ parallelsvm box saveas my_new_super_box
and now other VMs can use this by specifying BOX_NAME=my_new_super_box in their
parallelsvm.rc configuration.
Here is the script:
#!/bin/bash -e
#
# A Vagrant like shell script for managing CentOS VMs in Parallels.
#
# http://thrysoee.dk/parallelsvm/
#-----------------------------------------------------------------------------------------------
##
##
##
function main() {
init_static_env
create_tmp_dir
trap delete_tmp_dir EXIT
if [ -z "$1" ]
then
usage
fi
local action="$1"
shift 1
# init - create rc file
if [ "$action" = "init" ]
then
create_parallelsvm_rc
exit
# global status
elif [ "$action" = "global-status" ]
then
global_status
exit
fi
source_parallelsvm_rc
init_env
# Box
if [ "$action" = "box" ]
then
if [ "$1" = "add" ]
then
shift 1
assert_parallelsvm_rc
box_add "$@"
elif [ "$1" = "list" ]
then
box_list
elif [ "$1" = "saveas" ]
then
shift 1
box_saveas "$@"
elif [ "$1" = "remove" ]
then
shift 1
box_remove "$@"
elif [ "$1" = "rename" ]
then
shift 1
box_rename "$@"
fi
exit
fi
# VM
if [[ -n $1 && $1 != --* ]]
then
VM_NAME="$1"
shift 1
fi
assert_vm_name
# create VM
if [ "$action" = "up" ]
then
if vm_exists
then
start_vm
else
assert_parallelsvm_rc
if box_exists
then
create_vm_from_box
change_hostname
else
echo "$SCRIPT_NAME: Box '$BOX_NAME' not found: Change BOX_NAME or run '$SCRIPT_NAME box add' to create a new box."
exit
fi
fi
# halt VM
elif [ "$action" = "halt" ]
then
halt_vm "$1"
# suspend VM
elif [ "$action" = "suspend" ]
then
suspend_vm
# resume VM
elif [ "$action" = "resume" ]
then
resume_vm
# destroy VM
elif [ "$action" = "destroy" ]
then
delete_vm
# VM info
elif [ "$action" = "info" ]
then
info_vm
# VM status
elif [ "$action" = "status" ]
then
status_vm
# bash shell in VM
elif [ "$action" = "shell" ]
then
shell_vm
else
usage
fi
}
##
##
##
function usage() {
echo "usage: $SCRIPT_NAME ACTION [OPTIONS]"
echo ""
echo " ACTIONS:"
echo ""
echo " init"
echo " up [vm_name]"
echo ""
echo " halt [vm_name] [--force]"
echo " suspend [vm_name]"
echo " resume [vm_name]"
echo " destroy [vm_name]"
echo ""
echo " info [vm_name]"
echo " status [vm_name]"
echo " global-status"
echo ""
echo " shell [vm_name]"
echo ""
echo " box add"
echo " box list"
echo " box saveas <new_box_name> [--force]"
echo " box remove <box_name>"
echo " box rename <box_name> <new_box_name>"
echo ""
exit
}
##
##
##
function source_parallelsvm_rc() {
if [ -e "$PARALLELSVM_RC" ]
then
. "$PARALLELSVM_RC"
fi
}
##
##
##
function assert_parallelsvm_rc() {
if [ ! -e "$PARALLELSVM_RC" ]
then
echo "$SCRIPT_NAME: $PARALLELSVM_RC not found: call '$SCRIPT_NAME init' or change to dir containing a $PARALLELSVM_RC"
exit
fi
}
##
##
##
function assert_vm_name() {
if [ -z "$VM_NAME" ]
then
echo "$SCRIPT_NAME: VM name not found. Specify a [vm_name] or change to dir containing a $PARALLELSVM_RC"
usage
fi
}
##
##
##
function create_parallelsvm_rc() {
cat > "$PARALLELSVM_RC" <<EOF
HOSTNAME="${PWD##*/}"
VM_NAME="\$HOSTNAME"
VM_CPUS=1
VM_MEMSIZE=512
BOX_NAME=base
DISTRO_ISO_URL="http://isoredirect.centos.org/centos/7/isos/x86_64/CentOS-7.0-1406-x86_64-Minimal.iso"
EOF
}
##
##
##
function vm_exists() {
prlctl list "$VM_NAME" > /dev/null 2>&1
}
##
##
##
function box_exists() {
test -e "$BOX_PATH"
}
##
##
##
function box_template_name() {
local box_name="$1"
echo ${box_name}-template
}
##
##
##
function init_static_env() {
SCRIPT_NAME="${0##*/}"
PARALLELSVM_RC="parallelsvm.rc"
CONF_DIR="$HOME/.parallelsvm"
BOX_DIR="$CONF_DIR/box"
DISTRO_ISO_DIR="$CONF_DIR/iso"
PARALLELS_TOOLS_ISO_PATH="/Applications/Parallels Desktop.app/Contents/Resources/Tools/prl-tools-lin.iso"
}
##
##
##
function init_env() {
#HOSTNAME
#VM_NAME
#VM_CPUS
#VM_MEMSIZE
#DISTRO_ISO_URL
DISTRO_ISO_NAME="${DISTRO_ISO_URL##*/}"
DISTRO_ISO_PATH="$DISTRO_ISO_DIR/$DISTRO_ISO_NAME"
KICKSTART_ISO_DIR="$TMP_DIR/kickstart"
KICKSTART_ISO_PATH="$KICKSTART_ISO_DIR/kickstart.iso"
#BOX_NAME
BOX_TEMPLATE_NAME="$(box_template_name "${BOX_NAME}")"
BOX_PATH="$BOX_DIR/${BOX_TEMPLATE_NAME}.pvm"
}
##
##
##
function make_dirs() {
local dir
for dir in "$@"
do
mkdir -p "$dir"
done
}
##
##
##
function create_tmp_dir() {
TMP_DIR="$(mktemp -d "/tmp/${SCRIPT_NAME}.XXXXXX")"
mkdir -p "$TMP_DIR"
}
##
##
##
function delete_tmp_dir() {
if [ -e "$TMP_DIR" ]
then
rm -rf "$TMP_DIR"
fi
}
##
##
##
function random_string() {
local len="$1"
cat /dev/urandom | LC_CTYPE=C tr -dc "[:alpha:]" | head -c $len
}
##
##
##
function box_add() {
VM_NAME="$(random_string 12)"
VM_DIR="$TMP_DIR"
make_dirs "$DISTRO_ISO_DIR" "$KICKSTART_ISO_DIR" "$BOX_DIR" "$VM_DIR"
pick_isodirect_mirror
fetch_distro_iso
checksum_distro_iso
make_kickstart_cfg
make_kickstart_iso
create_vm
make_parallels_send_kickstart_boot_option
call_parallels_send_kickstart_boot_option
wait_for_shell 10 "Kickstarting"
halt_vm
umount_isos
clone_vm_to_template "$BOX_TEMPLATE_NAME" --force
delete_vm
}
##
##
##
function create_vm() {
prlctl create "$VM_NAME" --ostype linux --dst "$VM_DIR"
prlctl set "$VM_NAME" --on-shutdown close
prlctl set "$VM_NAME" --cpus "$VM_CPUS"
prlctl set "$VM_NAME" --memsize "$VM_MEMSIZE"
prlctl set "$VM_NAME" --device-del net0
prlctl set "$VM_NAME" --device-del cdrom0
prlctl set "$VM_NAME" --device-add cdrom --enable --image "$DISTRO_ISO_PATH"
prlctl set "$VM_NAME" --device-add cdrom --enable --image "$KICKSTART_ISO_PATH"
prlctl set "$VM_NAME" --device-add cdrom --enable --image "$PARALLELS_TOOLS_ISO_PATH"
prlctl set "$VM_NAME" --device-add net --enable --type bridged
prlctl start "$VM_NAME"
}
##
##
##
function clone_vm_to_template() {
local vm_template_name="$1"
local force="$2"
if [ "$force" = "--force" ]
then
rm -rf "$BOX_DIR/${vm_template_name}.pvm"
fi
if prlctl clone "$VM_NAME" --name "$vm_template_name" --template --dst "$BOX_DIR"
then
prlctl unregister "$vm_template_name"
fi
}
##
##
##
function create_vm_from_box() {
if prlctl register "$BOX_PATH"
then
VM_DIR="$PWD/.vm"
make_dirs "$VM_DIR"
prlctl create "$VM_NAME" --ostemplate "$BOX_TEMPLATE_NAME" --dst "$VM_DIR" || true
prlctl set "$VM_NAME" --cpus "$VM_CPUS"
prlctl set "$VM_NAME" --memsize "$VM_MEMSIZE"
prlctl unregister "$BOX_TEMPLATE_NAME"
fi
start_vm
}
##
##
##
function box_saveas() {
local new_box_name="$1"
local force="$2"
if [ -z "$new_box_name" ] then
then
echo "$SCRIPT_NAME: box saveas failed: specify a new box name"
fi
clone_vm_to_template "$(box_template_name "$new_box_name")" $force
}
##
##
##
function box_remove() {
local box_name="$1"
rm -r "$BOX_DIR/$(box_template_name "$box_name").pvm"
}
##
##
##
function box_list() {
ls -1 "$BOX_DIR" | sed 's/-template.pvm//'
}
##
##
##
function box_rename() {
local box_name="$1"
local new_box_name="$2"
local box_template_name="$(box_template_name "$box_name")"
local new_box_template_name="$(box_template_name "$new_box_name")"
prlctl register "$BOX_DIR/$box_template_name.pvm"
prlctl set "$box_template_name" --name "$new_box_template_name"
prlctl unregister "$new_box_template_name"
}
##
##
##
function make_change_hostname_script() {
cat > "$TMP_DIR/change_hostname" <<"EOF"
#!/bin/bash -e
HOSTNAME="$1"
hostnamectl set-hostname "$HOSTNAME"
ifcfg="/etc/sysconfig/network-scripts/ifcfg-eth0"
if grep -q DHCP_HOSTNAME "$ifcfg" 2> /dev/null
then
sed -i '1,$s/^DHCP_HOSTNAME=.*$/DHCP_HOSTNAME='$HOSTNAME'/' "$ifcfg"
else
echo "DHCP_HOSTNAME=$HOSTNAME" >> "$ifcfg"
fi
conn_uuid=$(nmcli -t -f device,uuid conn | grep eth0: | cut -d: -f2)
nmcli dev disconnect eth0
nmcli con up uuid "$conn_uuid"
systemctl restart avahi-daemon.service
EOF
chmod +x "$TMP_DIR/change_hostname"
}
##
##
##
function change_hostname() {
if [ "$(prlctl exec "$VM_NAME" hostname)" = "$HOSTNAME" ]
then
return
fi
local shf="parallelsvm_shared_host_folder"
make_change_hostname_script
if prlctl set "$VM_NAME" --shf-host-add "$shf" --path "$TMP_DIR"
then
## ls blocks until $shf is mounted
prlctl exec "$VM_NAME" ls -l /media/psf/ > /dev/null
prlctl exec "$VM_NAME" /media/psf/$shf/change_hostname $HOSTNAME || true
prlctl set "$VM_NAME" --shf-host-del "$shf"
fi
}
##
##
##
function start_vm() {
prlctl start "$VM_NAME"
wait_for_shell 1 "Booting"
}
##
##
##
function halt_vm() {
if [ "$1" = "--force" ]
then
prlctl stop "$VM_NAME" --kill || true
else
prlctl stop "$VM_NAME" || true
fi
}
##
##
##
function suspend_vm() {
prlctl suspend "$VM_NAME"
}
##
##
##
function resume_vm() {
prlctl resume "$VM_NAME"
}
##
##
##
function delete_vm() {
halt_vm --force 2> /dev/null
prlctl delete "$VM_NAME" || prlctl unregister "$VM_NAME"
}
##
##
##
function info_vm() {
prlctl list --info --full "$VM_NAME"
}
##
##
##
function status_vm() {
prlctl status "$VM_NAME"
}
##
##
##
function global_status() {
echo "VMs:"
echo "----"
prlctl list --all
echo ""
echo "TEMPLATE:"
echo "---------"
prlctl list --all --template
}
##
##
##
function shell_vm() {
prlctl enter "$VM_NAME"
}
##
##
##
function umount_isos() {
prlctl set "$VM_NAME" --device-set cdrom0 --disconnect
prlctl set "$VM_NAME" --device-del cdrom1
prlctl set "$VM_NAME" --device-del cdrom2
}
##
##
##
function wait_for_shell() {
local timeout="$1"
local message="$2"
printf "\n%s " "$message"
while ! prlctl exec "$VM_NAME" true 2> /dev/null
do
printf "."
sleep $timeout
done
printf "\n"
}
##
##
##
function pick_isodirect_mirror() {
if [[ $DISTRO_ISO_URL == */isoredirect.centos.org/* ]]
then
DISTRO_ISO_URL=$(curl -sSL "$DISTRO_ISO_URL" |\
xmllint --html --xmlout --encode utf-8 --dropdtd --nowarning - 2>/dev/null |\
xpath "string(//a/@href[contains(., '/isos/x86_64/')])" 2> /dev/null)
fi
}
##
##
##
function fetch_distro_iso() {
if [ ! -e "$DISTRO_ISO_PATH" ]
then
curl -o "$DISTRO_ISO_PATH" "$DISTRO_ISO_URL"
fi
}
##
##
##
function checksum_distro_iso() {
local mirror_sum=$(curl -s ${DISTRO_ISO_URL%/*}/md5sum.txt | grep "$DISTRO_ISO_NAME" | awk '{print $1}')
local local_sum=$(md5 -q $DISTRO_ISO_PATH)
if [ "$mirror_sum" != "$local_sum" ]
then
echo "$SCRIPT_NAME: checksum error -- try again after removing the old IOS: 'rm $DISTRO_ISO_PATH'"
exit
fi
}
##
##
##
function is_parallels_sdk_installed() {
if ! python -c 'import prlsdkapi' 2> /dev/null
then
echo "$SCRIPT_NAME: 'Parallels Virtualization SDK' is not installed. Get it from http://www.parallels.com/downloads/desktop/"
exit
fi
}
##
##
##
function call_parallels_send_kickstart_boot_option() {
python $TMP_DIR/parallels_send_kickstart_boot_option "$VM_NAME" "i"$'\t'" ks=cdrom:/dev/sr1:/ks.cfg"
}
##
##
##
function make_parallels_send_kickstart_boot_option() {
cat > "$TMP_DIR/parallels_send_kickstart_boot_option" <<"EOF"
#!/usr/bin/env python
import sys
import prlsdkapi
if len(sys.argv) != 3:
print "Usage : parallels_send_kickstart_boot_option '<VM_NAME>' '<KICKSTART_BOOT_OPTION>'"
print "Example: parallels_send_kickstart_boot_option 'CentOS VM' 'ks=http://127.0.0.1:1234/ks.cfg'"
exit()
vm_name=sys.argv[1]
kickstart_boot_option = sys.argv[2]
prlsdk = prlsdkapi.prlsdk
consts = prlsdkapi.prlsdk.consts
#print consts.ScanCodesList
prlsdk.InitializeSDK(consts.PAM_DESKTOP_MAC)
server = prlsdkapi.Server()
login_job=server.login_local()
login_job.wait()
vm_list_job = server.get_vm_list()
result= vm_list_job.wait()
vm_list = [result.get_param_by_index(i) for i in range(result.get_params_count())]
vm = [vm for vm in vm_list if vm.get_name() == vm_name]
if not vm:
vm_names = [vm.get_name() for vm in vm_list]
print "ERROR: Failed to find VM with name '%s' in:" % vm_name
for name in vm_names:
print "'" + name + "'"
exit()
vm = vm[0]
vm_io = prlsdkapi.VmIO()
try:
vm_io.connect_to_vm(vm).wait()
except prlsdkapi.PrlSDKError, e:
print "ERROR: %s" % e
exit()
press = consts.PKE_PRESS
release = consts.PKE_RELEASE
shift_left = consts.ScanCodesList['SHIFT_LEFT']
enter = consts.ScanCodesList['ENTER']
timeout = 5
for c in kickstart_boot_option:
shift = False
if c == " ":
c = consts.ScanCodesList['SPACE']
elif c == "\t":
c = consts.ScanCodesList['TAB']
elif c == "/":
c = consts.ScanCodesList['SLASH']
elif c == "=":
c = consts.ScanCodesList['PLUS']
elif c == ".":
c = consts.ScanCodesList['GREATER']
elif c == ":":
shift = True
c = consts.ScanCodesList['COLON']
else:
c = consts.ScanCodesList[c.upper()]
if shift:
vm_io.send_key_event(vm, shift_left, press, timeout)
vm_io.send_key_event(vm, c, press, timeout)
vm_io.send_key_event(vm, c, release, timeout)
if shift:
vm_io.send_key_event(vm, shift_left, release, timeout)
vm_io.send_key_event(vm, enter, press, timeout)
vm_io.send_key_event(vm, enter, release, timeout)
vm_io.disconnect_from_vm(vm)
server.logoff()
prlsdkapi.deinit_sdk
EOF
}
##
##
##
function make_kickstart_iso() {
hdiutil makehybrid -quiet -iso -joliet -o "$KICKSTART_ISO_PATH" "$KICKSTART_ISO_DIR"
}
##
##
##
function make_kickstart_cfg() {
cat > "$KICKSTART_ISO_DIR/ks.cfg" <<"EOF"
cmdline
skipx
install
cdrom
lang en_US.UTF-8
keyboard us
timezone --utc Etc/UTC
network --activate --onboot yes --device eth0 --bootproto dhcp --noipv6
rootpw --plaintext newroot
authconfig --enableshadow --passalgo=sha512
firewall --enabled --service=ssh,mdns,http,https --port=8080:tcp,8443:tcp
selinux --disabled
bootloader --location=mbr
zerombr
clearpart --all --initlabel
autopart
## TODO beta repo
repo --name=epel --baseurl=http://dl.fedoraproject.org/pub/epel/beta/7/x86_64/
#repo --name=epel --baseurl=http://dl.fedoraproject.org/pub/epel/7/x86_64/
reboot
%packages --nobase
@core --nodefaults
epel-release
%end
%post
exec < /dev/tty3 > /dev/tty3
chvt 3
echo
echo "################################"
echo "# Running Post Configuration #"
echo "################################"
(
echo "Installing Parallels Tools..."
mount -r -o exec /dev/sr2 /mnt
/mnt/install --install-unattended-with-deps --progress
umount /mnt
echo "Note: Parallels Tools can be updated by running 'ptiagent-cmd --info'"
yum install -y\
net-tools\
bash-completion\
git\
vim\
man\
avahi\
avahi-tools\
nss-mdns\
avahi-compat-libdns_sd\
docker-io
systemctl enable docker || true
yum upgrade -y
) 2>&1 | /usr/bin/tee /tmp/post_install.log
chvt 1
%end
EOF
}
main "$@"
# Copyright (c) 2014, Jess Thrysoee
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
download:
parallelsvmGitHub: parallelsvm