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 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:
parallelsvm
GitHub: parallelsvm