CentOS Parallels VM

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: vagrantfile

ParallelsVM

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 root
or
$ 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: parallelsvm

GitHub: parallelsvm