The original scripts would check the download directory and see if there was an installer package with the same name as an installer package in the JCDS2 distribution point. If there was an installer package with a matching name, download of the installer package was skipped and the script would move on to the next installer package.
In the updated scripts, if there are installer packages already in the download directory which have the same name as an installer package in the JCDS distribution point, the MD5 hash of the existing installer package is checked against the MD5 hash of the installer package stored in the JCDS distribution point.
If the MD5 hashes match, download of the installer package with the matching name is skipped. If the MD5 hashes do not match, the existing installer package in the download directory is deleted and a fresh copy of the installer package is downloaded. For more details, please see below the jump.
Usage:
/path/to/Jamf_Pro_JCDS_Installer_Package_Download.sh
The script takes the following actions:
The script should provide output similar to this:
username@computername ~ % /path/to/Jamf_Pro_JCDS_Installer_Package_Download.sh | |
Please enter your Jamf Pro server URL : https://server_name_here.jamfcloud.com | |
Please enter your Jamf Pro user account : username_goes_here | |
Please enter the password for the username_goes_here account: | |
Downloading Google_Chrome_121.0.6167.184.pkg to /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
################################################################################################################################################################# 100.0% | |
Google_Chrome_121.0.6167.184.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
Microsoft_Edge_122.0.2365.92.pkg found in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
Checking MD5 hash of Microsoft_Edge_122.0.2365.92.pkg in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR to verify match with Microsoft_Edge_122.0.2365.92.pkg on https://server_name_here.jamfcloud.com… | |
MD5 hash of Microsoft_Edge_122.0.2365.92.pkg in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR does not match Microsoft_Edge_122.0.2365.92.pkg on https://server_name_here.jamfcloud.com. | |
Deleting Microsoft_Edge_122.0.2365.92.pkg from /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
Downloading Microsoft_Edge_122.0.2365.92.pkg to /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
######################################################################################################################################### 100.0% | |
Microsoft_Edge_122.0.2365.92.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
Microsoft_Office_16.83.pkg found in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
Checking MD5 hash of Microsoft_Office_16.83.pkg in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR to verify match with Microsoft_Office_16.83.pkg on https://server_name_here.jamfcloud.com… | |
MD5 hash of Microsoft_Office_16.83.pkg in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR matches Microsoft_Office_16.83.pkg on https://server_name_here.jamfcloud.com. | |
Microsoft_Office_16.83.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
Microsoft_OneDrive_24.020.0128.pkg found in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
Checking MD5 hash of Microsoft_OneDrive_24.020.0128.pkg in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR to verify match with Microsoft_OneDrive_24.020.0128.pkg on https://server_name_here.jamfcloud.com… | |
MD5 hash of Microsoft_OneDrive_24.020.0128.pkg in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR matches Microsoft_OneDrive_24.020.0128.pkg on https://server_name_here.jamfcloud.com. | |
Microsoft_OneDrive_24.020.0128.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.R7v2rAecOR. | |
username@computername ~ % |
The scripts are available from GitHub at the following location:
]]>
Advisory: I strongly advise having Jamf’s Professional Services folks involved if you’re planning a migration like this. The reason is that, as of the current Jamf Pro 11.3.2 release, you can only have one cloud distribution point at a time. A migration like the one I performed will involve a cut-over process which includes having to re-upload your current distribution point’s installer packages to the new JCDS2 distribution point. This process also necessitates having good recent backups for the Jamf Pro instance in question.
With the assistance of Jamf’s Professional Services (particular thanks to Sepie Moinipanah, Leslie Helou and David Raabe for their support), the migration in my case went smoothly. Please see below for the process I followed to migrate from an AWS-hosted cloud distribution point to using a Jamf-hosted JCDS2 cloud distribution point:
Pre-requisites:
Getting the installers from your current distribution point:
There are several ways to get the installers from an AWS-hosted cloud distribution point. The method I chose was to use AWS’s command line tool to sync the contents of the S3 bucket used by the cloud distribution point to a local directory on my workstation:
API Client Role and API Client:
The jamfCPR wiki describes the necessary permissions needed to sync installer packages to a JCDS2 cloud distribution point, in the context of an API Client Role. As a result, I used an API Client Role and API Client in this case because the API Client Role permissions don’t always map one-to-one to the permissions available to Jamf Pro accounts and groups. Please see below for the permissions I set for my API Client Role:
Preparing for the distribution point migration:
1. Verify that you have the latest version of jamfCPR available.
Note: If the JCDS2 cloud distribution point you’re migrating to is located outside of the United States, make sure you’re using jamfCPR 5.x or later. jamfCPR 4.12 and earlier is not able to work with JCDS2 cloud distribution points which are hosted in AWS regions outside of the United States.
2. Verify that you have an API Client Role and API Client on the Jamf Pro server which has the correct permissions assigned.
3. Verify that you have a copy of all installers on your current AWS-hosted cloud distribution point stored on the same Mac that you have the jamfCPR app installed on.
4. Work with Jamf to make sure that a backup of your Jamf Pro service is made just prior to doing the migration.
Advisory about the Jamf Pro backup:
At this point, I want to stop for a moment and discuss why that backup of your Jamf Pro service is so important. The reason has to do with how AWS-hosted cloud distribution points are created. When you set one up, Jamf Pro will do the following:
As the Jamf Pro admin, you don’t get to choose anything in this process and you can’t select an existing S3 bucket. What this means is that the migration goes wrong and you need to revert back to your AWS-hosted cloud distribution point, the only way to do so is to roll back your Jamf Pro service to a point in time before the migration started. You will not be able to go back to using your existing AWS-hosted cloud distribution point without restoring from that backup because there is no way otherwise to have Jamf Pro use that existing AWS-hosted cloud distribution point.
If you try to go back otherwise, Jamf Pro will not use the existing AWS-hosted cloud distribution point. Instead, Jamf Pro will set up a new S3 bucket and CloudFront distribution and you will now have a brand-new and completely empty AWS-hosted cloud distribution point.
Running the migration:
1. Log into your Jamf Pro server as an admin user with all needed rights.
2. Verify that the Cloud Services connection is logged in and appears to be working properly.
3. Go to Settings: Server: Cloud distribution point.
4. Click the Test button and verify that your connection to the cloud distribution point is working correctly.
5. Install something from your Jamf Pro server and verify that installation is working correctly.
6. Verify in your policy logs that the installer is coming from an address which matches something similar to what’s shown below:
https://d2zft6agzhvlnv.cloudfront.net
7. In your Jamf Pro server, go to Settings: Server: Cloud distribution point.
Note: The next step is a point of no return, for reasons described above in the Advisory about the Jamf Pro backup section. Make sure a very recent Jamf Pro backup is available.
8. Select Jamf Cloud and click the Save icon.
9. Click the Test button and verify that your connection to the cloud distribution point is working correctly.
10. Open the jamfCPR app on your Mac.
11. Select the directory containing the downloaded copy of the installers from the existing AWS-hosted cloud distribution point.
12. Set up the connection to your Jamf Pro service, using the API Client ID and its associated Client ID Secret.
13. Click the List button in the jamfCPR app. You should now see a list of packages that your Jamf Pro service has, showing a status of different.
Note: To allow the migration process to go quickly for this blog post, I’m using dummy installer packages with a size of one kilobyte. The jamfCPR app should display the actual size of the installer packages in its application window when the installers are listed.
14. Select the packages you want to copy back from the downloaded copy of the installers to your Jamf Pro service.
15. Click the Replicate button.
You can monitor the replication process using the jamfCPR logs, which are available under the View menu using the Show Logs command.
16. Once replication is completed, the packages should appear as Availability Pending when you bring up information on the package in the Jamf Pro admin console.
You also won’t see any packages listed in the Cloud distribution point screen available via Settings: Server: Cloud distribution point.
This is normal and the package should already be available for installation. One way to verify that the packages are present in the JCDS2 distribution point is by downloading them from the JCDS2 distribution point. I have a post on how to do this available via the link below:
17. Install something from your Jamf Pro server and verify that installation is working correctly.
18. Verify in your policy logs that the installer is coming from an address which matches something similar to what’s shown below:
https://server_name_here.jamfcloud.com/jcds
Note: If you’re using a custom DNS name for your Jamf Cloud instance, it would appear similar to what’s shown below:
https://server_name_here.custom_domain.here/jcds
Post-migration:
After about an hour, the packages should appear listed in the Cloud distribution point screen available via Settings: Server: Cloud distribution point:
The Availability Pending message should also disappear when you view information about the installer package.
]]>For folks who don’t want the clock display, it’s possible to turn it off via System Settings. In the Lock Screen settings, set the Show large clock option to one of the following settings:
The Never setting will stop from being displayed on either the login window or as part of the screen saver.
For more information, please see below the jump.
For those who want a profile which can enforce the Never setting, please see below. It’s also available via the GitHub link below:
https://github.com/rtrouton/profiles/tree/main/DisableLoginWindowClock
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1"> | |
<dict> | |
<key>PayloadUUID</key> | |
<string>88268020-9123-40C7-8FEE-8505813ABCF2</string> | |
<key>PayloadType</key> | |
<string>Configuration</string> | |
<key>PayloadOrganization</key> | |
<string>Company Name</string> | |
<key>PayloadIdentifier</key> | |
<string>88268020-9123-40C7-8FEE-8505813ABCF2</string> | |
<key>PayloadDisplayName</key> | |
<string>Disable Login Window Clock</string> | |
<key>PayloadDescription</key> | |
<string /> | |
<key>PayloadVersion</key> | |
<integer>1</integer> | |
<key>PayloadEnabled</key> | |
<true /> | |
<key>PayloadRemovalDisallowed</key> | |
<true /> | |
<key>PayloadScope</key> | |
<string>System</string> | |
<key>PayloadContent</key> | |
<array> | |
<dict> | |
<key>PayloadDisplayName</key> | |
<string>Custom Settings</string> | |
<key>PayloadIdentifier</key> | |
<string>3E1EB1DB-34AF-4E59-B797-02991E2F6048</string> | |
<key>PayloadOrganization</key> | |
<string>Company Name</string> | |
<key>PayloadType</key> | |
<string>com.apple.ManagedClient.preferences</string> | |
<key>PayloadUUID</key> | |
<string>3E1EB1DB-34AF-4E59-B797-02991E2F6048</string> | |
<key>PayloadVersion</key> | |
<integer>1</integer> | |
<key>PayloadContent</key> | |
<dict> | |
<key>com.apple.loginwindow</key> | |
<dict> | |
<key>Forced</key> | |
<array> | |
<dict> | |
<key>mcx_preference_settings</key> | |
<dict> | |
<key>UsesLargeDateTime</key> | |
<false /> | |
</dict> | |
</dict> | |
</array> | |
</dict> | |
</dict> | |
</dict> | |
</array> | |
</dict> | |
</plist> |
Among these new API capabilities is the ability to query a JCDS 2 DP for download links, for the installer packages stored in the distribution point.
https://developer.jamf.com/jamf-pro/reference/get_v1-jcds-files-filename
I’m interested in this because I’ve had an existing workflow for downloading installer packages from a non-JCDS AWS-hosted cloud distribution point, where I’ve been using AWS’s awscli command line tool to run a one-way synchronization process between the cloud distribution point in Amazon’s S3 service and a local directory stored on a Mac.
For those who want to use this new capability, I’ve written a script which uses the Jamf Pro Classic API and Jamf Pro API to get the list of installer packages on a Jamf Pro server, retrieve the associated download links and download the installer packages to a directory on my Mac. For more details, please see below the jump.
Pre-requisites:
If setting up a specific Jamf Pro user account for this purpose with limited rights, here are the required API privileges for the account on the Jamf Pro server:
Jamf Pro Server Objects:
For authentication, the script can accept manual input or values stored in a ~/Library/Preferences/com.github.jamfpro-info.plist file. The plist file can be created by running the following commands and substituting your own values where appropriate:
To store the Jamf Pro URL in the plist file:
defaults write com.github.jamfpro-info jamfpro_url https://jamf.pro.server.goes.here:port_number_goes_here |
To store the account username in the plist file:
defaults write com.github.jamfpro-info jamfpro_user account_username_goes_here |
To store the account password in the plist file:
defaults write com.github.jamfpro-info jamfpro_password account_password_goes_here |
Usage:
/path/to/Jamf_Pro_JCDS_Installer_Package_Download.sh
The script takes the following actions:
The script should provide output similar to this:
username@computername ~ % /path/to/Jamf_Pro_JCDS_Installer_Package_Download.sh | |
A location to store downloaded installer packages has not been specified. | |
Downloaded installer packages will be stored in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
Please enter your Jamf Pro server URL : https://jamf.pro.server.here | |
Please enter your Jamf Pro user account : username_goes_here | |
Please enter the password for the username_goes_here account: | |
Downloading Google_Chrome_121.0.6167.184.pkg to /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
################################################################################################################################################# 100.0% | |
Downloading Google_Chrome_122.0.6261.57.pkg to /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
################################################################################################################################################# 100.0% | |
Downloading Google_Chrome_122.0.6261.69.pkg to /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
################################################################################################################################################# 100.0% | |
Microsoft_Defender_101.23122.0005.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
Microsoft_Edge_121.0.2277.128.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
Microsoft_Office_16.80.0.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
Microsoft_Office_16.81.2.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
Microsoft_Office_16.82.1.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
Microsoft_Office_16.82.pkg is available in /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
Downloading Microsoft_OneDrive_24.020.0128.pkg to /var/folders/vd/c27hl4p53j1_5cnv9ynpxp6m0000gn/T/tmp.EPeGBdNgVO. | |
################################################################################################################################################# 100.0% | |
username@computername ~ % |
This script is available below and also from GitHub at the following location:
#!/bin/bash | |
# This script is designed to download installer packages from a JCDS 2 distribution point. | |
# As part of that, it uses the Jamf Pro API to identify the individual IDs of | |
# the installer packages stored on a Jamf Pro server then do the following: | |
# | |
# 1. Download the package information as XML | |
# 2. Identify the installer package name from downloaded XML | |
# 3. Get the download URL for the installer package | |
# 4. Save the installer package to a specified directory | |
# If setting up a specific user account with limited rights, here are the required API privileges | |
# for the account on the Jamf Pro server: | |
# | |
# Jamf Pro Server Objects: | |
# | |
# Packages: Read | |
# Jamf Content Distribution Server Files: Read | |
# | |
# Set exit error status | |
ERROR=0 | |
# If you choose to specify a directory to save the downloaded installer packages into, | |
# please enter the complete directory path into the JCDSInstallerDownloadDirectory | |
# variable below. | |
JCDSInstallerDownloadDirectory="" | |
# If the JCDSInstallerDownloadDirectory isn't specified above, a directory will be | |
# created and the complete directory path displayed by the script. | |
if [[ -z "$JCDSInstallerDownloadDirectory" ]]; then | |
JCDSInstallerDownloadDirectory=$(mktemp -d) | |
echo "A location to store downloaded installer packages has not been specified." | |
echo "Downloaded installer packages will be stored in $JCDSInstallerDownloadDirectory." | |
fi | |
# If you choose to hardcode API information into the script, set one or more of the following values: | |
# | |
# The username for an account on the Jamf Pro server with sufficient API privileges | |
# The password for the account | |
# The Jamf Pro URL | |
# Set the Jamf Pro URL here if you want it hardcoded. | |
jamfpro_url="" | |
# Set the username here if you want it hardcoded. | |
jamfpro_user="" | |
# Set the password here if you want it hardcoded. | |
jamfpro_password="" | |
# If you do not want to hardcode API information into the script, you can also store | |
# these values in a ~/Library/Preferences/com.github.jamfpro-info.plist file. | |
# | |
# To create the file and set the values, run the following commands and substitute | |
# your own values where appropriate: | |
# | |
# To store the Jamf Pro URL in the plist file: | |
# defaults write com.github.jamfpro-info jamfpro_url https://jamf.pro.server.goes.here:port_number_goes_here | |
# | |
# To store the account username in the plist file: | |
# defaults write com.github.jamfpro-info jamfpro_user account_username_goes_here | |
# | |
# To store the account password in the plist file: | |
# defaults write com.github.jamfpro-info jamfpro_password account_password_goes_here | |
# | |
# If the com.github.jamfpro-info.plist file is available, the script will read in the | |
# relevant information from the plist file. | |
if [[ -f "$HOME/Library/Preferences/com.github.jamfpro-info.plist" ]]; then | |
if [[ -z "$jamfpro_url" ]]; then | |
jamfpro_url=$(defaults read $HOME/Library/Preferences/com.github.jamfpro-info jamfpro_url) | |
fi | |
if [[ -z "$jamfpro_user" ]]; then | |
jamfpro_user=$(defaults read $HOME/Library/Preferences/com.github.jamfpro-info jamfpro_user) | |
fi | |
if [[ -z "$jamfpro_password" ]]; then | |
jamfpro_password=$(defaults read $HOME/Library/Preferences/com.github.jamfpro-info jamfpro_password) | |
fi | |
fi | |
# If the Jamf Pro URL, the account username or the account password aren't available | |
# otherwise, you will be prompted to enter the requested URL or account credentials. | |
if [[ -z "$jamfpro_url" ]]; then | |
read -p "Please enter your Jamf Pro server URL : " jamfpro_url | |
fi | |
if [[ -z "$jamfpro_user" ]]; then | |
read -p "Please enter your Jamf Pro user account : " jamfpro_user | |
fi | |
if [[ -z "$jamfpro_password" ]]; then | |
read -p "Please enter the password for the $jamfpro_user account: " -s jamfpro_password | |
fi | |
echo "" | |
GetJamfProAPIToken() { | |
# This function uses Basic Authentication to get a new bearer token for API authentication. | |
# Use user account's username and password credentials with Basic Authorization to request a bearer token. | |
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then | |
api_token=$(/usr/bin/curl -X POST –silent -u "${jamfpro_user}:${jamfpro_password}" "${jamfpro_url}/api/v1/auth/token" | python -c 'import sys, json; print json.load(sys.stdin)["token"]') | |
else | |
api_token=$(/usr/bin/curl -X POST –silent -u "${jamfpro_user}:${jamfpro_password}" "${jamfpro_url}/api/v1/auth/token" | plutil -extract token raw -) | |
fi | |
} | |
APITokenValidCheck() { | |
# Verify that API authentication is using a valid token by running an API command | |
# which displays the authorization details associated with the current API user. | |
# The API call will only return the HTTP status code. | |
api_authentication_check=$(/usr/bin/curl –write-out %{http_code} –silent –output /dev/null "${jamfpro_url}/api/v1/auth" –request GET –header "Authorization: Bearer ${api_token}") | |
} | |
CheckAndRenewAPIToken() { | |
# Verify that API authentication is using a valid token by running an API command | |
# which displays the authorization details associated with the current API user. | |
# The API call will only return the HTTP status code. | |
APITokenValidCheck | |
# If the api_authentication_check has a value of 200, that means that the current | |
# bearer token is valid and can be used to authenticate an API call. | |
if [[ ${api_authentication_check} == 200 ]]; then | |
# If the current bearer token is valid, it is used to connect to the keep-alive endpoint. This will | |
# trigger the issuing of a new bearer token and the invalidation of the previous one. | |
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then | |
api_token=$(/usr/bin/curl "${jamfpro_url}/api/v1/auth/keep-alive" –silent –request POST –header "Authorization: Bearer ${api_token}" | python -c 'import sys, json; print json.load(sys.stdin)["token"]') | |
else | |
api_token=$(/usr/bin/curl "${jamfpro_url}/api/v1/auth/keep-alive" –silent –request POST –header "Authorization: Bearer ${api_token}" | plutil -extract token raw -) | |
fi | |
else | |
# If the current bearer token is not valid, this will trigger the issuing of a new bearer token | |
# using Basic Authentication. | |
GetJamfProAPIToken | |
fi | |
} | |
InvalidateToken() { | |
# Verify that API authentication is using a valid token by running an API command | |
# which displays the authorization details associated with the current API user. | |
# The API call will only return the HTTP status code. | |
APITokenValidCheck | |
# If the api_authentication_check has a value of 200, that means that the current | |
# bearer token is valid and can be used to authenticate an API call. | |
if [[ ${api_authentication_check} == 200 ]]; then | |
# If the current bearer token is valid, an API call is sent to invalidate the token. | |
authToken=$(/usr/bin/curl "${jamfpro_url}/api/v1/auth/invalidate-token" –silent –header "Authorization: Bearer ${api_token}" -X POST) | |
# Explicitly set value for the api_token variable to null. | |
api_token="" | |
fi | |
} | |
# Remove the trailing slash from the Jamf Pro URL if needed. | |
jamfpro_url=${jamfpro_url%%/} | |
# If configured to get one, get a Jamf Pro API Bearer Token | |
GetJamfProAPIToken | |
initializeJCDSInstallerDownloadDirectory () | |
{ | |
if [[ -z "$JCDSInstallerDownloadDirectory" ]]; then | |
JCDSInstallerDownloadDirectory=$(mktemp -d) | |
echo "A location to store downloaded installer packages has not been specified." | |
echo "Downloaded installer packages will be stored in $JCDSInstallerDownloadDirectory." | |
echo "$JCDSInstallerDownloadDirectory not found. Creating…" | |
mkdir -p $JCDSInstallerDownloadDirectory | |
if [[ $? -eq 0 ]]; then | |
echo "Successfully created $JCDSInstallerDownloadDirectory" | |
else | |
echo "Could not create $JCDSInstallerDownloadDirectory" | |
echo "Please make sure the parent directory is writable. Exiting…." | |
ERROR=1 | |
fi | |
else | |
# Remove the trailing slash from the JCDSInstallerDownloadDirectory variable if needed. | |
JCDSInstallerDownloadDirectory=${JCDSInstallerDownloadDirectory%%/} | |
if [[ -d "$JCDSInstallerDownloadDirectory" ]] && [[ -z "$(ls -A "$JCDSInstallerDownloadDirectory")" ]]; then | |
echo "$JCDSInstallerDownloadDirectory exists but is empty. Using existing directory for downloading installer packages." | |
elif [[ -n "$JCDSInstallerDownloadDirectory" ]] && [[ ! -d "$JCDSInstallerDownloadDirectory" ]]; then | |
echo "$JCDSInstallerDownloadDirectory does not exist. Creating $JCDSInstallerDownloadDirectory for downloading installer packages." | |
mkdir -p $JCDSInstallerDownloadDirectory | |
if [[ $? -eq 0 ]]; then | |
echo "Successfully created new $JCDSInstallerDownloadDirectory" | |
else | |
echo "Could not create new $JCDSInstallerDownloadDirectory" | |
echo "Please make sure the parent directory is writable. Exiting…." | |
ERROR=1 | |
fi | |
fi | |
fi | |
} | |
InstallerPackageDownloadURLRetrieval() { | |
# Replace spaces in filenames with %20, so that | |
# curl isn't trying to send a filename with spaces | |
# as part of an API command. | |
PackageNameSpacesSanitized=${PackageName// /%20} | |
# Retrieves a download URL for an installer package | |
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then | |
InstallerPackageURI=$(/usr/bin/curl -s –header "Authorization: Bearer ${api_token}" "${jamfpro_url}/api/v1/jcds/files/${PackageNameSpacesSanitized}" -H "Accept: application/json" | python -c 'import sys, json; print json.load(sys.stdin)["uri"]') | |
else | |
InstallerPackageURI=$(/usr/bin/curl -s –header "Authorization: Bearer ${api_token}" "${jamfpro_url}/api/v1/jcds/files/${PackageNameSpacesSanitized}" -H "Accept: application/json" | plutil -extract uri raw -) | |
fi | |
} | |
# The following function downloads individual Jamf Pro policy as XML data | |
# then mines the policy data for the relevant information, which are the | |
# download URLs for the packages stored in the JCDS distribution point. | |
# | |
# Once the download URLs are identified, the installer packages are then | |
# downloaded to the specified download directory. If there are installer | |
# packages already in the download directory which have the same name | |
# as an installer package in the JCDS distribution point, download of | |
# the installer package with the matching name is skipped. | |
DownloadInstallerPackages(){ | |
local InstallerPackageID="$1" | |
if [[ -n "$InstallerPackageID" ]]; then | |
CheckAndRenewAPIToken | |
local DownloadedXMLData=$(/usr/bin/curl -s –header "Authorization: Bearer ${api_token}" -H "Accept: application/xml" "${jamfpro_url}/JSSResource/packages/id/$InstallerPackageID") | |
local PackageName=$( echo "$DownloadedXMLData" | xmllint –xpath '/package/filename/text()' – 2>/dev/null) | |
# Download installer packages to the download directory | |
if [[ -n "$PackageName" ]]; then | |
InstallerPackageCheck=$(ls -a "$JCDSInstallerDownloadDirectory" | grep "$PackageName") | |
# Only download installer packages that haven't already been downloaded. | |
if [[ -z "$InstallerPackageCheck" ]]; then | |
echo "Downloading $PackageName to $JCDSInstallerDownloadDirectory." | |
InstallerPackageDownloadURLRetrieval | |
curl –progress-bar ${InstallerPackageURI} -X GET –output "${JCDSInstallerDownloadDirectory}"/"${PackageName}" | |
else | |
echo "$PackageName is available in $JCDSInstallerDownloadDirectory." | |
fi | |
fi | |
fi | |
} | |
initializeJCDSInstallerDownloadDirectory | |
if [[ $ERROR -eq 0 ]]; then | |
# Download all Jamf Pro installer package ID numbers | |
CheckAndRenewAPIToken | |
InstallerPackageIDList=$(/usr/bin/curl -s –header "Authorization: Bearer ${api_token}" -H "Accept: application/xml" "${jamfpro_url}/JSSResource/packages" | xmllint –xpath '//id' – 2>/dev/null) | |
InstallerPackageIDs=$(echo "$InstallerPackageIDList" | grep -Eo "[0-9]+") | |
for anID in ${InstallerPackageIDs}; do | |
# Download installer packages from the JCDS distribution point. | |
DownloadInstallerPackages $anID | |
done | |
fi | |
exit $ERROR |
A version of this script which uses Jamf’s API client authentication instead of a Jamf Pro user account is available from GitHub at the following location:
Both scripts can be accessed via the following link:
]]>For criteria #2, this references the fact that there are two kinds of modern installer packages for macOS:
By default, AutoPkg will build component packages using the PkgCreator processor or the AppPkgCreator processor. But there is a relatively straightforward way to create a a distribution package while using an existing component package as a source, using the productbuild command. To create a distribution installer package from an existing component installer package, you would use a command similar to the one shown below:
/usr/bin/productbuild –package /path/to/package_being_converted_to_distribution.pkg /path/to/new_distribution_package.pkg |
Note: If using a signed component installer package as a source, the resulting new distribution package will not be signed. If needed, you will need to sign the distribution package following its creation.
For those who want to create distribution packages as part of an AutoPkg workflow, I’ve written a DistributionPackageCreator AutoPkg processor which is designed to perform the following tasks:
For more details, please see below the jump.
The DistributionPackageCreator processor is shown below, as well as being available via the following link:
https://github.com/rtrouton/AutoPkg_Processors/tree/main/DistributionPackageCreator
#!/usr/local/autopkg/python | |
# | |
# Copyright 2010 Per Olofsson | |
# | |
# 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 plistlib | |
import subprocess | |
import os | |
from autopkglib import Processor, ProcessorError | |
__all__ = ["DistributionPackageCreator"] | |
class DistributionPackageCreator(Processor): | |
description = "Creates a distribution package from an existing component package using ProductBuild." | |
input_variables = { | |
"pkg_path": { | |
"required": True, | |
"description": "Path to the component package used to create the distribution package", | |
}, | |
} | |
output_variables = { | |
"pkg_path": {"description": "Path to the distribution package."} | |
} | |
__doc__ = description | |
def main(self): | |
# Rename component package so that we can retain the original name for the distribution package. | |
pkg_dir = os.path.dirname(self.env["pkg_path"]) | |
pkg_base_name = os.path.basename(self.env["pkg_path"]) | |
(pkg_name_no_extension, pkg_extension) = os.path.splitext(pkg_base_name) | |
component_pkg_path = os.path.join( | |
pkg_dir, pkg_name_no_extension + "-component" + pkg_extension | |
) | |
os.rename(self.env["pkg_path"], component_pkg_path) | |
command_line_list = [ | |
"/usr/bin/productbuild", | |
"–package", | |
component_pkg_path, | |
self.env["pkg_path"], | |
] | |
print(command_line_list) | |
# print command_line_list | |
subprocess.call(command_line_list) | |
if __name__ == "__main__": | |
processor = DistributionPackageCreator() | |
processor.execute_shell() |
Update – 2-5-2024: It turns out that both myself and @davidbpirie wrote practically identical AutoPkg processors. His processor is FlatToDistPkg (written in 2022) and it is available in his repo:
https://github.com/autopkg/davidbpirie-recipes/tree/main/SharedProcessors
When included in an AutoPkg recipe, the DistributionPackageCreator processor will locate AutoPkg-generated component packages by using the pkg_path variable and do the following:
Note: Setting the distribution package’s name to match the original component package’s name allows AutoPkg to continue to work with the distribution installer package.
To assist folks who want to use this processor, but don’t want to rewrite their existing .pkg recipes, I’ve written an example recipe to assist with this: the .distpkg recipe.
The .distpkg recipe uses the DistributionPackageCreator processor and is designed to be placed in the AutoPkg workflow between a .pkg recipe and whatever else came next. In this case, the .pkg recipe would be a parent recipe for the .distpkg recipe. In turn, the .distpkg recipe would be used as the parent recipe for whatever came next in the workflow.
A good example would be if you wanted to create a signed distribution package. In that case, you could combine a .pkg recipe, a .distpkg recipe and a .sign recipe into the same workflow to produce a signed distribution package, which should meet all the necessary requirements to install the package via an MDM command.
For those who want to use .distpkg recipes, there is an example recipe available via the link below:
https://github.com/autopkg/rtrouton-recipes/blob/master/SharedProcessors/Example.distpkg.recipe
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>Description</key> | |
<string>Creates a distribution package from an existing component package.</string> | |
<key>Identifier</key> | |
<string>com.github.distpkg.MyGreatApp</string> | |
<key>Input</key> | |
<dict> | |
</dict> | |
<key>ParentRecipe</key> | |
<string>com.github.pkg.MyGreatApp</string> | |
<key>Process</key> | |
<array> | |
<dict> | |
<key>Processor</key> | |
<string>com.github.rtrouton.SharedProcessors/DistributionPackageCreator</string> | |
<key>Arguments</key> | |
<dict> | |
<key>pkg_path</key> | |
<string>%RECIPE_CACHE_DIR%/%NAME%-%version%.pkg</string> | |
</dict> | |
</dict> | |
</array> | |
</dict> | |
</plist> |
If you want to use the DistributionPackageCreator processor hosted from my AutoPkg recipe repo, first verify that AutoPkg is installed on the Mac you’re using. Once verified, run the following command:
autopkg repo-add rtrouton-recipes |
In this example, there are going to be multiple AutoPkg recipes and support files referenced:
Important information:
A. The recipes as written assume the following:
B. You absolutely must create an AutoPkg override to work with these recipes. The download location, configuration for the installer choices XML file and other settings are not included in the AutoPkg recipes themselves and must be defined in the override.
C. The Cisco Secure Client disk image does not have a set address for download, so you will need to do one of the following:
D. To configure the installer choices XML file, you must designate what modules you want to include using ones and zeros in the AutoPkg override. By default, the .pkg recipe is configured to install all modules:
<key>CHOICE_VPN</key> | |
<string>1</string> | |
<key>CHOICE_WEBSECURITY</key> | |
<string>1</string> | |
<key>CHOICE_FIREAMP</key> | |
<string>1</string> | |
<key>CHOICE_DART</key> | |
<string>1</string> | |
<key>CHOICE_DUO</key> | |
<string>1</string> | |
<key>CHOICE_POSTURE</key> | |
<string>1</string> | |
<key>CHOICE_ISEPOSTURE</key> | |
<string>1</string> | |
<key>CHOICE_NVM</key> | |
<string>1</string> | |
<key>CHOICE_THOUSANDEYES</key> | |
<string>1</string> | |
<key>CHOICE_UMBRELLA</key> | |
<string>1</string> | |
<key>CHOICE_ZEROTRUST</key> | |
<string>1</string> |
To change this, change one to zero for the modules you don’t want to install. For example, the configuration below will configure the Secure Client installer to only install the Secure Client Umbrella module:
<key>CHOICE_VPN</key> | |
<string>0</string> | |
<key>CHOICE_WEBSECURITY</key> | |
<string>0</string> | |
<key>CHOICE_FIREAMP</key> | |
<string>0</string> | |
<key>CHOICE_DART</key> | |
<string>0</string> | |
<key>CHOICE_DUO</key> | |
<string>0</string> | |
<key>CHOICE_POSTURE</key> | |
<string>0</string> | |
<key>CHOICE_ISEPOSTURE</key> | |
<string>0</string> | |
<key>CHOICE_NVM</key> | |
<string>0</string> | |
<key>CHOICE_THOUSANDEYES</key> | |
<string>0</string> | |
<key>CHOICE_UMBRELLA</key> | |
<string>1</string> | |
<key>CHOICE_ZEROTRUST</key> | |
<string>0</string> |
E. If you do not want to have the VPN module installed or enabled, you will need to set the CHOICE_VPN and DISABLE_VPN settings in the recipe override. Please see below for an example:
<key>CHOICE_VPN</key> | |
<string>0</string> | |
<key>CHOICE_WEBSECURITY</key> | |
<string>0</string> | |
<key>CONTENT_XML</key> | |
<string>Put_escaped_XML_profile_text_into_AutoPkg_recipe_override</string> | |
<key>DISABLE_CUSTOMER_EXPERIENCE_FEEDBACK</key> | |
<string>false</string> | |
<key>DISABLE_VPN</key> | |
<string>true</string> |
In this example, the CHOICE_VPN setting is set to zero and the DISABLE_VPN setting is set to true.
F. These recipes allow you to hide the Cisco-provided Secure Client installers, so that your users will not be able to see them in the Finder. Like the installer choices selection, this can be set using ones and zeros in the AutoPkg override.
To hide, set the HIDE_UNINSTALLERS setting to one:
<key>HIDE_UNINSTALLERS</key> | |
<string>1</string> |
To not hide the uninstallers, set the HIDE_UNINSTALLERS setting to zero:
<key>HIDE_UNINSTALLERS</key> | |
<string>0</string> |
G. It’s possible to disable the customer feedback functionality through the installer. To set this to be disabled, set the DISABLE_CUSTOMER_EXPERIENCE_FEEDBACK setting to true.
<key>DISABLE_CUSTOMER_EXPERIENCE_FEEDBACK</key> | |
<string>true</string> |
To leave the customer feedback functionality enabled, set the DISABLE_CUSTOMER_EXPERIENCE_FEEDBACK setting to false.
<key>DISABLE_CUSTOMER_EXPERIENCE_FEEDBACK</key> | |
<string>false</string> |
Please see below for the example .download and .pkg recipes, example VPN XML file and example .pkg recipe override:
Download recipe:
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>Description</key> | |
<string>Downloads a Cisco Secure Client package from a specified URL.</string> | |
<key>Identifier</key> | |
<string>com.company.download.CiscoSecureClient</string> | |
<key>Input</key> | |
<dict> | |
<key>NAME</key> | |
<string>Cisco Secure Client</string> | |
<key>VENDOR</key> | |
<string>Cisco</string> | |
<key>DOWNLOAD_URL</key> | |
<string>Put_download_URL_into_AutoPkg_recipe_override</string> | |
</dict> | |
<key>MinimumVersion</key> | |
<string>1.0.0</string> | |
<key>Process</key> | |
<array> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>predicate</key> | |
<string>DOWNLOAD_URL == "Put_download_URL_into_AutoPkg_recipe_override"</string> | |
</dict> | |
<key>Processor</key> | |
<string>StopProcessingIf</string> | |
</dict> | |
<dict> | |
<key>Processor</key> | |
<string>URLDownloader</string> | |
<key>Arguments</key> | |
<dict> | |
<key>url</key> | |
<string>%DOWNLOAD_URL%</string> | |
<key>request_headers</key> | |
<dict> | |
<key>user-agent</key> | |
<string>%DOWNLOAD_USERAGENT%</string> | |
<key>referer</key> | |
<string>%DOWNLOAD_REFERER%</string> | |
</dict> | |
</dict> | |
</dict> | |
<dict> | |
<key>Processor</key> | |
<string>FlatPkgUnpacker</string> | |
<key>Arguments</key> | |
<dict> | |
<key>flat_pkg_path</key> | |
<string>%pathname%/Cisco Secure Client.pkg</string> | |
<key>destination_path</key> | |
<string>%RECIPE_CACHE_DIR%/%VENDOR%/unpack</string> | |
<key>purge_destination</key> | |
<true /> | |
</dict> | |
</dict> | |
<dict> | |
<key>Processor</key> | |
<string>PkgPayloadUnpacker</string> | |
<key>Arguments</key> | |
<dict> | |
<key>pkg_payload_path</key> | |
<string>%RECIPE_CACHE_DIR%/%VENDOR%/unpack/vpn_module.pkg/Payload</string> | |
<key>destination_path</key> | |
<string>%RECIPE_CACHE_DIR%/%VENDOR%/vpn_module_payload</string> | |
<key>purge_destination</key> | |
<true /> | |
</dict> | |
</dict> | |
<dict> | |
<key>Processor</key> | |
<string>Versioner</string> | |
<key>Arguments</key> | |
<dict> | |
<key>input_plist_path</key> | |
<string>%RECIPE_CACHE_DIR%/%VENDOR%/vpn_module_payload/Applications/Cisco/Cisco Secure Client.app/Contents/Info.plist</string> | |
<key>plist_version_key</key> | |
<string>CFBundleShortVersionString</string> | |
</dict> | |
</dict> | |
<dict> | |
<key>Processor</key> | |
<string>EndOfCheckPhase</string> | |
</dict> | |
</array> | |
</dict> | |
</plist> |
Package recipe:
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>Comments</key> | |
<string>Based on https://github.com/autopkg/grahampugh-recipes/blob/master/CiscoSecureClient/CiscoSecureClient.pkg.recipe | |
Wraps the vendor package so that it can be run with Choice Changes XML. | |
Requires a configuration xml profile (VPN_XML) which should be added to the RECIPE_OVERRIDE_DIR before running the recipe. An example is provided in the recipe repo folder, named example.xml. | |
If you need more packages installing, you should make a copy of this recipe and edit the ChoicesXMLGenerator choices list. This isn't overridable. Don't forget to change the Identifier if you do this.</string> | |
<key>Description</key> | |
<string>Creates a Cisco Secure Client package with all modules enabled.</string> | |
<key>Identifier</key> | |
<string>com.company.pkg.CiscoSecureClient-AllModulesEnabled</string> | |
<key>ParentRecipe</key> | |
<string>com.company.download.CiscoSecureClient</string> | |
<key>Input</key> | |
<dict> | |
<key>NAME</key> | |
<string>Cisco Secure Client</string> | |
<key>VPN_XML</key> | |
<string>example.xml</string> | |
<key>CONTENT_XML</key> | |
<string>Put_escaped_XML_profile_text_into_AutoPkg_recipe_override</string> | |
<key>UMBRELLA_ORGANIZATION_ID</key> | |
<string>Put_text_into_AutoPkg_recipe_override</string> | |
<key>UMBRELLA_FINGERPRINT</key> | |
<string>Put_text_into_AutoPkg_recipe_override</string> | |
<key>UMBRELLA_USER_ID</key> | |
<string>Put_text_into_AutoPkg_recipe_override</string> | |
<key>CHOICE_VPN</key> | |
<string>1</string> | |
<key>CHOICE_WEBSECURITY</key> | |
<string>1</string> | |
<key>CHOICE_FIREAMP</key> | |
<string>1</string> | |
<key>CHOICE_DART</key> | |
<string>1</string> | |
<key>CHOICE_DUO</key> | |
<string>1</string> | |
<key>CHOICE_POSTURE</key> | |
<string>1</string> | |
<key>CHOICE_ISEPOSTURE</key> | |
<string>1</string> | |
<key>CHOICE_NVM</key> | |
<string>1</string> | |
<key>CHOICE_THOUSANDEYES</key> | |
<string>1</string> | |
<key>CHOICE_UMBRELLA</key> | |
<string>1</string> | |
<key>CHOICE_ZEROTRUST</key> | |
<string>1</string> | |
<key>DISABLE_VPN</key> | |
<string>false</string> | |
<key>DISABLE_CUSTOMER_EXPERIENCE_FEEDBACK</key> | |
<string>false</string> | |
<key>HIDE_UNINSTALLERS</key> | |
<string>1</string> | |
<key>VENDOR</key> | |
<string>Cisco</string> | |
<key>SOFTWARETITLE1</key> | |
<string>Secure</string> | |
<key>SOFTWARETITLE2</key> | |
<string>Client</string> | |
<key>SOFTWARETITLE3</key> | |
<string>All_Modules_Enabled</string> | |
<key>PKGID</key> | |
<string>com.company.cisco.SecureClient</string> | |
</dict> | |
<key>MinimumVersion</key> | |
<string>1.0.0</string> | |
<key>Process</key> | |
<array> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>pkgdirs</key> | |
<dict> | |
<key>Profiles</key> | |
<string>0755</string> | |
<key>Profiles/ampenabler</key> | |
<string>0755</string> | |
<key>Profiles/feedback</key> | |
<string>0755</string> | |
<key>Profiles/iseposture</key> | |
<string>0755</string> | |
<key>Profiles/nvm</key> | |
<string>0755</string> | |
<key>Profiles/umbrella</key> | |
<string>0755</string> | |
<key>Profiles/vpn</key> | |
<string>0755</string> | |
<key>Profiles/websecurity</key> | |
<string>0755</string> | |
</dict> | |
<key>pkgroot</key> | |
<string>%RECIPE_CACHE_DIR%/Scripts</string> | |
</dict> | |
<key>Processor</key> | |
<string>PkgRootCreator</string> | |
</dict> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>pkgdirs</key> | |
<dict /> | |
<key>pkgroot</key> | |
<string>%RECIPE_CACHE_DIR%/pkgroot</string> | |
</dict> | |
<key>Processor</key> | |
<string>PkgRootCreator</string> | |
</dict> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>pkg_path</key> | |
<string>%RECIPE_CACHE_DIR%/Scripts/CiscoSecureClient.pkg</string> | |
<key>source_pkg</key> | |
<string>%pathname%/Cisco Secure Client.pkg</string> | |
</dict> | |
<key>Processor</key> | |
<string>PkgCopier</string> | |
</dict> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>file_content</key> | |
<string>%CONTENT_XML%</string> | |
<key>file_mode</key> | |
<string>0644</string> | |
<key>file_path</key> | |
<string>%RECIPE_CACHE_DIR%/Scripts/Profiles/vpn/%VPN_XML%</string> | |
</dict> | |
<key>Processor</key> | |
<string>FileCreator</string> | |
</dict> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>file_content</key> | |
<string>{ | |
"organizationId" : "%UMBRELLA_ORGANIZATION_ID%", | |
"fingerprint" : "%UMBRELLA_FINGERPRINT%", | |
"userId" : "%UMBRELLA_USER_ID%" | |
}</string> | |
<key>file_mode</key> | |
<string>0644</string> | |
<key>file_path</key> | |
<string>%RECIPE_CACHE_DIR%/Scripts/Profiles/umbrella/OrgInfo.json</string> | |
</dict> | |
<key>Processor</key> | |
<string>FileCreator</string> | |
</dict> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>file_content</key> | |
<string><?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<array> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_VPN%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_anyconnect_vpn</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_WEBSECURITY%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_websecurity</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_FIREAMP%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_fireamp</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_DART%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_dart</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_DUO%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_duo</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_POSTURE%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_secure_firewall_posture</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_ISEPOSTURE%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_iseposture</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_NVM%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_nvm</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_THOUSANDEYES%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_thousandeyes</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_UMBRELLA%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_secure_umbrella</string> | |
</dict> | |
<dict> | |
<key>attributeSetting</key> | |
<integer>%CHOICE_ZEROTRUST%</integer> | |
<key>choiceAttribute</key> | |
<string>selected</string> | |
<key>choiceIdentifier</key> | |
<string>choice_zta</string> | |
</dict> | |
</array> | |
</plist></string> | |
<key>file_mode</key> | |
<string>0755</string> | |
<key>file_path</key> | |
<string>%RECIPE_CACHE_DIR%/Scripts/InstallerChoices.xml</string> | |
</dict> | |
<key>Processor</key> | |
<string>FileCreator</string> | |
</dict> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>file_content</key> | |
<string><!– Optional SecureClient installer settings are provided below. Configure the setting(s) as "true" (default "false" or commented out) to perform optional action(s) at install time. –> | |
<Transforms> | |
<DisableVPN>%DISABLE_VPN%</DisableVPN> | |
<DisableCustomerExperienceFeedback>%DISABLE_CUSTOMER_EXPERIENCE_FEEDBACK%</DisableCustomerExperienceFeedback> | |
</Transforms></string> | |
<key>file_mode</key> | |
<string>0755</string> | |
<key>file_path</key> | |
<string>%RECIPE_CACHE_DIR%/Scripts/Profiles/ACTransforms.xml</string> | |
</dict> | |
<key>Processor</key> | |
<string>FileCreator</string> | |
</dict> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>file_content</key> | |
<string>#!/bin/bash | |
packagePath="${0%/*}/CiscoSecureClient.pkg" | |
choicesXML="${0%/*}/InstallerChoices.xml" | |
# Set value in AutoPkg override, default is 1 for "true" | |
hideUninstallers="%HIDE_UNINSTALLERS%" | |
# Array of uninstall application bundles on the installer's target volume | |
if [[ -n $3 ]]; then | |
targetVolume=$3 | |
else | |
targetVolume="" | |
fi | |
uninstallAppBundlePaths=( | |
"${targetVolume}/Applications/Cisco/Uninstall Cisco Secure Client – DART.app" | |
"${targetVolume}/Applications/Cisco/Uninstall Cisco Secure Client.app" | |
) | |
exitCode=0 | |
# Run embedded installer with the Choice Changes XML | |
if [[ -r "${packagePath}" && -r "${choicesXML}" ]]; then | |
if ! /usr/sbin/installer -applyChoiceChangesXML "${choicesXML}" -pkg "${packagePath}" -target "$3"; then | |
echo "Installation of package \"${packagePath}\" failed." | |
exitCode=1 | |
fi | |
else | |
echo "Package \"${packagePath}\" not found." | |
exitCode=1 | |
fi | |
# Hide uninstallers | |
# Override will specify 1 as "true" | |
# Set to another value to specify "false" | |
# If variable is unset/null or still contains an AutoPkg variable reference, treat as "true" | |
if [[ "${hideUninstallers}" -eq 1 || \ | |
-z "${hideUninstallers}" || \ | |
"${hideUninstallers}" == "%""HIDE_UNINSTALLERS""%" ]]; then | |
for uninstallAppBundlePath in "${uninstallAppBundlePaths[@]}" | |
do | |
if [[ -d "${uninstallAppBundlePath}" ]]; then | |
if /usr/bin/chflags hidden "${uninstallAppBundlePath}"; then | |
echo "Uninstaller app bundle \"${uninstallAppBundlePath}\" was hidden successfully." | |
else | |
echo "Uninstaller app bundle \"${uninstallAppBundlePath}\" was not hidden successfully." | |
exitCode=1 | |
fi | |
else | |
echo "Uninstaller app bundle \"${uninstallAppBundlePath}\" was not found and cannot be hidden." | |
fi | |
done | |
else | |
echo "Uninstallers will not be hidden." | |
fi | |
exit "${exitCode}"</string> | |
<key>file_mode</key> | |
<string>0755</string> | |
<key>file_path</key> | |
<string>%RECIPE_CACHE_DIR%/Scripts/postinstall</string> | |
</dict> | |
<key>Processor</key> | |
<string>FileCreator</string> | |
</dict> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>pkg_request</key> | |
<dict> | |
<key>chown</key> | |
<array /> | |
<key>id</key> | |
<string>%PKGID%.%VENDOR%%SOFTWARETITLE1%%SOFTWARETITLE2%</string> | |
<key>pkgname</key> | |
<string>%VENDOR%_%SOFTWARETITLE1%_%SOFTWARETITLE2%_%SOFTWARETITLE3%_%version%</string> | |
<key>pkgroot</key> | |
<string>%RECIPE_CACHE_DIR%/pkgroot</string> | |
<key>pkgtype</key> | |
<string>flat</string> | |
<key>scripts</key> | |
<string>%RECIPE_CACHE_DIR%/Scripts</string> | |
<key>version</key> | |
<string>%version%</string> | |
</dict> | |
</dict> | |
<key>Processor</key> | |
<string>PkgCreator</string> | |
</dict> | |
<dict> | |
<key>Arguments</key> | |
<dict> | |
<key>path_list</key> | |
<array> | |
<string>%RECIPE_CACHE_DIR%/%VENDOR%</string> | |
<string>%RECIPE_CACHE_DIR%/pkgroot</string> | |
</array> | |
</dict> | |
<key>Processor</key> | |
<string>PathDeleter</string> | |
</dict> | |
</array> | |
</dict> | |
</plist> |
Example VPN XML file:
<?xml version="1.0" encoding="UTF-8"?> | |
<AnyConnectProfile xmlns="http://schemas.xmlsoap.org/encoding/" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://schemas.xmlsoap.org/encoding/ AnyConnectProfile.xsd"> | |
<ClientInitialization> | |
<UseStartBeforeLogon UserControllable="true">false</UseStartBeforeLogon> | |
<AutomaticCertSelection UserControllable="false">true</AutomaticCertSelection> | |
<ShowPreConnectMessage>false</ShowPreConnectMessage> | |
<CertificateStore>All</CertificateStore> | |
<CertificateStoreOverride>true</CertificateStoreOverride> | |
<ProxySettings>Native</ProxySettings> | |
<AllowLocalProxyConnections>true</AllowLocalProxyConnections> | |
<AuthenticationTimeout>12</AuthenticationTimeout> | |
<AutoConnectOnStart UserControllable="true">false</AutoConnectOnStart> | |
<MinimizeOnConnect UserControllable="true">true</MinimizeOnConnect> | |
<LocalLanAccess UserControllable="true">true</LocalLanAccess> | |
<DisableCaptivePortalDetection UserControllable="false">false</DisableCaptivePortalDetection> | |
<ClearSmartcardPin UserControllable="false">true</ClearSmartcardPin> | |
<IPProtocolSupport>IPv4,IPv6</IPProtocolSupport> | |
<AutoReconnect UserControllable="false">true | |
<AutoReconnectBehavior UserControllable="false">DisconnectOnSuspend</AutoReconnectBehavior> | |
</AutoReconnect> | |
<AutoUpdate UserControllable="false">true</AutoUpdate> | |
<RSASecurIDIntegration UserControllable="false">Automatic</RSASecurIDIntegration> | |
<WindowsLogonEnforcement>SingleLocalLogon</WindowsLogonEnforcement> | |
<WindowsVPNEstablishment>LocalUsersOnly</WindowsVPNEstablishment> | |
<AutomaticVPNPolicy>false</AutomaticVPNPolicy> | |
<PPPExclusion UserControllable="false">Disable | |
<PPPExclusionServerIP UserControllable="false"></PPPExclusionServerIP> | |
</PPPExclusion> | |
<EnableScripting UserControllable="false">false</EnableScripting> | |
<EnableAutomaticServerSelection UserControllable="true">false | |
<AutoServerSelectionImprovement>20</AutoServerSelectionImprovement> | |
<AutoServerSelectionSuspendTime>4</AutoServerSelectionSuspendTime> | |
</EnableAutomaticServerSelection> | |
<RetainVpnOnLogoff>false</RetainVpnOnLogoff> | |
<AllowManualHostInput>true</AllowManualHostInput> | |
</ClientInitialization> | |
<ServerList> | |
<HostEntry> | |
<HostName>example.com/</HostName> | |
<HostAddress>example.com</HostAddress> | |
<UserGroup>example</UserGroup> | |
</HostEntry> | |
</ServerList> | |
</AnyConnectProfile> |
Example .pkg recipe override:
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>Identifier</key> | |
<string>local.pkg.CiscoSecureClient-AllModulesEnabled</string> | |
<key>Input</key> | |
<dict> | |
<key>CHOICE_DART</key> | |
<string>1</string> | |
<key>CHOICE_DUO</key> | |
<string>1</string> | |
<key>CHOICE_FIREAMP</key> | |
<string>1</string> | |
<key>CHOICE_ISEPOSTURE</key> | |
<string>1</string> | |
<key>CHOICE_NVM</key> | |
<string>1</string> | |
<key>CHOICE_POSTURE</key> | |
<string>1</string> | |
<key>CHOICE_THOUSANDEYES</key> | |
<string>1</string> | |
<key>CHOICE_UMBRELLA</key> | |
<string>1</string> | |
<key>CHOICE_VPN</key> | |
<string>1</string> | |
<key>CHOICE_WEBSECURITY</key> | |
<string>1</string> | |
<key>CHOICE_ZEROTRUST</key> | |
<string>1</string> | |
<key>CONTENT_XML</key> | |
<string><?xml version="1.0" encoding="UTF-8"?> | |
<AnyConnectProfile xmlns="http://schemas.xmlsoap.org/encoding/" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://schemas.xmlsoap.org/encoding/ AnyConnectProfile.xsd"> | |
<ClientInitialization> | |
<UseStartBeforeLogon UserControllable="true">false</UseStartBeforeLogon> | |
<AutomaticCertSelection UserControllable="false">true</AutomaticCertSelection> | |
<ShowPreConnectMessage>false</ShowPreConnectMessage> | |
<CertificateStore>All</CertificateStore> | |
<CertificateStoreOverride>true</CertificateStoreOverride> | |
<ProxySettings>Native</ProxySettings> | |
<AllowLocalProxyConnections>true</AllowLocalProxyConnections> | |
<AuthenticationTimeout>12</AuthenticationTimeout> | |
<AutoConnectOnStart UserControllable="true">false</AutoConnectOnStart> | |
<MinimizeOnConnect UserControllable="true">true</MinimizeOnConnect> | |
<LocalLanAccess UserControllable="true">true</LocalLanAccess> | |
<DisableCaptivePortalDetection UserControllable="false">false</DisableCaptivePortalDetection> | |
<ClearSmartcardPin UserControllable="false">true</ClearSmartcardPin> | |
<IPProtocolSupport>IPv4,IPv6</IPProtocolSupport> | |
<AutoReconnect UserControllable="false">true | |
<AutoReconnectBehavior UserControllable="false">DisconnectOnSuspend</AutoReconnectBehavior> | |
</AutoReconnect> | |
<AutoUpdate UserControllable="false">true</AutoUpdate> | |
<RSASecurIDIntegration UserControllable="false">Automatic</RSASecurIDIntegration> | |
<WindowsLogonEnforcement>SingleLocalLogon</WindowsLogonEnforcement> | |
<WindowsVPNEstablishment>LocalUsersOnly</WindowsVPNEstablishment> | |
<AutomaticVPNPolicy>false</AutomaticVPNPolicy> | |
<PPPExclusion UserControllable="false">Disable | |
<PPPExclusionServerIP UserControllable="false"></PPPExclusionServerIP> | |
</PPPExclusion> | |
<EnableScripting UserControllable="false">false</EnableScripting> | |
<EnableAutomaticServerSelection UserControllable="true">false | |
<AutoServerSelectionImprovement>20</AutoServerSelectionImprovement> | |
<AutoServerSelectionSuspendTime>4</AutoServerSelectionSuspendTime> | |
</EnableAutomaticServerSelection> | |
<RetainVpnOnLogoff>false</RetainVpnOnLogoff> | |
<AllowManualHostInput>true</AllowManualHostInput> | |
</ClientInitialization> | |
<ServerList> | |
<HostEntry> | |
<HostName>example.com/</HostName> | |
<HostAddress>example.com</HostAddress> | |
<UserGroup>example</UserGroup> | |
</HostEntry> | |
</ServerList> | |
</AnyConnectProfile></string> | |
<key>DISABLE_CUSTOMER_EXPERIENCE_FEEDBACK</key> | |
<string>false</string> | |
<key>DISABLE_VPN</key> | |
<string>false</string> | |
<key>DOWNLOAD_URL</key> | |
<string>https://company.com/cisco-secure-client-macos-5.1.1.42-predeploy-k9.dmg</string> | |
<key>HIDE_UNINSTALLERS</key> | |
<string>1</string> | |
<key>NAME</key> | |
<string>Cisco Secure Client</string> | |
<key>PKGID</key> | |
<string>com.company.cisco.SecureClient</string> | |
<key>SOFTWARETITLE1</key> | |
<string>Secure</string> | |
<key>SOFTWARETITLE2</key> | |
<string>Client</string> | |
<key>SOFTWARETITLE3</key> | |
<string>All_Modules_Enabled</string> | |
<key>UMBRELLA_FINGERPRINT</key> | |
<string>2a7145a02f0b4b9799695b224af3f6c3</string> | |
<key>UMBRELLA_ORGANIZATION_ID</key> | |
<string>7775762</string> | |
<key>UMBRELLA_USER_ID</key> | |
<string>65034259</string> | |
<key>VENDOR</key> | |
<string>Cisco</string> | |
<key>VPN_XML</key> | |
<string>example.xml</string> | |
</dict> | |
<key>ParentRecipe</key> | |
<string>com.company.pkg.CiscoSecureClient-AllModulesEnabled</string> | |
<key>ParentRecipeTrustInfo</key> | |
<dict> | |
<key>non_core_processors</key> | |
<dict/> | |
<key>parent_recipes</key> | |
<dict> | |
<key>com.company.download.CiscoSecureClient</key> | |
<dict> | |
<key>path</key> | |
<string>~/Library/AutoPkg/RecipeRepos/com.company.autopkg_recipes/CiscoSecureClient.download.recipe</string> | |
<key>sha256_hash</key> | |
<string>711dc16d406d7a0197e507cbb227e058e974276743bd9108b0b8146525e256f6</string> | |
</dict> | |
<key>com.company.pkg.CiscoSecureClient-AllModulesEnabled</key> | |
<dict> | |
<key>path</key> | |
<string>~/Library/AutoPkg/RecipeRepos/com.company.autopkg_recipes/CiscoSecureClient-AllModulesEnabled.pkg.recipe</string> | |
<key>sha256_hash</key> | |
<string>845152bc12c86d485f5712c4361339250bc93f621ed3d02da4b88c3807d4c99f</string> | |
</dict> | |
</dict> | |
</dict> | |
</dict> | |
</plist> |
The Jamf Pro Server Action privilege, “Enroll Computers and Mobile Devices”, was split into two separate privileges.
What this means is that in Jamf Pro versions before 11.2.0, you had one permission:
As of Jamf Pro 11.2.0, there are now two permissions:
The important thing to know is that as part of upgrading to Jamf Pro 11.2.0, the Enroll Computers and Mobile Devices permission is removed, but the following permissions are not selected automatically:
To address this issue, if needed go into the Jamf Pro admin console following the upgrade to Jamf Pro 11.2.0 and select the new separate Enroll Computers and Enroll Mobile Devices permissions.
]]>Desired behavior:
Actual behavior:
Unfortunately for my use case, I really needed to have the application in question prompt the user for which account they needed to log in with because a user account other than the one registered for single sign-on needed to be able to sign in to the application in question.
After some discussion in the #jamf-intune-integration channel in the Mac Admins Slack, I was pointed towards a way to sign out the account which was enabled for single sign-on using Microsoft’s Company Portal application. With no account enabled for single sign-on, the application would now prompt for a user account to sign in with. For more details, please see below the jump.
To sign out the Entra ID account enabled for single sign-on using the Company Portal application, please use the procedure described below:
1. Open the Company Portal application.
2. Sign into the Company Portal application as the user of the computer.
3. Under the Company Portal menu in the menubar, select Settings…
4. In the Settings window, in the Single sign-on (SSO) section, click the Remove account from this device button.
Note: I’ve noticed that clicking the Remove account from this device button doesn’t make a noticeable change in the Settings window; the account still appears as enabled. However, clicking the button should do what’s needed and applications should now prompt for a user account.
]]>https://www.motionbug.com/jamf-pro-and-macos-onboarding/
One of the things to be aware of with the new macOS Onboarding feature is that once the feature has been enabled, macOS Onboarding will run its associated policies on all Macs which don’t have the following user-level preference set:
Value set to FALSE (allowing macOS Onboarding to run):
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>com.jamfsoftware.selfservice.onboardingcomplete</key> | |
<false/> | |
</dict> | |
</plist> |
Value set to TRUE (blocking macOS Onboarding from running):
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>com.jamfsoftware.selfservice.onboardingcomplete</key> | |
<true/> | |
</dict> | |
</plist> |
Fortunately, it’s possible to add this setting with a value of TRUE to a macOS configuration profile and deploy the profile to all Macs that you don’t want to run macOS Onboarding on.
For those who would need this, I have an example macOS configuration profile with com.jamfsoftware.selfservice.onboardingcomplete set to TRUE available via the link below:
https://github.com/rtrouton/profiles/tree/main/JamfProSelfServiceOnboardingCompleted
]]>For those interested, all of the the JNUC 2023 session videos are available on YouTube, though it doesn’t appear that there’s a dedicated YouTube playlist at this point.
Update – 2023-01-03: There is now a dedicated YouTube playlist available.
For convenience, I’ve linked my session here.
]]>