Mac – Bash – Parallels Agent Install Script

email me

The Parallels Management Agent shell script—InstallAgentUnattend.sh—is used to automate the Parallels Mac Management Agent installation.

This works great for on site computers, or with machines connected to VPN.

What needs more research, is line 247 in the script: pma_agent_registrator (this file is responsible for the registration portion of the setup process). I would like to figure out how to pass the web enrollment URL (example: https://DMZ.DOMAIN.COM/ParallelsMacManagement.Enrollment) to automatically enroll off site, non-VPN machines.

Expand>

 

But…for the connected machines, the script downloads the DMG, mounts the DMG, installs the agent, enrolls the Mac, and dismounts the DMG. Honestly, it’s all pretty cool. If you decide to embed the credentials, I do recommend creating a PKG.

When the script runs, you should see this at the end:

If you have any issues, such as KDC and KRBv5 errors, check to make sure ports 8760 and 8761 are open (check your AV and firewalls)

How to test port: terminal > telnet 11.11.11.11 8760 (where the IP is your internal proxy server)

http://kb.parallels.com/en/124318 http://kb.parallels.com/en/122879 http://kb.parallels.com/en/124306


Script

#!/usr/bin/env bash

################################################################
#
# Parallels Mac Management for SCCM
# Mac Client Unattended Installation Script
# Tested: 8/21/2019, High Sierra, Mojave
#
################################################################

# Administrator Settings

# PMA Agent installer image download URL
PMA_AGENT_DMG_DOWNLOAD_URL=http://www.YOURWEBSITE/pma_agent.dmg

# Dedicated PMA Agent registration user credentials
# to authenticate with Active Directory
export PMA_AGENT_REGISTRATION_USERNAME=YourUsername
export PMA_AGENT_REGISTRATION_PASSWORD=YourPassword
export PMA_AGENT_REGISTRATION_DOMAIN=YourDomain.com

################################################################

PMA_AGENT_DMG_LOCAL_FILENAME=/tmp/pma_agent.$RANDOM.dmg

clear && printf '\e[3J'

VER_PRODUCTNAME_STR="Parallels Mac Management for Microsoft SCCM"

MAGT_INSTALL_DIR="/Library/Parallels"
MAGT_PLIST_ID="com.parallels.pma.agent"
MAGT_LAUNCHDAEMON_PLISTFILE="/Library/LaunchDaemons/com.parallels.pma.agent.launchdaemon.plist"
MAGT_LAUNCHAGENT_PLISTFILE="/Library/LaunchAgents/com.parallels.pma.agent.launchagent.plist"
MAGT_LAUNCH_APPINDEX_DAEMON_PLISTFILE="/Library/LaunchDaemons/com.parallels.pma.agent.launch.appindex.daemon.plist"
MAGT_LAUNCH_CEP_DAEMON_PLISTFILE="/Library/LaunchDaemons/com.parallels.pma.agent.launchcep.plist"
MAGT_UNATTENDED_INSTALLATION_FLAG_FILE="/tmp/pma_agent.installing.unattended"

if [ $PARALLELS_INTERNAL ]; then
ALLOW_UNTRUSTED_FLAG="-allowUntrusted"
fi

Cleanup()
{
hdiutil detach "/Volumes/$VER_PRODUCTNAME_STR"
rm -f $PMA_AGENT_DMG_LOCAL_FILENAME
rm -f $MAGT_UNATTENDED_INSTALLATION_FLAG_FILE
}

Log()
{
echo "$@"
}

LogError()
{
echo >&2 "Error: $@"
}

IsSystemAtLeast()
{
if [ -z "$SYSTEM_VERSION" ]; then
local sysVerPlist="/System/Library/CoreServices/SystemVersion.plist"
SYSTEM_VERSION=$(/usr/libexec/PlistBuddy -c "Print :ProductVersion" "$sysVerPlist")
fi

local minVer=$(printf "%04d%04d%04d%04d" $(echo "$1" | tr '.' '\n' | head -n 4))
local curVer=$(printf "%04d%04d%04d%04d" $(echo "$SYSTEM_VERSION" | tr '.' '\n' | head -n 4))

if [ $minVer -le $curVer ]; then
echo YES
return 0
else
echo ''
return 1
fi
}

GetFinderPid()
{
local userName="$1"
local output
local rv

output=$(ps -xo user,pid,comm -u "$userName" | grep -E "^.*/Finder.app/.*/Finder(?:\s+.*)*$" | head -n 1)
test "$output" || { echo "Cannot find Finder for ${userName}."; return 1; }

output=$(echo "$output" | tr -s ' ' | cut -d ' ' -f 2)
test "$output" || { echo "Cannot get Finder PID for ${userName}."; return 1; }

echo "$output"
return 0
}

LaunchctlLoad()
{
local jobPlist="$1"
local userName="$2"

local bootstrapPid
local jobLabel
local output
local userID

jobLabel=$(/usr/libexec/PlistBuddy -c "Print :Label" "$jobPlist" 2>&1)
(( $? )) && { LogError "Cannot read job label from $jobPlist"; LogError "$jobLabel"; return 1; }

(( $EUID )) && { LogError "You must be root to perform this operation."; return 1; }

if [ -z "$userName" ]; then
Log "Loading $jobLabel for system..."
else
Log "Loading $jobLabel for user '$userName'..."
fi

test "$DRY_RUN" && return 0

if [ $(IsSystemAtLeast "10.11") ]; then
if [ -z "$userName" ]; then
output=$(launchctl bootstrap system "$jobPlist" 2>&1)
else
userID=$(id -u "$userName" 2>&1)
(( $? )) && { LogError "$userID"; return 1; }
output=$(launchctl bootstrap gui/$userID "$jobPlist" 2>&1)
fi
else
if [ -z "$userName" ]; then
output=$(launchctl load "$jobPlist" 2>&1)
else
bootstrapPid=$(GetFinderPid $userName)
(( $? )) && { LogError "$bootstrapPid"; return 1; }
output=$(launchctl bsexec "$bootstrapPid" sudo -u "$userName" launchctl load "$jobPlist" 2>&1)
fi
fi

(( $? )) && { LogError "$output"; return 1; }

Log "Job $jobLabel loaded successfully"
return 0
}

LaunchctlUnload()
{
local jobPlist="$1"
local userName="$2"

local bootstrapPid
local jobLabel
local output
local userID

jobLabel=$(/usr/libexec/PlistBuddy -c "Print :Label" "$jobPlist" 2>&1)
(( $? )) && { LogError "Cannot read job label from $jobPlist"; LogError "$jobLabel"; return 1; }

(( $EUID )) && { LogError "You must be root to perform this operation."; return 1; }

if [ -z "$userName" ]; then
Log "Unloading $jobLabel for system..."
else
Log "Unloading $jobLabel for user '$userName'..."
fi

test "$DRY_RUN" && return 0

if [ $(IsSystemAtLeast "10.11") ]; then
if [ -z "$userName" ]; then
output=$(launchctl bootout system "$jobPlist" 2>&1)
else
userID=$(id -u "$userName" 2>&1)
(( $? )) && { LogError "$userID"; return 1; }
output=$(launchctl bootout gui/$userID "$jobPlist" 2>&1)
fi
else
if [ -z "$userName" ]; then
output=$(launchctl unload "$jobPlist" 2>&1)
else
bootstrapPid=$(GetFinderPid $userName)
(( $? )) && { LogError "$bootstrapPid"; return 1; }
output=$(launchctl bsexec "$bootstrapPid" sudo -u "$userName" launchctl unload "$jobPlist" 2>&1)
fi
fi

(( $? )) && { LogError "$output"; return 1; }

Log "Job $jobLabel unloaded successfully"
return 0
}

StopAgents()
{
local users

users=$(ps -xao user,comm | grep -E "^.*/pma_agent.app/.*/pma_agent_ui(?:\s+.*)*$" | tr -s ' ' | cut -d ' ' -f 1)
for user in $users
do
LaunchctlUnload "$MAGT_LAUNCHAGENT_PLISTFILE" "$user"
done
}

StartAgents()
{
local users

users=$(ps -xao user,comm | grep -E "^.*/Finder.app/.*/Finder$" | grep -v grep | awk '{print $1}')
for user in $users
do
LaunchctlLoad "$MAGT_LAUNCHAGENT_PLISTFILE" "$user"
done
}

###############################################################################
# Main
###############################################################################

trap Cleanup EXIT
trap "exit 1" SIGHUP SIGINT SIGTERM SIGQUIT

# Create flag file and fill with random content
echo $PMA_AGENT_DMG_LOCAL_FILENAME > $MAGT_UNATTENDED_INSTALLATION_FLAG_FILE
if [ $? -ne 0 ]; then
LogError "Cannot create flag file '$MAGT_UNATTENDED_INSTALLATION_FLAG_FILE'"
exit 1
fi

Log "Downloading Mac Client installation image to $PMA_AGENT_DMG_LOCAL_FILENAME..."
curl -# -o "$PMA_AGENT_DMG_LOCAL_FILENAME" "$PMA_AGENT_DMG_DOWNLOAD_URL" || exit 1

Log "Installing Mac Client..."
hdiutil attach "$PMA_AGENT_DMG_LOCAL_FILENAME" || exit 1
installer -verbose -pkg "/Volumes/$VER_PRODUCTNAME_STR/$VER_PRODUCTNAME_STR.pkg" -target / ${ALLOW_UNTRUSTED_FLAG} || exit 1

Log "Waiting for postinstall script completion..."
while [ -n "$(ps aux | grep "$VER_PRODUCTNAME_STR" | grep postinstall | grep -v grep)" ]; do sleep 0.1; done

Log "Stop components..."
test -f "$MAGT_LAUNCHAGENT_PLISTFILE" && StopAgents
test -f "$MAGT_LAUNCH_APPINDEX_DAEMON_PLISTFILE" && LaunchctlUnload "$MAGT_LAUNCH_APPINDEX_DAEMON_PLISTFILE"
test -f "$MAGT_LAUNCH_CEP_DAEMON_PLISTFILE" && LaunchctlUnload "$MAGT_LAUNCH_CEP_DAEMON_PLISTFILE"
test -f "$MAGT_LAUNCHDAEMON_PLISTFILE" && LaunchctlUnload "$MAGT_LAUNCHDAEMON_PLISTFILE"

Log "Register Mac Client..."
"$MAGT_INSTALL_DIR/pma_agent.app/Contents/MacOS/pma_agent_registrator" || exit 1

Log "Start components..."
LaunchctlLoad "$MAGT_LAUNCHDAEMON_PLISTFILE" || { Log "STOP"; exit 1; }
LaunchctlLoad "$MAGT_LAUNCH_CEP_DAEMON_PLISTFILE"
LaunchctlLoad "$MAGT_LAUNCH_APPINDEX_DAEMON_PLISTFILE"
StartAgents

Log "Mac Client successfully installed"

 

 

Notes

User Guide (PDF)

Parallels Mac Management Technical Documentation

Required Ports

Mac SEP Uninstall Script

 

Some Install Paths:


Agent Cache:

Library > Caches > com.parallels.pma.agent

Keychain Access:
Library > Keychains > pmm-client.keychain

1 plist in Library > LaunchAgents
— com.parallels.pma.agent.launchagent.plist

3 plist in Library > LaunchDaemons
— com.parallels.pma.agent.appindex.daemon.plist
— com.parallels.pma.agent.launchcep.plist
— com.parallels.pma.agent.launchdaemon.plist

6 files in Library > Preferences
— com.parallels.pma.agent.cert.pem
— com.parallels.pma.agent.pkey.pem
— com.parallels.pma.agent.pki.cert.pem
— com.parallels.pma.agent.pki.pkey.pem
— com.parallels.pma.agent.plist {this data shows up in System Preferences > Parallels}
— com.parallels.pma.agent.sccmproxy.cert.pem

PMA Settings Location:
/Library/Application Support/Parallels/PMA_Agent

PMA APP Location:
/Library/Parallels/pma_agent.app
— Contents
—— MacOS
——— authhelper
——— pma_agent
——— pma_agent_registrator
——— pma_agent_ui
——— pma_agent_uninstaller
——— pma_crash_monitor
——— pma_fdehelper
——— pma_forwarding
——— pma_installer_helper
——— pma_report_tool
——— pmm_app_portal
——— pmm_appindex_agent
——— pmm_cep_service
——— pmmctl

Scripted unattend, by passing values into parameters:
sudo ./InstallAgentUnattended.sh http://dmz.domain.com/pma_agent.dmg UserName UserPassword UserDomain

Get Policies: /Library/Parallels/pma_agent.app/Contents/MacOS
pmmctl get-policies

Scan Updates: /Library/Parallels/pma_agent.app/Contents/MacOS
pmmctl scan-updates

Send Inventory: /Library/Parallels/pma_agent.app/Contents/MacOS
pmmctl report-hv-inventory

Uninstall PMA by APP: /Library/Parallels/pma_agent.app/Contents/MacOS/
pma_agent_uninstaller.app

or

Uninstall PMA by Shell Script:
sudo /bin/bash –с /Library/Parallels/pma_agent.app/Contents/MacOS/pma_agent_uninstaller.app/Contents/Resources/UninstallAgentScript.sh

or

#! /bin/bash

VER_SHORTPRODUCTNAME_STR="Parallels Mac Management"
VER_FULL_BUILD_NUMBER_STR="7.3.3.5"
MAGT_LAUNCHAGENT_PLISTFILE="/Library/LaunchAgents/com.parallels.pma.agent.launchagent.plist"
MAGT_LAUNCHDAEMON_PLISTFILE="/Library/LaunchDaemons/com.parallels.pma.agent.launchdaemon.plist"
MAGT_LAUNCH_APPINDEX_DAEMON_PLISTFILE="/Library/LaunchDaemons/com.parallels.pma.agent.launch.appindex.daemon.plist"
MAGT_LAUNCH_CEP_DAEMON_PLISTFILE="/Library/LaunchDaemons/com.parallels.pma.agent.launchcep.plist"
MAGT_PLISTFILE="/Library/Preferences/com.parallels.pma.agent.plist"
MAGT_SUPPORT_DIR="/Library/Application Support/Parallels/PMA_Agent"
MAGT_KEYCHAIN_FILE="/Library/Keychains/pmm-client.keychain"
MAGT_CACHE_DIR="/Library/Caches/com.parallels.pma.agent"
MAGT_INSTALL_DIR="/Library/Parallels"
MAGT_APP_PORTAL_INSTALL_DIR="/Applications"
MAGT_APP_PORTAL_BUNDLE_NAME="Parallels Application Portal.app"
MAC_AGENT_SUCATALOG_URL="SuCatalogUrl"
PMM_CLIENT_BUNDLE_NAME="pma_agent.app"
SOFTWARE_UPDATE_TOOL_PATH="/usr/sbin/softwareupdate"
SOFTWARE_UPDATE_PREFERENCES_PLIST="/Library/Preferences/com.apple.SoftwareUpdate.plist"
PMM_PREFERENCE_PANE_PATH="/Library/PreferencePanes/PRLPmmPreferencePane.prefPane"
MAGT_PROBLEM_REPORTS_DIR="/Users/Shared/Parallels/Problem Reports"
DSCL_USERS_CACHE_PATH="/var/db/dslocal/nodes/Default/users"

SCRIPT_NAME=`basename "${0}"`
SCRIPT_DIR=`dirname "${0}"`
SCRIPT_TITLE="$VER_SHORTPRODUCTNAME_STR v$VER_FULL_BUILD_NUMBER_STR - Uninstall Mac Client"

Log()
{
echo "${SCRIPT_NAME%.*}: $@"
test -z "$ROOT_PATH" && logger -p install.info -t "$VER_SHORTPRODUCTNAME_STR" "$@"
return 0
}

LogError()
{
echo >&2 "${SCRIPT_NAME%.*}:Error: $@"
test -z "$ROOT_PATH" && logger -p install.info -t "$VER_SHORTPRODUCTNAME_STR" "Error: $@"
return 0
}

RemoveFile()
{
local path="$(CleanPath "$ROOT_PATH/$1")"
local fileName="$(basename "$path")"
local dirPath="$(dirname "$path")"

while IFS= read -r -d '' file; do
Log "Removing file $file..."

test "$DRY_RUN" && continue

local output=$(rm -f "$file" 2>&1)
if [ $? -ne 0 ]; then
LogError "Cannot delete file: $file"
LogError "$output"
fi
done < <(find "$dirPath" -maxdepth 1 -type f -name "$fileName" -print0 2>/dev/null)

return 0
}

RemoveDir()
{
local path="$(CleanPath "$ROOT_PATH/$1")"
local fileName="$(basename "$path")"
local dirPath="$(dirname "$path")"

while IFS= read -r -d '' file; do
Log "Removing directory $file..."

test "$DRY_RUN" && continue

local output=$(rm -rf "$file" 2>&1)
if [ $? -ne 0 ]; then
LogError "Cannot delete file: $file"
LogError "$output"
fi
done < <(find "$dirPath" -maxdepth 1 -type d -name "$fileName" -print0 2>/dev/null)

return 0
}

#
# Checks is specified directory empty. Ignores .DS_Store file.
#
IsDirEmpty()
{
local path="$(CleanPath "$ROOT_PATH/$1")"

test ! -d "$path" && return 1

find "$path" -maxdepth 1 -mindepth 1 -not -name ".DS_Store" &>/dev/null
if [ $? -eq 0 ]; then
echo YES
return 0
else
echo ''
return 1
fi
}

#
# Removes all redundant '/' from path
#
CleanPath()
{
shopt -s extglob
echo "${1//+(\/)//}"
shopt -u extglob
}

#
# Prints script usage information
#
PrintUsage()
{
read -d '' help <<- EOF
Usage: $SCRIPT_NAME [--root <path>] [--dry-run]
EOF

LogError "$help"
}

IsSystemAtLeast()
{
if [ -z "$SYSTEM_VERSION" ]; then
SYSTEM_VERSION=$(/usr/bin/sw_vers -productVersion)
fi

local minVer=$(printf "%04d%04d%04d%04d" $(echo "$1" | tr '.' '\n' | head -n 4))
local curVer=$(printf "%04d%04d%04d%04d" $(echo "$SYSTEM_VERSION" | tr '.' '\n' | head -n 4))

if [ $minVer -le $curVer ]; then
echo YES
return 0
else
echo ''
return 1
fi
}

# Source launchctl helpers
. "$SCRIPT_DIR/launchctl_utils.sh"

###############################################################################
# Main
###############################################################################

Log "${SCRIPT_TITLE} ("`date`")"

#
# Parse command line arguments
#
while [[ $# > 0 ]]
do
key="$1"
case $key in

--root)
ROOT_PATH="$2"
shift
;;

--dry-run)
DRY_RUN="yes"
;;

*)
LogError "Unrecognized argument \"$key\""
PrintUsage
exit 1
;;
esac
shift
done

#
# Check required arguments
#
if [ "${ROOT_PATH+yes}" ]; then
if [ -z "$ROOT_PATH" ]; then
LogError "Argument --root cannot be empty"
exit 1
elif [ ! -d "$ROOT_PATH" ]; then
LogError "Directory not found: $ROOT_PATH"
exit 1
fi
fi

if [ "$EUID" -ne 0 ]; then
LogError "Requires admin privileges, please re-run as root via sudo"
exit 1
fi

#
# Stop services
#
if [ -z "$ROOT_PATH" ]; then

if [ -f "$MAGT_LAUNCHAGENT_PLISTFILE" ]; then
# Stop all instances of MacClient agent
for userName in $(ps -xao user,comm | grep -E "^.*/pma_agent.app/.*/pma_agent_ui(?:\s+.*)*$" | tr -s ' ' | cut -d ' ' -f 1)
do
LaunchctlUnload "$MAGT_LAUNCHAGENT_PLISTFILE" "$userName"
done
fi

test -f "$MAGT_LAUNCHDAEMON_PLISTFILE" && LaunchctlUnload "$MAGT_LAUNCHDAEMON_PLISTFILE"
test -f "$MAGT_LAUNCH_CEP_DAEMON_PLISTFILE" && LaunchctlUnload "$MAGT_LAUNCH_CEP_DAEMON_PLISTFILE"
test -f "$MAGT_LAUNCH_APPINDEX_DAEMON_PLISTFILE" && LaunchctlUnload "$MAGT_LAUNCH_APPINDEX_DAEMON_PLISTFILE"
fi

#
# Remove services configuration plists
#
RemoveFile "$MAGT_LAUNCHDAEMON_PLISTFILE"
RemoveFile "$MAGT_LAUNCHAGENT_PLISTFILE"
RemoveFile "$MAGT_LAUNCH_APPINDEX_DAEMON_PLISTFILE"
RemoveFile "$MAGT_LAUNCH_CEP_DAEMON_PLISTFILE"

#
# Remove Application Support data
#
RemoveDir "$MAGT_SUPPORT_DIR"

#
# Remove per-user Application Support data
#
for userPlist in "$(CleanPath "$ROOT_PATH/$DSCL_USERS_CACHE_PATH")"/[!_]*.plist
do
test -e "$userPlist" || break

userName=$(/usr/libexec/PlistBuddy -c "Print :name:0" "$userPlist" 2>&1)
if [ $? -ne 0 ]; then
LogError "$userName"
continue
fi

userUid=$(/usr/libexec/PlistBuddy -c "Print :uid:0" "$userPlist" 2>&1)
if [ $? -ne 0 ]; then continue; fi

# Local user has UIDs in range (500;1000) or 0 (root)
if [[ "$userUid" != 0 && ( "$userUid" -le 500 || "$userUid" -ge 1000 ) ]]; then continue; fi

userHome=$(/usr/libexec/PlistBuddy -c "Print :home:0" "$userPlist" 2>&1)
if [ $? -ne 0 ]; then continue; fi

RemoveDir "$userHome/$MAGT_SUPPORT_DIR"
done

#
# Revert Software Update catalog URL if need
#
if [ -z "$ROOT_PATH" ]; then
expectedURL=$(/usr/libexec/PlistBuddy -c "Print :$MAC_AGENT_SUCATALOG_URL" "$MAGT_PLISTFILE" 2>/dev/null)
actualURL=$(/usr/libexec/PlistBuddy -c "Print :CatalogURL" "$SOFTWARE_UPDATE_PREFERENCES_PLIST" 2>/dev/null)
if [[ "$actualURL" && "$actualURL" == "$expectedURL" ]]; then
if [ $(IsSystemAtLeast "10.9") ]; then
$($SOFTWARE_UPDATE_TOOL_PATH --clear-catalog)
else
$(/usr/libexec/PlistBuddy -c "Delete :CatalogURL" "$SOFTWARE_UPDATE_PREFERENCES_PLIST")
fi
fi
fi

# Remove preferences
RemoveFile "${MAGT_PLISTFILE/%plist/*}"

# Remove keychain file
RemoveFile "$MAGT_KEYCHAIN_FILE"

# Remove cache directory
RemoveDir "$MAGT_CACHE_DIR"

# Remove problem reports
RemoveFile "$MAGT_PROBLEM_REPORTS_DIR/PmaProblemReport*"
# TODO: remove dirs up to "/Users/Shared/Parallels" if empty

# Remove MacClient bundle
RemoveDir "$MAGT_INSTALL_DIR/$PMM_CLIENT_BUNDLE_NAME"
if [ $(IsDirEmpty "$MAGT_INSTALL_DIR") ]; then
RemoveDir "$MAGT_INSTALL_DIR"
fi

# Remove AppPortal bundle
RemoveDir "$MAGT_APP_PORTAL_INSTALL_DIR/$MAGT_APP_PORTAL_BUNDLE_NAME"

# Remove Preferences Pane bundle
RemoveDir "$PMM_PREFERENCE_PANE_PATH"

# Remove install info
RemoveFile "/var/db/receipts/com.parallels.pkg.pma.agent.*"

Log "Completed"

 

echo $TMPDIR

 

The Parallels Mac Management log files are located in the following directories:

• Windows computer running Parallels Configuration Manager Proxy: %Windir%\Logs; %Windir%\Logs\pmm

• Windows computer running Parallels OS X Software Update Point: %Windir%\Logs\pmm

• Windows computer running Configuration Manager console: %Windir%\Logs

• OS X (Parallels Mac Client): /Library/Logs/

 

tags: MrNetTek