├── .gitignore ├── API_Scripts ├── Jamf_API_Ceate_ConfigProfiles.sh ├── Jamf_API_Create_Buildings.sh ├── Jamf_API_Create_Categories.sh ├── Jamf_API_Create_Departments.sh ├── Jamf_API_Create_EA.sh ├── Jamf_API_Create_JSS_Accounts.sh ├── Jamf_API_Create_Policy.sh ├── Jamf_API_Create_Scripts.sh ├── Jamf_API_Create_SmartGroups.sh ├── Jamf_API_Delete_InventorySearch.sh ├── Jamf_API_Delete_Policy.sh ├── Jamf_API_Delete_Script.sh ├── Jamf_API_Delete_SmartGroup.sh ├── Jamf_API_Find_Policy.sh ├── Jamf_API_Get_ComputerSerial.sh ├── Jamf_API_Get_DeviceCount.sh ├── Jamf_API_Update_AccountPassword.sh ├── Jamf_API_Update_Script.sh └── Jamf_API_Upload_Package.sh ├── JamfProObjectBackup ├── README.md ├── SetComputerName ├── macOSUpgrades ├── macOSUpgrades_CheckCachedInstaller └── macOSUpgrades_SelfService /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /API_Scripts/Jamf_API_Ceate_ConfigProfiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script is designed to check if an Configuration Profile exists on a Jamf Pro Server and 4 | ## if not to upload a new one. 5 | ## Configuration Profiles can be uploaded by id to the following endpoint: 6 | ## /JSSResource//osxconfigurationprofiles/id/0 7 | 8 | ##Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 9 | 10 | ### When using the XML downloaded from the JSS we must strip the ID tags from the config profile 11 | ## and any categories associated with the config profile. We also must make sure any associated 12 | ## categories exist in the JSS first. 13 | 14 | ## Define Global Variables 15 | 16 | ## apiParms should be a path to a script that exports API user name and password so 17 | ## they don't have to be entered on the command line. 18 | ## Script should be in a format like the next 4 lines: 19 | ## #!/bin/bash 20 | ## 21 | ## apiUser='' 22 | ## apiPass='' 23 | apiParams="" 24 | 25 | ## If not using a script or file to set the user name or password define variables to get them from stdin 26 | ## JAMF Pro User with privileges to update the corresponding API endpoints 27 | apiUser="" 28 | ## Password for the user specified 29 | apiPass="" 30 | 31 | ##Get API variables. 32 | 33 | ## If the file containing the appropriate API Parameters does not exist or has a size 34 | ## of zero bytes then prompt the user for the necessary credentials. 35 | 36 | if [ ! -s "$apiParams" ] 37 | then 38 | read -r -p "Please enter a JAMF API administrator name: " apiUser 39 | read -r -s -p "Please enter the password for the account: " apiPass 40 | else 41 | ## Run the script found specified in apiParms to populate the variables 42 | ## Use dot notation so it says within the same process 43 | . "$apiParams" 44 | fi 45 | 46 | ## ARGV $1 is the path to the xml file of the Configuration Profile we want to we want to upload/create 47 | ## ARGV $2 is the list of JAMF instances. 48 | ## This instance name file MUST end with one blank lime otherwise errors in processing will occur. 49 | ## The file CANNOT contain windows line breaks otherwise sadness will happen. 50 | 51 | ## ARGV $1 52 | ############################################### 53 | ### IS IT BETTER TO USE THE FORMATTED CONFIGURATION PROFILE OR CURL THE UNFORMATTED ONE DIRECTLY FROM THE COMMAND LINE? 54 | ### THE JAMF SERVER EG: 55 | ### curl -k -u "$apiUser":"$apiPass" -H "accept: text/xml" https://your.jamfserver.url/JSSResource/osxconfigurationprofiles/id/INSERT ID NUMBER 56 | ### and pasting it tp a new XML file instead of copying the xml from the JAMF api page as doing it this way 57 | ### will will contain all the XML escape codes in the configuration profile. Without them, the XML file may fail to properly upload. 58 | ### Can remove the from the new file though as well as id references. 59 | 60 | if [ "$1" == "" ] 61 | then 62 | configXML="PATH TO XML FILE" 63 | else 64 | configXML="$1" 65 | fi 66 | 67 | if [ "$2" == "" ] 68 | then 69 | InstanceList="PATH TO Instance File" 70 | else 71 | InstanceList="$2" 72 | fi 73 | 74 | ## Search for the actual (Display) name of the configuration profile we are going to create by reading in the XML file for the configuration profile. 75 | ## We first grep for which is the opening tag or first line of the file which will return that line, which contains the 76 | ## name of the configuration profile we are looking for. Then using awk with or (which surround the configuration profile name) as 77 | ## field separators we extract the second field which is the name of the configuration profile. 78 | 79 | configName=$(cat "$configXML" | grep "" | awk -F "|" '{print $2}') 80 | 81 | ## Use a here block to Create an xslt template file that is used to format the XML 82 | ## outputted from the JSS to our standards in this case it will be used to produce 83 | ## a list of all the names of the Configuration Profiles in the JSS. 84 | ## We'll first match the top element of the list of Configuration Profiles returned from the JSS 85 | ## which is os_x_configuration_profiles. Then from each child element (os_x_configuration_profile>) 86 | ## we'll select the value of the name attribute and then add a line break. 87 | 88 | cat < /tmp/JSS_template.xslt 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | EOF 100 | 101 | ###Just echo a blank line for readability 102 | echo "" 103 | 104 | while read instanceName 105 | do 106 | 107 | ## Translate the instance name to all lower case to prevent any processing errors in the shell 108 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 109 | 110 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 111 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to express 112 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 113 | InstanceExists=$(curl --silent --head "$instanceName" | head -n 1) 114 | 115 | if [ "$InstanceExists" != "" ] 116 | then 117 | 118 | JSSResource="$instanceName/JSSResource" 119 | 120 | ## Strip the company name from the instance using // and - as field separators and 121 | ## return the 2 field which in the JSS Instance URL will be the company name. 122 | ## This will be used for logging purposes if necessary. 123 | CompanyName=$(echo $JSSResource | awk -F "//|-" '{print $2}') 124 | 125 | ## Using curl with the silent option to suppress extraneous output we'll 126 | ## output the full list of configuration profiles from the JSS to a file. 127 | ## The list of configuration profiles can be found at the /osxconfigurationprofiles endpoint 128 | ## We must use accept on the header since we are using a GET request 129 | ## AND we want the JAMF Server to return an XML output. 130 | 131 | curl \ 132 | -k \ 133 | -s \ 134 | -u "$apiUser":"$apiPass" \ 135 | -H "accept: text/xml" \ 136 | "$JSSResource/osxconfigurationprofiles" \ 137 | -X GET > /tmp/JSS_output.xml 138 | 139 | ## Since outputting the list of configuration profiles to a file just dumps the raw XML to the file 140 | ## it contains information that we might not want such as id number and other XML attributes. 141 | ## It also contains no formatting so using xsltproc we apply the template we created earlier 142 | ## and then output the formatted XML (which in this case is just the names of all the configuration profiles) 143 | ## to a text file. 144 | 145 | xsltproc /tmp/JSS_template.xslt /tmp/JSS_output.xml > /tmp/JSS_configList.txt 146 | 147 | ## Read in the previous text file line by line to see if it contains the name of the configuration profile 148 | ## we want to add. We check for a positive match one time only to avoid continual iterations of the 149 | ## loop to cause negative values to be reported. This is expected behavior but would throw off the results. 150 | 151 | while read name 152 | do 153 | if [ "$name" == "$configName" ] 154 | then 155 | configFound="Yes" 156 | fi 157 | done < /tmp/JSS_configList.txt 158 | 159 | ## If a positive match is never found then it means the configuration profile we want to create does not already exist 160 | ## so using curl we can POST a new configuration profile to the JSS instance. We send it to the 0 endpoint since this allows 161 | ## the JSS to create it at the next available slot. Also since we are send the curl command the location of an XML 162 | ## file in order to create the group we use the -T switch. If we were using just XML data or a variable with XML in 163 | ## it we would use the -d switch. 164 | 165 | if [ "$configFound" != "Yes" ] 166 | then 167 | curl \ 168 | -s \ 169 | -k \ 170 | -u "$apiUser":"$apiPass" \ 171 | -H "content-type: text/xml" \ 172 | $JSSResource/osxconfigurationprofiles/id/0 \ 173 | -T "$configXML" \ 174 | -X POST 175 | ## Use printf (more reliable than echo) to insert a mew line after the curl statement for readability because 176 | ## bash will prob put the following echo command on the same line as the curl output 177 | printf "\n" 178 | echo "Configuration Profile:$configName Created for $CompanyName" 179 | echo "" 180 | else 181 | echo "Configuration Profile *$configName* already exists for $CompanyName" 182 | echo "" 183 | fi 184 | 185 | ## Clean up temp files and variables after each iteration of the loop. 186 | ## While a new file will be created on each iteration it doesn't 187 | ## hurt to be careful and this will handle the last iteration. 188 | 189 | #rm /tmp/JSS_configList.txt 190 | #rm /tmp/JSS_output.xml 191 | configFound="" 192 | 193 | # End instance exists if statement 194 | else 195 | echo "$instanceName" 196 | echo "Instance is currently unreachable." 197 | echo "" 198 | 199 | fi 200 | 201 | done < "$InstanceList" 202 | 203 | #rm /tmp/JSS* 204 | -------------------------------------------------------------------------------- /API_Scripts/Jamf_API_Create_Buildings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script is designed to check if a Building exists on a Jamf Pro Server and 4 | ## if not to upload a new one. 5 | ## Buildings can be uploaded by id to the following endpoint: 6 | ## /JSSResource/buildings/id/0 7 | 8 | ##Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 9 | 10 | ## Define Global Variables 11 | 12 | ## apiParms should be a path to a script that exports API user name and password so 13 | ## they don't have to be entered on the command line. 14 | ## Script should be in a format like the next 4 lines: 15 | ## #!/bin/bash 16 | ## 17 | ## apiUser='' 18 | ## apiPass='' 19 | apiParams="" 20 | 21 | ## If not using a script or file to set the user name or password define variables to get them from stdin 22 | ## JAMF Pro User with privileges to update the corresponding API endpoints 23 | apiUser="" 24 | ## Password for the user specified 25 | apiPass="" 26 | 27 | ##Get API variables. 28 | 29 | ## If the file containing the appropriate API Parameters does not exist or has a size 30 | ## of zero bytes then prompt the user for the necessary credentials. 31 | 32 | if [ ! -s "$apiParams" ] 33 | then 34 | read -r -p "Please enter a JAMF API administrator name: " apiUser 35 | read -r -s -p "Please enter the password for the account: " apiPass 36 | else 37 | ## Run the script found specified in apiParms to populate the variables 38 | ## Use dot notation so it says within the same process 39 | . "$apiParams" 40 | fi 41 | 42 | ## ARGV $1 is the name of the Building (either singular or from a text file) we want to upload/create 43 | ## ARGV $2 is the list of JAMF instances. 44 | ## This instance name file MUST end with one blank lime otherwise errors in processing will occur. 45 | ## The file CANNOT contain windows line breaks otherwise sadness will happen. 46 | 47 | ## ARGV $1 48 | if [ "$1" == "" ] 49 | then 50 | BuildingNames="BUILDING NAMES" 51 | else 52 | BuildingNames="$1" 53 | fi 54 | 55 | if [ "$2" == "" ] 56 | then 57 | InstanceList="PATH TO Instance File" 58 | else 59 | InstanceList="$2" 60 | fi 61 | 62 | ## Use a here block to Create an xslt template file that is used to format the XML 63 | ## outputted from the JSS to our standards in this case it will be used to produce 64 | ## a list of all the names of the Buildings in the JSS. 65 | ## We'll first match the top element of the list of Buildings returned from the JSS 66 | ## which is buildings. Then from each child element (building) 67 | ## we'll select the value of the name attribute and then add a line break. 68 | 69 | cat < /tmp/JSS_template.xslt 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | EOF 81 | 82 | ###Just echo a blank line for readability 83 | echo "" 84 | 85 | while read instanceName 86 | do 87 | 88 | ## Translate the instance name to all lower case to prevent any processing errors in the shell 89 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 90 | 91 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 92 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to express 93 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 94 | InstanceExists=$(curl --silent --head "$instanceName" | head -n 1) 95 | 96 | if [ "$InstanceExists" != "" ] 97 | then 98 | 99 | JSSResource="$instanceName/JSSResource" 100 | 101 | ## Strip the company name from the instance using // and - as field separators and 102 | ## return the 2 field which in the JSS Instance URL will be the company name. 103 | ## This will be used for logging purposes if necessary. 104 | CompanyName=$(echo $JSSResource | awk -F "//|-" '{print $2}') 105 | 106 | ## Using curl with the silent option to suppress extraneous output we'll 107 | ## output the full list of buildings from the JSS to a file. 108 | ## The list of Buildings can be found at the /buildings endpoint 109 | ## We must use accept on the header since we are using a GET request 110 | ## AND we want the JAMF Server to return an XML output. 111 | 112 | curl \ 113 | -k \ 114 | -s \ 115 | -u "$apiUser":"$apiPass" \ 116 | -H "accept: text/xml" \ 117 | "$JSSResource/buildings" \ 118 | -X GET > /tmp/JSS_output.xml 119 | 120 | ## Since outputting the list of buildings to a file just dumps the raw XML to the file 121 | ## it contains information that we might not want such as id number and other XML attributes. 122 | ## It also contains no formatting so using xsltproc we apply the template we created earlier 123 | ## and then output the formatted XML (which in this case is just the names of all the buildings) 124 | ## to a text file. 125 | 126 | xsltproc /tmp/JSS_template.xslt /tmp/JSS_output.xml > /tmp/JSS_buildinglist.txt 127 | 128 | ## Read in the new buildings from the variable populated at the begging of the script. 129 | ## Either passed on the command line or defined in the script. 130 | ## Buildings can be listed in a text file line by line. Make sure the file has a blank line at the end 131 | ## and NO Windows line breaks. 132 | ## Then do a grep to compare each new building read in to the buildings in the JSS_buildinglist we created earlier. 133 | ## We use grep with the -e option to check for a supplied pattern (NewBuildingName) against the supplied input (JSS_buildinglist) 134 | ## The -q option runs grep in quiet mode to suppress extraneous output and the -i option so that pattern checking is not case sensitive. 135 | ## If grep does not find a match in the existing buildings with the new building its output code is 1 which we check with the $? variable 136 | ## If the output code is 1 we expect the building not to already exist and create it with the API call. 137 | ## We let the user know if a new building was created or found. 138 | 139 | while read NewBuildingName 140 | do 141 | grep -qi -e "$NewBuildingName" /tmp/JSS_buildinglist.txt 142 | if [ $? == 1 ] 143 | then 144 | curl \ 145 | -s \ 146 | -k \ 147 | -u "$apiUser":"$apiPass" \ 148 | -H "content-type: text/xml" \ 149 | $JSSResource/buildings/id/0 \ 150 | -d "$NewBuildingName" \ 151 | -X POST 152 | printf "\n" 153 | echo "Building:$NewBuildingName Created for $CompanyName" 154 | echo "" 155 | else 156 | echo "Building: *$NewBuildingName* already exists for $CompanyName" 157 | echo "" 158 | fi 159 | done < "$BuildingNames" 160 | 161 | ## Clean up temp files and variables after each iteration of the loop. 162 | ## While a new file will be created on each iteration it doesn't 163 | ## hurt to be careful and this will handle the last iteration. 164 | 165 | rm /tmp/JSS_buildinglist.txt 166 | rm /tmp/JSS_output.xml 167 | 168 | # End instance exists if statement 169 | else 170 | echo "$instanceName" 171 | echo "Instance is currently unreachable." 172 | echo "" 173 | 174 | fi 175 | 176 | done < "$InstanceList" 177 | 178 | rm /tmp/JSS* 179 | -------------------------------------------------------------------------------- /API_Scripts/Jamf_API_Create_Categories.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script is designed to check if a Category exists on a Jamf Pro Server and 4 | ## if not to upload a new one. 5 | ## Categories can be uploaded by id to the following endpoint: 6 | ## /JSSResource/categories/id/0 7 | 8 | ##Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 9 | 10 | ### When using the XML downloaded from the JSS we must strip the ID tags from the category 11 | ## and any categories associated with the category. We also must make sure any associated 12 | ## categories exist in the JSS first. 13 | 14 | ## Define Global Variables 15 | 16 | ## apiParms should be a path to a script that exports API user name and password so 17 | ## they don't have to be entered on the command line. 18 | ## Script should be in a format like the next 4 lines: 19 | ## #!/bin/bash 20 | ## 21 | ## apiUser='' 22 | ## apiPass='' 23 | apiParams="" 24 | 25 | ## If not using a script or file to set the user name or password define variables to get them from stdin 26 | ## JAMF Pro User with privileges to update the corresponding API endpoints 27 | apiUser="" 28 | ## Password for the user specified 29 | apiPass="" 30 | 31 | ##Get API variables. 32 | 33 | ## If the file containing the appropriate API Parameters does not exist or has a size 34 | ## of zero bytes then prompt the user for the necessary credentials. 35 | 36 | if [ ! -s "$apiParams" ] 37 | then 38 | read -r -p "Please enter a JAMF API administrator name: " apiUser 39 | read -r -s -p "Please enter the password for the account: " apiPass 40 | else 41 | ## Run the script found specified in apiParms to populate the variables 42 | ## Use dot notation so it says within the same process 43 | . "$apiParams" 44 | fi 45 | 46 | ## ARGV $1 is the path to the xml file of the Category we want to upload/create 47 | ## ARGV $2 is the list of JAMF instances. 48 | ## This instance name file MUST end with one blank lime otherwise errors in processing will occur. 49 | ## The file CANNOT contain windows line breaks otherwise sadness will happen. 50 | 51 | ## ARGV $1 52 | if [ "$1" == "" ] 53 | then 54 | CategoryXML="PATH TO XML FILE" 55 | else 56 | CategoryXML="$1" 57 | fi 58 | 59 | if [ "$2" == "" ] 60 | then 61 | InstanceList="PATH TO Instance File" 62 | else 63 | InstanceList="$2" 64 | fi 65 | 66 | ## Search for the actual (Display) name of the category we are going to create by reading in the XML file for the category. 67 | ## We first grep for which is the opening tag or first line of the file and return that and the 1 line below it 68 | ## which (2nd line) contains the name of the category. Then working on those 2 lines returned we use awk with or 69 | ## as the field separators which will return only the data between those tags which corresponds to the category's name. Since awk 70 | ## will not find any data on the first line we use NR==2 to tell awk to only return the 2nd line and just the second field from 71 | ## the returned data which is guaranteed to be the entire category name. 72 | ## In case there's any white space we use sub and a regular expression (^ ) to replace it with nothing "" 73 | 74 | CategoryName=$(cat "$CategoryXML" | grep -A 1 "" | awk -F "|" 'NR==2{sub(/^ /,"");print $2}') 75 | 76 | ## Use a here block to Create an xslt template file that is used to format the XML 77 | ## outputted from the JSS to our standards in this case it will be used to produce 78 | ## a list of all the names of the Categories in the JSS. 79 | ## We'll first match the top element of the list of Categories returned from the JSS 80 | ## which is categories. Then from each child element (category) 81 | ## we'll select the value of the name attribute and then add a line break. 82 | 83 | cat < /tmp/JSS_template.xslt 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | EOF 95 | 96 | ###Just echo a blank line for readability 97 | echo "" 98 | 99 | while read instanceName 100 | do 101 | 102 | ## Translate the instance name to all lower case to prevent any processing errors in the shell 103 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 104 | 105 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 106 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to express 107 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 108 | InstanceExists=$(curl --silent --head "$instanceName" | head -n 1) 109 | 110 | if [ "$InstanceExists" != "" ] 111 | then 112 | 113 | JSSResource="$instanceName/JSSResource" 114 | 115 | ## Strip the company name from the instance using // and - as field separators and 116 | ## return the 2 field which in the JSS Instance URL will be the company name. 117 | ## This will be used for logging purposes if necessary. 118 | CompanyName=$(echo $JSSResource | awk -F "//|-" '{print $2}') 119 | 120 | ## Using curl with the silent option to suppress extraneous output we'll 121 | ## output the full list of categories from the JSS to a file. 122 | ## The list of Categories can be found at the /categories endpoint 123 | ## We must use accept on the header since we are using a GET request 124 | ## AND we want the JAMF Server to return an XML output. 125 | 126 | curl \ 127 | -k \ 128 | -s \ 129 | -u "$apiUser":"$apiPass" \ 130 | -H "accept: text/xml" \ 131 | "$JSSResource/categories" \ 132 | -X GET > /tmp/JSS_output.xml 133 | 134 | ## Since outputting the list of Categories to a file just dumps the raw XML to the file 135 | ## it contains information that we might not want such as id number and other XML attributes. 136 | ## It also contains no formatting so using xsltproc we apply the template we created earlier 137 | ## and then output the formatted XML (which in this case is just the names of all the categories) 138 | ## to a text file. 139 | 140 | xsltproc /tmp/JSS_template.xslt /tmp/JSS_output.xml > /tmp/JSS_categorylist.txt 141 | 142 | ## Read in the previous text file line by line to see if it contains the name of the Category 143 | ## we want to add. We check for a positive match one time only to avoid continual iterations of the 144 | ## loop to cause negative values to be reported. This is expected behavior but would throw off the results. 145 | 146 | while read name 147 | do 148 | if [ "$name" == "$CategoryName" ] 149 | then 150 | CategoryFound="Yes" 151 | fi 152 | done < /tmp/JSS_categorylist.txt 153 | 154 | ## If a positive match is never found then it means the category we want to create does not already exist 155 | ## so using curl we can POST a new category to the JSS instance. We send it to the 0 endpoint since this allows 156 | ## the JSS to create it at the next available slot. Also since we are send the curl command the location of an XML 157 | ## file in order to create the group we use the -T switch. If we were using just XML data or a variable with XML in 158 | ## it we would use the -d switch. 159 | 160 | if [ "$CategoryFound" != "Yes" ] 161 | then 162 | curl \ 163 | -s \ 164 | -k \ 165 | -u "$apiUser":"$apiPass" \ 166 | -H "content-type: text/xml" \ 167 | $JSSResource/categories/id/0 \ 168 | -T "$CategoryXML" \ 169 | -X POST 170 | ## Use printf (more reliable than echo) to insert a mew line after the curl statement for readability because 171 | ## bash will prob put the following echo command on the same line as the curl output 172 | printf "\n" 173 | echo "Category:$CategoryName Created for $CompanyName" 174 | echo "" 175 | else 176 | echo "Category *$CategoryName* already exists for $CompanyName" 177 | echo "" 178 | fi 179 | 180 | ## Clean up temp files and variables after each iteration of the loop. 181 | ## While a new file will be created on each iteration it doesn't 182 | ## hurt to be careful and this will handle the last iteration. 183 | 184 | rm /tmp/JSS_categorylist.txt 185 | rm /tmp/JSS_output.xml 186 | CategoryFound="" 187 | 188 | # End instance exists if statement 189 | else 190 | echo "$instanceName" 191 | echo "Instance is currently unreachable." 192 | echo "" 193 | 194 | fi 195 | 196 | done < "$InstanceList" 197 | 198 | rm /tmp/JSS* 199 | -------------------------------------------------------------------------------- /API_Scripts/Jamf_API_Create_Departments.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script is designed to check if a Department exists on a JSS and 4 | ## if not to upload a new one. 5 | ## Departments can be uploaded by id to the following endpoint: 6 | ## /JSSResource/departments/id/0 7 | 8 | ##Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 9 | 10 | ### When using the XML downloaded from the JSS we must strip the ID tags from the department 11 | ## and any categories associated with the department. We also must make sure any associated 12 | ## categories exist in the JSS first. 13 | 14 | ## Define Global Variables 15 | 16 | ## apiParms should be a path to a script that exports API user name and password so 17 | ## they don't have to be entered on the command line. 18 | ## Script should be in a format like the next 4 lines: 19 | ## #!/bin/bash 20 | ## 21 | ## apiUser='' 22 | ## apiPass='' 23 | apiParams="" 24 | 25 | ## If not using a script or file to set the user name or password define variables to get them from stdin 26 | ## JAMF Pro User with privileges to update the corresponding API endpoints 27 | apiUser="" 28 | ## Password for the user specified 29 | apiPass="" 30 | 31 | ##Get API variables. 32 | 33 | ## If the file containing the appropriate API Parameters does not exist or has a size 34 | ## of zero bytes then prompt the user for the necessary credentials. 35 | 36 | if [ ! -s "$apiParams" ] 37 | then 38 | read -r -p "Please enter a JAMF API administrator name: " apiUser 39 | read -r -s -p "Please enter the password for the account: " apiPass 40 | else 41 | ## Run the script found specified in apiParms to populate the variables 42 | ## Use dot notation so it says within the same process 43 | . "$apiParams" 44 | fi 45 | 46 | ## ARGV $1 is the path to the xml file of the Department we want to upload/create 47 | ## ARGV $2 is the list of JAMF instances. 48 | ## This instance name file MUST end with one blank lime otherwise errors in processing will occuur. 49 | ## The file CANNOT contain windows line breaks otherwise sadness will happen. 50 | 51 | if [ "$1" == "" ] 52 | then 53 | DepartmentFile="PATH TO XML FILE" 54 | else 55 | DepartmentFile="$1" 56 | fi 57 | 58 | if [ "$2" == "" ] 59 | then 60 | InstanceList="PATH TO Instance File" 61 | else 62 | InstanceList="$2" 63 | fi 64 | 65 | ## Use a here block to Create an xslt template file that is used to format the XML 66 | ## outputted from the JSS to our standards in this case it will be used to produce 67 | ## a list of all the names of the Departments in the JSS. 68 | ## We'll first match the top element of the list of Departments returned from the JSS 69 | ## which is departments. Then from each child element (department) 70 | ## we'll select the value of the name attribute and then add a line break. 71 | 72 | cat < /tmp/JSS_template.xslt 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | EOF 84 | 85 | ###Just echo a blank line for readability 86 | echo "" 87 | 88 | while read instanceName 89 | do 90 | 91 | ## Translate the instance name to all lower case to prevent any processing errors in the shell 92 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 93 | 94 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 95 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to express 96 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 97 | InstanceExists=$(curl --silent --head "$instanceName" | head -n 1) 98 | 99 | if [ "$InstanceExists" != "" ] 100 | then 101 | 102 | JSSResource="$instanceName/JSSResource" 103 | 104 | ## Strip the company name from the instance using // and - as field separators and 105 | ## return the 2 field which in the JSS Instance URL will be the company name. 106 | ## This will be used for logging purposes if necessary. 107 | CompanyName=$(echo $JSSResource | awk -F "//|-" '{print $2}') 108 | 109 | ## Using curl with the silent option to suppress extraneous output we'll 110 | ## output the full list of departments from the JSS to a file. 111 | ## The list of Departments can be found at the /departments endpoint 112 | ## We must use accept on the header since we are using a GET request 113 | ## AND we want the JAMF Server to return an XML output. 114 | 115 | curl \ 116 | -k \ 117 | -s \ 118 | -u "$apiUser":"$apiPass" \ 119 | -H "accept: text/xml" \ 120 | "$JSSResource/departments" \ 121 | -X GET > /tmp/JSS_output.xml 122 | 123 | ## Since outputting the list of departments to a file just dumps the raw XML to the file 124 | ## it contains information that we might not want such as id number and other XML attributes. 125 | ## It also contains no formatting so using xsltproc we apply the template we created earlier 126 | ## and then output the formatted XML (which in this case is just the names of all the departments) 127 | ## to a text file. 128 | 129 | xsltproc /tmp/JSS_template.xslt /tmp/JSS_output.xml > /tmp/JSS_departmentlist.txt 130 | 131 | ## Read in the previous text file line by line to see if it contains the name of the department 132 | ## we want to add. We check for a positive match one time only to avoid continual iterations of the 133 | ## loop to cause negative values to be reported. This is expected behavior but would throw off the results. 134 | 135 | while read NewDepartmentName 136 | do 137 | grep -qi -e "$NewDepartmentName" /tmp/JSS_departmentlist.txt 138 | if [ $? == 1 ] 139 | then 140 | curl \ 141 | -s \ 142 | -k \ 143 | -u "$apiUser":"$apiPass" \ 144 | -H "content-type: text/xml" \ 145 | $JSSResource/departments/id/0 \ 146 | -d "$NewDepartmentName" \ 147 | -X POST 148 | printf "\n" 149 | echo "Department:$NewDepartmentName Created for $CompanyName" 150 | echo "" 151 | else 152 | echo "Department: *$NewDepartmentName* already exists for $CompanyName" 153 | echo "" 154 | fi 155 | done < "$DepartmentFile" 156 | 157 | ## Clean up temp files and variables after each iteration of the loop. 158 | ## While a new file will be created on each iteration it doesn't 159 | ## hurt to be careful and this will handle the last iteration. 160 | 161 | rm /tmp/JSS_departmentlist.txt 162 | rm /tmp/JSS_output.xml 163 | 164 | # End instance exists if statement 165 | else 166 | echo "$instanceName" 167 | echo "Instance is currently unreachable." 168 | echo "" 169 | 170 | fi 171 | 172 | done < "$InstanceList" 173 | 174 | rm /tmp/JSS* 175 | -------------------------------------------------------------------------------- /API_Scripts/Jamf_API_Create_EA.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script is designed to check if a Computer Extension Attribute exists on a Jamf Pro Server 4 | ## and if not to upload a new one. 5 | ## Computer Extension Attributes can be uploaded by id to the following endpoint: 6 | ## /JSSResource/computerextensionattributes/id/0 7 | 8 | ##Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 9 | 10 | ### When using the XML downloaded from the JSS we must strip the ID tags from the extension attribute 11 | ## and any categories associated with the extension attribute. We also must make sure any associated 12 | ## categories exist in the JSS first. 13 | 14 | ## Define Global Variables 15 | 16 | ## apiParms should be a path to a script that exports API user name and password so 17 | ## they don't have to be entered on the command line. 18 | ## Script should be in a format like the next 4 lines: 19 | ## #!/bin/bash 20 | ## 21 | ## apiUser='' 22 | ## apiPass='' 23 | 24 | apiParams="" 25 | 26 | ## If not using a script or file to set the user name or password define variables to get them from stdin 27 | 28 | ## JAMF Pro User with privileges to update the corresponding API endpoints 29 | apiUser="" 30 | ## Password for the user specified 31 | apiPass="" 32 | 33 | ##Get API variables. 34 | 35 | ## If the file containing the appropriate API Parameters does not exist or has a size 36 | ## of zero bytes then prompt the user for the necessary credentials. 37 | 38 | if [ ! -s "$apiParams" ] 39 | then 40 | read -r -p "Please enter a JAMF API administrator name: " apiUser 41 | read -r -s -p "Please enter the password for the account: " apiPass 42 | else 43 | ## Run the script found specified in apiParms to populate the variables 44 | ## Use dot notation so it says within the same process 45 | . "$apiParams" 46 | fi 47 | 48 | ## ARGV $1 is the path to the xml file of the Extension Attribute we want to upload/create 49 | ## ARGV $2 is the list of JAMF instances. 50 | ## This instance name file MUST end with one blank lime otherwise errors in processing will occuur. 51 | ## The file CANNOT contain windows line breaks otherwise sadness will happen. 52 | 53 | ## ARGV $1 54 | ############################################### 55 | ### BETTER TO CURL THE XML OF THE EXISTING EXTENSION ATTRIBUTE DIRECTLY FROM THE JAMF SERVER TO DEAL WITH SPECIAL CHARACTERS EG: 56 | ### curl -k -u "$apiUser":"$apiPass" -H "accept: text/xml" https://your.jamfserver.url/JSSResource/computerextensionattributes/id/INSERT ID NUMBER 57 | ### and pasting it tp a new XML file instead of copying the xml from the JAMF api page as doing it this way 58 | ### will will contain all the XML escape codes in the extension attribute. Without them, the XML file may fail to properly upload. 59 | ### Can remove the from the new file though as well as id references. 60 | 61 | if [ "$1" == "" ] 62 | then 63 | eaXML="PATH TO XML FILE" 64 | else 65 | eaXML="$1" 66 | fi 67 | 68 | if [ "$2" == "" ] 69 | then 70 | InstanceList="PATH TO Instance File" 71 | else 72 | InstanceList="$2" 73 | fi 74 | 75 | ## Search for the actual (Display) name of the extension attribute we are going to create by reading in the XML file for the extension attribute. 76 | ## We first grep for which is the opening tag or first line of the file which will return that line, which contains the 77 | ## name of the extension attribute we are looking for. Then using awk with or (which surround the extension attribute name) as 78 | ## field separators we extract the second field which is the name of the extension attribute. 79 | 80 | eaName=$(cat "$eaXML" | grep "" | awk -F "|" '{print $2}') 81 | 82 | ## Use a here block to Create an xslt template file that is used to format the XML 83 | ## outputted from the JSS to our standards in this case it will be used to produce 84 | ## a list of all the names of the Extension Attributes in the JSS. 85 | ## We'll first match the top element of the list of JSS Computer Extension Attributes returned from the JSS 86 | ## which is computer_extension_attributes. Then from each child element (computer_extension_attribute) 87 | ## we'll select the value of the name attribute and then add a line break. 88 | 89 | cat < /tmp/JSS_template.xslt 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | EOF 101 | 102 | ###Just echo a blank line for readability 103 | echo "" 104 | 105 | while read instanceName 106 | do 107 | 108 | ## Translate the instance name to all lower case to prevent any processing errors in the shell 109 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 110 | 111 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 112 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to suppress 113 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 114 | InstanceExists=$(curl --silent --head "$instanceName" | head -n 1) 115 | 116 | if [ "$InstanceExists" != "" ] 117 | then 118 | 119 | JSSResource="$instanceName/JSSResource" 120 | 121 | ## Strip the company name from the instance using // and - as field separators and 122 | ## return the 2 field which in the JSS Instance URL will be the company name. 123 | ## This will be used for logging purposes if necessary. 124 | CompanyName=$(echo $JSSResource | awk -F "//|-" '{print $2}') 125 | 126 | ## Using curl with the silent option to suppress extraneous output we'll 127 | ## output the full list of accounts from the JSS to a file. 128 | ## The list of Extension attributes can be found at the /computerextensionattributes endpoint 129 | ## We must use accept on the header since we are using a GET request 130 | ## AND we want the JAMF Server to return an XML output. 131 | 132 | curl \ 133 | -k \ 134 | -s \ 135 | -u "$apiUser":"$apiPass" \ 136 | -H "accept: text/xml" \ 137 | "$JSSResource/computerextensionattributes" \ 138 | -X GET > /tmp/JSS_output.xml 139 | 140 | ## Since outputting the list of extension attributes to a file just dumps the raw XML to the file 141 | ## it contains information that we might not want such as id number and other XML attributes. 142 | ## It also contains no formatting so using xsltproc we apply the template we created earlier 143 | ## and then output the formatted XML (which in this case is just the names of all the categories) 144 | ## to a text file. 145 | 146 | xsltproc /tmp/JSS_template.xslt /tmp/JSS_output.xml > /tmp/JSS_eaList.txt 147 | 148 | ## Read in the previous text file line by line to see if it contains the name of the extension attribute 149 | ## we want to add. We check for a positive match one time only to avoid continual iterations of the 150 | ## loop to cause negative values to be reported. This is expected behavior but would throw off the results. 151 | 152 | while read name 153 | do 154 | if [ "$name" == "$eaName" ] 155 | then 156 | eaFound="Yes" 157 | fi 158 | done < /tmp/JSS_eaList.txt 159 | 160 | ## If a positive match is never found then it means the extension attribute we want to create does not already exist 161 | ## so using curl we can POST a new extension attribute to the JSS instance. We send it to the 0 endpoint since this allows 162 | ## the JSS to create it at the next available slot. Also since we are send the curl command the location of an XML 163 | ## file in order to create the extension attribute we use the -T switch. If we were using just XML data or a variable with XML in 164 | ## it we would use the -d switch. 165 | 166 | if [ "$eaFound" != "Yes" ] 167 | then 168 | curl \ 169 | -s \ 170 | -k \ 171 | -u "$apiUser":"$apiPass" \ 172 | -H "content-type: text/xml" \ 173 | $JSSResource/computerextensionattributes/id/0 \ 174 | -T "$eaXML" \ 175 | -X POST 176 | ## Use printf (more reliable than echo) to insert a mew line after the curl statement for readability because 177 | ## bash will prob put the following echo command on the same line as the curl output 178 | printf "\n" 179 | echo "Extension Attribute:$eaName Created for $CompanyName" 180 | echo "" 181 | else 182 | echo "Extension Attribute *$eaName* already exists for $CompanyName" 183 | echo "" 184 | fi 185 | 186 | ## Clean up temp files and variables after each iteration of the loop. 187 | ## While a new file will be created on each iteration it doesn't 188 | ## hurt to be careful and this will handle the last iteration. 189 | 190 | rm /tmp/JSS_eaList.txt 191 | rm /tmp/JSS_output.xml 192 | eaFound="" 193 | 194 | # End instance exists if statement 195 | else 196 | echo "$instanceName" 197 | echo "Instance is currently unreachable." 198 | echo "" 199 | 200 | fi 201 | 202 | done < "$InstanceList" 203 | 204 | rm /tmp/JSS* 205 | -------------------------------------------------------------------------------- /API_Scripts/Jamf_API_Create_JSS_Accounts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script is designed to check if a JSS User Account exists on a Jamf Pro Server and 4 | ## if not to upload a new one. 5 | ## User Accounts can be uploaded by id to the following endpoint: 6 | ## /JSSResource/accounts/userid/0 7 | ## ################################################# ## 8 | ## THIS SCRIPT WILL NOT CREATE A VALID PASSWORD HASH ## 9 | ## THIS IS A LIMITATION OF THE CLASSIC JAMF PRO API ## 10 | ## ################################################# ## 11 | 12 | ## Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 13 | 14 | ### When using the XML downloaded from the JSS we must strip the ID tags from the account 15 | ## and any categories associated with the account. We also must make sure any associated 16 | ## categories exist in the JSS first. 17 | 18 | ## Define Global Variables 19 | 20 | ## apiParms should be a path to a script that exports API user name and password so 21 | ## they don't have to be entered on the command line. 22 | ## Script should be in a format like the next 4 lines: 23 | ## #!/bin/bash 24 | ## 25 | ## apiUser='' 26 | ## apiPass='' 27 | apiParams="" 28 | 29 | ## If not using a script or file to set the user name or password define variables to get them from stdin 30 | ## JAMF Pro User with privileges to update the corresponding API endpoints 31 | apiUser="" 32 | ## Password for the user specified 33 | apiPass="" 34 | 35 | ##Get API variables. 36 | 37 | ## If the file containing the appropriate API Parameters does not exist or has a size 38 | ## of zero bytes then prompt the user for the necessary credentials. 39 | 40 | if [ ! -s "$apiParams" ] 41 | then 42 | read -r -p "Please enter a JAMF API administrator name: " apiUser 43 | read -r -s -p "Please enter the password for the account: " apiPass 44 | else 45 | ## Run the script found specified in apiParms to populate the variables 46 | ## Use dot notation so it says within the same process 47 | . "$apiParams" 48 | fi 49 | 50 | ## ARGV $1 is the path to the XML file of the Account we want to upload/create 51 | ## ARGV $2 is the list of JAMF instances. 52 | ## This instance name file MUST end with one blank lime otherwise errors in processing will occuur. 53 | ## The file CANNOT contain windows line breaks otherwise sadness will happen. 54 | 55 | ## ARGV $1 56 | ############################################### 57 | ### BETTER TO CURL THE XML OF THE EXISTING ACCOUNT DIRECTLY FROM THE JAMF SERVER TO DEAL WITH SPECIAL CHARACTERS EG: 58 | ### curl -k -u "$apiUser":"$apiPass" -H "accept: text/xml" https://your.jamfserver.url/JSSResource/accounts/userid/id/INSERT ID NUMBER 59 | ### and pasting it tp a new XML file instead of copying the xml from the JAMF api page as doing it this way 60 | ### will will contain all the XML escape codes in the account. Without them, the XML file may fail to properly upload. 61 | ### Can remove the from the new file though as well as id references. 62 | 63 | if [ "$1" == "" ] 64 | then 65 | accountXML="PATH TO XML FILE" 66 | else 67 | accountXML="$1" 68 | fi 69 | 70 | if [ "$2" == "" ] 71 | then 72 | InstanceList="PATH TO Instance File" 73 | else 74 | InstanceList="$2" 75 | fi 76 | 77 | ### Search for the actual (Display) name of the account we are going to create by reading in the XML file for the account. 78 | ### We first grep for which is the opening tag or first line of the file which will return that line, which contains the name of the 79 | ### account we are looking for. 80 | ### Then working on the line returned we use awk with or as the field separators which will return only the data between those 81 | ### tags which corresponds to the account's name. Since in this case awk will find any data on the first line and 82 | ### return a blank line below it we use NR==1 to tell awk to only return the 1st line and just the second field from 83 | ### the returned data which is guaranteed to be the entire account name. 84 | ### In case there's any white space we use sub and a regular expression (^ ) to replace it with nothing "" 85 | ### Since we are using raw XML we use NR==1 if we switch to formatted XML it would be NR==2 86 | 87 | accountName=$(cat "$accountXML" | grep -A 1 "" | awk -F "|" 'NR==2{sub(/^ /,"");print $2}') 88 | 89 | ## Use a here block to Create an xslt template file that is used to format the XML 90 | ## outputted from the JSS to our standards in this case it will be used to produce 91 | ## a list of all the names of the Accounts in the JSS. 92 | ## We'll first match the top element of the list of JSS User Accounts returned from the JSS 93 | ## which is accounts. Then from each child element (users) 94 | ## we need to find its child element (user) which we can do in one line with users/user 95 | ## and then we'll select the value of the name attribute and then add a line break. 96 | 97 | cat < /tmp/JSS_template.xslt 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | EOF 109 | 110 | ###Just echo a blank line for readability 111 | echo "" 112 | 113 | while read instanceName 114 | do 115 | 116 | ## Translate the instance name to all lower case to prevent any processing errors in the shell 117 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 118 | 119 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 120 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to express 121 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 122 | InstanceExists=$(curl --silent --head "$instanceName" | head -n 1) 123 | 124 | if [ "$InstanceExists" != "" ] 125 | then 126 | 127 | JSSResource="$instanceName/JSSResource" 128 | 129 | ## Strip the company name from the instance using // and - as field separators and 130 | ## return the 2 field which in the JSS Instance URL will be the company name. 131 | ## This will be used for logging purposes if necessary. 132 | CompanyName=$(echo $JSSResource | awk -F "//|-" '{print $2}') 133 | 134 | ## Using curl with the silent option to suppress extraneous output we'll 135 | ## output the full list of accounts from the JSS to a file. 136 | ## The list of Extension attributes can be found at the /accounts endpoint 137 | ## We must use accept on the header since we are using a GET request 138 | ## AND we want the JAMF Server to return an XML output. 139 | 140 | curl \ 141 | -k \ 142 | -s \ 143 | -u "$apiUser":"$apiPass" \ 144 | -H "accept: text/xml" \ 145 | "$JSSResource/accounts" \ 146 | -X GET > /tmp/JSS_output.xml 147 | 148 | ## Since outputting the list of accounts to a file just dumps the raw XML to the file 149 | ## it contains information that we might not want such as id number and other XML attributes. 150 | ## It also contains no formatting so using xsltproc we apply the template we created earlier 151 | ## and then output the formatted XML (which in this case is just the names of all the categories) 152 | ## to a text file. 153 | 154 | xsltproc /tmp/JSS_template.xslt /tmp/JSS_output.xml > /tmp/JSS_Accountlist.txt 155 | 156 | ## Read in the previous text file line by line to see if it contains the name of the account 157 | ## we want to add. We check for a positive match one time only to avoid continual iterations of the 158 | ## loop to cause negative values to be reported. This is expected behavior but would throw off the results. 159 | 160 | while read name 161 | do 162 | if [ "$name" == "$accountName" ] 163 | then 164 | accountFound="Yes" 165 | fi 166 | done < /tmp/JSS_Accountlist.txt 167 | 168 | ## If a positive match is never found then it means the account we want to create does not already exist 169 | ## so using curl we can POST a new account to the JSS instance. We send it to the 0 endpoint since this allows 170 | ## the JSS to create it at the next available slot. Also since we are send the curl command the location of an XML 171 | ## file in order to create the group we use the -T switch. If we were using just XML data or a variable with XML in 172 | ## it we would use the -d switch. 173 | 174 | if [ "$accountFound" != "Yes" ] 175 | then 176 | curl \ 177 | -s \ 178 | -k \ 179 | -u "$apiUser":"$apiPass" \ 180 | -H "content-type: text/xml" \ 181 | $JSSResource/accounts/userid/0 \ 182 | -T "$accountXML" \ 183 | -X POST 184 | ## Use printf (more reliable than echo) to insert a mew line after the curl statement for readability because 185 | ## bash will prob put the following echo command on the same line as the curl output 186 | printf "\n" 187 | echo "Account:$accountName Created for $CompanyName" 188 | echo "" 189 | else 190 | echo "Account *$accountName* already exists for $CompanyName" 191 | echo "" 192 | fi 193 | 194 | ## Clean up temp files and variables after each iteration of the loop. 195 | ## While a new file will be created on each iteration it doesn't 196 | ## hurt to be careful and this will handle the last iteration. 197 | 198 | rm /tmp/JSS_Accountlist.txt 199 | rm /tmp/JSS_output.xml 200 | accountFound="" 201 | 202 | # End instance exists if statement 203 | else 204 | echo "$instanceName" 205 | echo "Instance is currently unreachable." 206 | echo "" 207 | 208 | fi 209 | 210 | done < "$InstanceList" 211 | 212 | rm /tmp/JSS* 213 | -------------------------------------------------------------------------------- /API_Scripts/Jamf_API_Create_Policy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script is designed to check if a Policy exists on a Jamf Pro Server and 4 | ## if not to upload a new one. 5 | ## Policies can be uploaded by id to the following endpoint: 6 | ## /JSSResource/policies/id/0 7 | 8 | ##Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 9 | 10 | ### When using the XML downloaded from the JSS we must strip the ID tags from the policy 11 | ## and any categories associated with the policy. We also must make sure any associated 12 | ## categories exist in the JSS first. 13 | 14 | ## Define Global Variables 15 | 16 | ## apiParms should be a path to a script that exports API user name and password so 17 | ## they don't have to be entered on the command line. 18 | ## Script should be in a format like the next 4 lines: 19 | ## #!/bin/bash 20 | ## 21 | ## apiUser='' 22 | ## apiPass='' 23 | apiParams="" 24 | 25 | ## If not using a script or file to set the user name or password define variables to get them from stdin 26 | ## JAMF Pro User with privileges to update the corresponding API endpoints 27 | apiUser="" 28 | ## Password for the user specified 29 | apiPass="" 30 | 31 | ##Get API variables. 32 | 33 | ## If the file containing the appropriate API Parameters does not exist or has a size 34 | ## of zero bytes then prompt the user for the necessary credentials. 35 | 36 | if [ ! -s "$apiParams" ] 37 | then 38 | read -r -p "Please enter a JAMF API administrator name: " apiUser 39 | read -r -s -p "Please enter the password for the account: " apiPass 40 | else 41 | ## Run the script found specified in apiParms to populate the variables 42 | ## Use dot notation so it says within the same process 43 | . "$apiParams" 44 | fi 45 | 46 | ## ARGV $1 is the path to the xml file of the Policy we want to upload/create 47 | ## ARGV $2 is the list of JAMF instances. 48 | ## This instance name file MUST end with one blank lime otherwise errors in processing will occuur. 49 | ## The file CANNOT contain windows line breaks otherwise sadness will happen. 50 | 51 | ## ARGV $1 52 | if [ "$1" == "" ] 53 | then 54 | PolicyXML="PATH TO XML FILE" 55 | else 56 | PolicyXML="$1" 57 | fi 58 | 59 | if [ "$2" == "" ] 60 | then 61 | InstanceList="PATH TO Instance File" 62 | else 63 | InstanceList="$2" 64 | fi 65 | 66 | ## Search fir the actual (Display) name of the policy we are going to create by reading in the XML file for the policy. 67 | ## We first grep for which is the opening tag or first line of the file and return that and the 2 lines below it 68 | ## the last of which (3rd line) contains the name of the policy. Then working on those 3 lines returned we use awk with or 69 | ## as the field separators which will return only the data between those tags which corresponds to the policy's name. Since awk 70 | ## will not find any data on the first 2 lines we use NR==3 to tell awk to only return the 3rd line and just the second field from 71 | ## the returned data which is guaranteed to be the entire policy name. 72 | ## In case there's any white space we use sub and a regular expression (^ ) to replace it with nothing "" 73 | 74 | PolicyName=$(cat "$PolicyXML" | grep -A 2 "" | awk -F "|" 'NR==3{sub(/^ /,"");print $2}') 75 | 76 | ## Use a here block to Create an xslt template file that is used to format the XML 77 | ## outputted from the JSS to our standards in this case it will be used to produce 78 | ## a list of all the names of the Policies in the JSS. 79 | ## We'll first match the top element of the list of Policies returned from the JSS 80 | ## which is policies. Then from each child element (policy) 81 | ## we'll select the value of the name attribute and then add a line break. 82 | 83 | cat < /tmp/JSS_template.xslt 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | EOF 95 | 96 | ###Just echo a blank line for readability 97 | echo "" 98 | 99 | while read instanceName 100 | do 101 | 102 | ## Translate the instance name to all lower case to prevent any processing errors in the shell 103 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 104 | 105 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 106 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to express 107 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 108 | InstanceExists=$(curl --silent --head "$instanceName" | head -n 1) 109 | 110 | if [ "$InstanceExists" != "" ] 111 | then 112 | 113 | JSSResource="$instanceName/JSSResource" 114 | 115 | ## Strip the company name from the instance using // and - as field separators and 116 | ## return the 2 field which in the JSS Instance URL will be the company name. 117 | ## This will be used for logging purposes if necessary. 118 | CompanyName=$(echo $JSSResource | awk -F "//|-" '{print $2}') 119 | 120 | ## Using curl with the silent option to suppress extraneous output we'll 121 | ## output the full list of policies from the JSS to a file. 122 | ## The list of Policies can be found at the /policies endpoint 123 | ## We must use accept on the header since we are using a GET request 124 | ## AND we want the JAMF Server to return an XML output. 125 | 126 | curl \ 127 | -k \ 128 | -s \ 129 | -u "$apiUser":"$apiPass" \ 130 | -H "accept: text/xml" \ 131 | "$JSSResource/policies" \ 132 | -X GET > /tmp/JSS_output.xml 133 | 134 | ## Since outputting the list of policies to a file just dumps the raw XML to the file 135 | ## it contains information that we might not want such as id number and other XML attributes. 136 | ## It also contains no formatting so using xsltproc we apply the template we created earlier 137 | ## and then output the formatted XML (which in this case is just the names of all the policies) 138 | ## to a text file. 139 | 140 | xsltproc /tmp/JSS_template.xslt /tmp/JSS_output.xml > /tmp/JSS_policylist.txt 141 | 142 | ## Read in the previous text file line by line to see if it contains the name of the policy 143 | ## we want to add. We check for a positive match one time only to avoid continual iterations of the 144 | ## loop to cause negative values to be reported. This is expected behavior but would throw off the results. 145 | 146 | while read name 147 | do 148 | if [ "$name" == "$PolicyName" ] 149 | then 150 | PolicyFound="Yes" 151 | fi 152 | done < /tmp/JSS_policylist.txt 153 | 154 | ## If a positive match is never found then it means the policy we want to create does not already exist 155 | ## so using curl we can POST a new policy to the JSS instance. We send it to the 0 endpoint since this allows 156 | ## the JSS to create it at the next available slot. Also since we are sending the curl command the location of an XML 157 | ## file in order to create the group we use the -T switch. If we were using just XML data or a variable with XML in 158 | ## it we would use the -d switch. 159 | 160 | if [ "$PolicyFound" != "Yes" ] 161 | then 162 | curl \ 163 | -s \ 164 | -k \ 165 | -u "$apiUser":"$apiPass" \ 166 | -H "content-type: text/xml" \ 167 | $JSSResource/policies/id/0 \ 168 | -T "$PolicyXML" \ 169 | -X POST 170 | ## Use printf (more reliable than echo) to insert a mew line after the curl statement for readability because 171 | ## bash will prob put the following echo command on the same line as the curl output 172 | printf "\n" 173 | echo "Policy:$PolicyName Created for $CompanyName" 174 | echo "" 175 | else 176 | echo "Policy: *$PolicyName* already exists for $CompanyName" 177 | echo "" 178 | fi 179 | 180 | ## Clean up temp files and variables after each iteration of the loop. 181 | ## While a new file will be created on each iteration it doesn't 182 | ## hurt to be careful and this will handle the last iteration. 183 | 184 | rm /tmp/JSS_policylist.txt 185 | rm /tmp/JSS_output.xml 186 | PolicyFound="" 187 | 188 | # End instance exists if statement 189 | else 190 | echo "$instanceName" 191 | echo "Instance is currently unreachable." 192 | echo "" 193 | 194 | fi 195 | 196 | done < "$InstanceList" 197 | 198 | rm /tmp/JSS* 199 | -------------------------------------------------------------------------------- /API_Scripts/Jamf_API_Create_Scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script is designed to check if a Script exists on a Jamf Pro Server and 4 | ## if not to upload a new one. 5 | ## Scripts can be uploaded by id to the following endpoint: 6 | ## /JSSResource/scripts/id/0 7 | 8 | ##Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 9 | 10 | ### When using the XML downloaded from the JSS we must strip the ID tags from the script 11 | ## and any categories associated with the script. We also must make sure any associated 12 | ## categories exist in the JSS first. 13 | 14 | ## Define Global Variables 15 | 16 | ## apiParms should be a path to a script that exports API user name and password so 17 | ## they don't have to be entered on the command line. 18 | ## Script should be in a format like the next 4 lines: 19 | ## #!/bin/bash 20 | ## 21 | ## apiUser='' 22 | ## apiPass='' 23 | apiParams="" 24 | 25 | ## If not using a script or file to set the user name or password define variables to get them from stdin 26 | ## JAMF Pro User with privileges to update the corresponding API endpoints 27 | apiUser="" 28 | ## Password for the user specified 29 | apiPass="" 30 | 31 | ##Get API variables. 32 | 33 | ## If the file containing the appropriate API Parameters does not exist or has a size 34 | ## of zero bytes then prompt the user for the necessary credentials. 35 | 36 | if [ ! -s "$apiParams" ] 37 | then 38 | read -r -p "Please enter a JAMF API administrator name: " apiUser 39 | read -r -s -p "Please enter the password for the account: " apiPass 40 | else 41 | ## Run the script found specified in apiParms to populate the variables 42 | ## Use dot notation so it says within the same process 43 | . "$apiParams" 44 | fi 45 | 46 | ScriptFound="" 47 | 48 | ## ARGV $1 the path to the xml file of the Script we want to upload/create 49 | ## ARGV $2 is the list of JAMF instances. 50 | ## This instance name file MUST end with one blank lime otherwise errors in processing will occuur. 51 | ## The file CANNOT contain windows line breaks otherwise sadness will happen. 52 | 53 | ## ARGV $1 54 | ############################################### 55 | ## BETTER TO CURL THE XML OF THE EXISTING SCRIPT DIRECTLY FROM THE JAMF SERVER EG: 56 | ### curl -k -u "$apiUser":"$apiPass" -H "accept: text/xml" https://your.jamfserver.url/JSSResource/scripts/id/INSERT ID NUMBER 57 | ### and pasting it to a new XML file instead of copying the xml from the JAMF api page as doing it this way 58 | ### will will contain all the XML escape codes in the script. Without them, the XML file may fail to properly upload. 59 | ### Can remove the from the new file though as well as id references. 60 | 61 | if [ "$1" == "" ] 62 | then 63 | ScriptXML="PATH TO XML FILE" 64 | else 65 | ScriptXML="$1" 66 | fi 67 | 68 | if [ "$2" == "" ] 69 | then 70 | InstanceList="PATH TO Instance File" 71 | else 72 | InstanceList="$2" 73 | fi 74 | 75 | ## Search for the actual (Display) name of the script we are going to create by reading in the XML file for the script. 76 | ## We first grep for " \ 188 | -X PUT 189 | 190 | ##Not necessary to upload the encoding as JAMF will generate a new value. Keep here just in case. 191 | 192 | #curl \ 193 | #-s \ 194 | #-k \ 195 | #-u "$apiUser":"$apiPass" \ 196 | #-H "Content-type: text/xml" \ 197 | #$JSSResource/scripts/id/$Scriptid \ 198 | #-d "" \ 199 | #-X PUT 200 | 201 | if [ "$DisplayName" != "" ] 202 | then 203 | curl \ 204 | -s \ 205 | -k \ 206 | -u "$apiUser":"$apiPass" \ 207 | -H "Content-type: text/xml" \ 208 | $JSSResource/scripts/id/$Scriptid \ 209 | -d "" \ 210 | -X PUT 211 | fi 212 | 213 | if [ "$Notes" != "" ] 214 | then 215 | curl \ 216 | -s \ 217 | -k \ 218 | -u "$apiUser":"$apiPass" \ 219 | -H "Content-type: text/xml" \ 220 | $JSSResource/scripts/id/$Scriptid \ 221 | -d "" \ 222 | -X PUT 223 | fi 224 | 225 | if [ "$Info" != "" ] 226 | then 227 | curl \ 228 | -s \ 229 | -k \ 230 | -u "$apiUser":"$apiPass" \ 231 | -H "Content-type: text/xml" \ 232 | $JSSResource/scripts/id/$Scriptid \ 233 | -d "" \ 234 | -X PUT 235 | fi 236 | 237 | if [ "$Parameters" != "" ] 238 | then 239 | curl \ 240 | -s \ 241 | -k \ 242 | -u "$apiUser":"$apiPass" \ 243 | -H "Content-type: text/xml" \ 244 | $JSSResource/scripts/id/$Scriptid \ 245 | -d "" \ 246 | -X PUT 247 | fi 248 | 249 | 250 | ## Use printf (more reliable than echo) to insert a mew line after the curl statement for readability because 251 | ## bash will prob put the following echo command on the same line as the curl output 252 | printf "\n" 253 | echo "Script:$ScriptName Updated for $CompanyName" 254 | echo "" 255 | else 256 | echo "Script *$ScriptName* not found for $CompanyName" 257 | echo "" 258 | fi 259 | 260 | ## Clean up temp files and variables after each iteration of the loop. 261 | ## While a new file will be created on each iteration it doesn't 262 | ## hurt to be careful and this will handle the last iteration. 263 | 264 | rm /tmp/JSS_Scriptlist.txt 265 | rm /tmp/JSS_output.xml 266 | ScriptFound="" 267 | 268 | # End instance exists if statement 269 | else 270 | echo "$instanceName" 271 | echo "Instance is currently unreachable." 272 | echo "" 273 | 274 | fi 275 | 276 | done < "$InstanceList" 277 | 278 | rm /tmp/JSS* 279 | -------------------------------------------------------------------------------- /API_Scripts/Jamf_API_Upload_Package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script is designed to check if a Package exists on a Jamf Pro Server and 4 | ## if not to upload a new one. 5 | ## Package data can be uploaded by id to the following endpoint: 6 | ## /JSSResource/packages/id/0 7 | ## Actual packages can be uploaded to the following endpoing: 8 | ## /dbfileupload 9 | 10 | ##Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 11 | 12 | ### When using the XML downloaded from the JSS we must strip the ID tags from the package 13 | ## and any categories associated with the package. We also must make sure any associated 14 | ## categories exist in the JSS first. 15 | 16 | ## Define Global Variables 17 | 18 | ## apiParms should be a path to a script that exports API user name and password so 19 | ## they don't have to be entered on the command line. 20 | ## Script should be in a format like the next 4 lines: 21 | ## #!/bin/bash 22 | ## 23 | ## apiUser='' 24 | ## apiPass='' 25 | apiParams="" 26 | 27 | ## If not using a script or file to set the user name or password define variables to get them from stdin 28 | ## JAMF Pro User with privileges to update the corresponding API endpoints 29 | apiUser="" 30 | ## Password for the user specified 31 | apiPass="" 32 | 33 | ##Get API variables. 34 | 35 | ## If the file containing the appropriate API Parameters does not exist or has a size 36 | ## of zero bytes then prompt the user for the necessary credentials. 37 | 38 | if [ ! -s "$apiParams" ] 39 | then 40 | read -r -p "Please enter a JAMF API administrator name: " apiUser 41 | read -r -s -p "Please enter the password for the account: " apiPass 42 | else 43 | ## Run the script found specified in apiParms to populate the variables 44 | ## Use dot notation so it says within the same process 45 | . "$apiParams" 46 | fi 47 | 48 | ## ARGV $1 is the list of JAMF instances. 49 | ## This instance name file MUST end with one blank lime otherwise errors in processing will occuur. 50 | ## The file CANNOT contain windows line breaks otherwise sadness will happen. 51 | 52 | if [ "$1" == "" ] 53 | then 54 | InstanceList="PATH TO Instance File" 55 | else 56 | InstanceList="$1" 57 | fi 58 | 59 | ## The path to a local package we want to upload 60 | PackageLocation="" 61 | 62 | ## If variable containing the path to the package does exists or does not have a size of 0 bytes 63 | ## prompt the user for the local path of the package to upload to the Jamf server 64 | if [ ! -s "$PackageLocation" ] 65 | then 66 | read -r -p "Please enter the path to the package you wish to upload: " PackageLocation 67 | fi 68 | 69 | ## Extract the file name from the package 70 | PackageName=$(basename $PackageLocation) 71 | echo "Uploading $PackageName to Jamf." 72 | 73 | ## The human readbale name of the package in Jamf 74 | ## Would not recommend changing in some environements as this can cause DP resyncs to occur 75 | #DisplayName="" 76 | 77 | ## Text for the info field for the package 78 | #Info="This package does...... 79 | #Designed to be run....." 80 | 81 | ## Text for the Notes field for the script 82 | #Notes="Additional Package 83 | #Information" 84 | 85 | ## Priority level of the package. A number between 1 and 10. 86 | ## If not assigned default is 10 87 | #Priority="" 88 | 89 | ## The category the package should be assigned to in Jamf 90 | ## !! The category must exist on the Jamf server first !! 91 | #Category="" 92 | 93 | ## Use a here block to Create an xslt template file that is used to format the XML 94 | ## outputted from the JSS to our standards in this case it will be used to produce 95 | ## a list of all the names of the Packages in the JSS. 96 | ## We'll first match the top element of the list of Packages returned from the JSS 97 | ## which is packages. Then from each child element (package) 98 | ## we'll select the value of the name attribute and then add a line break. 99 | 100 | cat < /tmp/JSS_template.xslt 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | EOF 112 | 113 | ###Just echo a blank line for readability 114 | echo "" 115 | 116 | while read instanceName 117 | do 118 | 119 | ## Translate the instance name to all lower case to prevent any processing errors in the shell 120 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 121 | 122 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 123 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to express 124 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 125 | InstanceExists=$(curl --silent --head "$instanceName" | head -n 1) 126 | 127 | if [ "$InstanceExists" != "" ] 128 | then 129 | 130 | JSSResource="$instanceName/JSSResource" 131 | 132 | ## Strip the company name from the instance using // and - as field separators and 133 | ## return the 2 field which in the JSS Instance URL will be the company name. 134 | ## This will be used for logging purposes if necessary. 135 | CompanyName=$(echo $JSSResource | awk -F "//|-" '{print $2}') 136 | 137 | ## Using curl with the silent option to suppress extraneous output we'll 138 | ## output the full list of packages from the JSS to a file. 139 | ## The list of Packages can be found at the /packages endpoint 140 | ## We must use accept on the header since we are using a GET request 141 | ## AND we want the JAMF Server to return an XML output. 142 | 143 | curl \ 144 | -k \ 145 | -s \ 146 | -u "$apiUser":"$apiPass" \ 147 | -H "accept: text/xml" \ 148 | "$JSSResource/packages" \ 149 | -X GET > /tmp/JSS_output.xml 150 | 151 | ## Since outputting the list of packages to a file just dumps the raw XML to the file 152 | ## it contains information that we might not want such as id number and other XML attributes. 153 | ## It also contains no formatting so using xsltproc we apply the template we created earlier 154 | ## and then output the formatted XML (which in this case is just the names of all the packages) 155 | ## to a text file. 156 | 157 | xsltproc /tmp/JSS_template.xslt /tmp/JSS_output.xml > /tmp/JSS_packagelist.txt 158 | 159 | ## Read in the previous text file line by line to see if it contains the name of the package 160 | ## we want to add. We check for a positive match one time only to avoid continual iterations of the 161 | ## loop to cause negative values to be reported. This is expected behavior but would throw off the results. 162 | 163 | while read name 164 | do 165 | if [ "$name" == "$PackageName" ] 166 | then 167 | PackageFound="Yes" 168 | fi 169 | done < /tmp/JSS_packagelist.txt 170 | 171 | ## If a positive match is never found then it means the package we want to create does not already exist 172 | ## so using curl we can POST a new package to the JSS instance. We send it to the 0 endpoint since this allows 173 | ## the JSS to create it at the next available slot. Also since we are sending the curl command the location of an XML 174 | ## file in order to create the group we use the -T switch. If we were using just XML data or a variable with XML in 175 | ## it we would use the -d switch. 176 | 177 | if [ "$PackageFound" != "Yes" ] 178 | then 179 | CurlOutput=$(curl -s -u "$apiUser":"$apiPass" -X POST $instanceName/dbfileupload \ 180 | -H "DESTINATION: 0" -H "OBJECT_ID: -1" -H "FILE_TYPE: 0" -H "FILE_NAME: $PackageName" -T "$PackageLocation") 181 | 182 | Packageid=$(echo $CurlOutput | awk -F "|" '{print $2}') 183 | 184 | if [ "$DisplayName" != "" ] 185 | then 186 | curl \ 187 | -s \ 188 | -k \ 189 | -u "$apiUser":"$apiPass" \ 190 | -H "Content-type: text/xml" \ 191 | $JSSResource/packages/id/$Packageid \ 192 | -d "$DisplayName" \ 193 | -X PUT 194 | fi 195 | 196 | if [ "$Info" != "" ] 197 | then 198 | curl \ 199 | -s \ 200 | -k \ 201 | -u "$apiUser":"$apiPass" \ 202 | -H "Content-type: text/xml" \ 203 | $JSSResource/packages/id/$Packageid \ 204 | -d "$Info" \ 205 | -X PUT 206 | fi 207 | 208 | if [ "$Notes" != "" ] 209 | then 210 | curl \ 211 | -s \ 212 | -k \ 213 | -u "$apiUser":"$apiPass" \ 214 | -H "Content-type: text/xml" \ 215 | $JSSResource/packages/id/$Packageid \ 216 | -d "$Notes" \ 217 | -X PUT 218 | fi 219 | 220 | if [ "$Category" != "" ] 221 | then 222 | curl \ 223 | -s \ 224 | -k \ 225 | -u "$apiUser":"$apiPass" \ 226 | -H "Content-type: text/xml" \ 227 | $JSSResource/packages/id/$Packageid \ 228 | -d "$Category" \ 229 | -X PUT 230 | fi 231 | 232 | if [ "$Priority" != "" ] 233 | then 234 | curl \ 235 | -s \ 236 | -k \ 237 | -u "$apiUser":"$apiPass" \ 238 | -H "Content-type: text/xml" \ 239 | $JSSResource/packages/id/$Packageid \ 240 | -d "$Priority" \ 241 | -X PUT 242 | fi 243 | 244 | ## Use printf (more reliable than echo) to insert a mew line after the curl statement for readability because 245 | ## bash will prob put the following echo command on the same line as the curl output 246 | printf "\n" 247 | echo "Package:$PackageName Created for $CompanyName" 248 | echo "" 249 | else 250 | echo "Package: *$PackageName* already exists for $CompanyName" 251 | echo "" 252 | fi 253 | 254 | ## Clean up temp files and variables after each iteration of the loop. 255 | ## While a new file will be created on each iteration it doesn't 256 | ## hurt to be careful and this will handle the last iteration. 257 | 258 | rm /tmp/JSS_packagelist.txt 259 | rm /tmp/JSS_output.xml 260 | PackageFound="" 261 | 262 | # End instance exists if statement 263 | else 264 | echo "$instanceName" 265 | echo "Instance is currently unreachable." 266 | echo "" 267 | 268 | fi 269 | 270 | done < "$InstanceList" 271 | 272 | rm /tmp/JSS* 273 | -------------------------------------------------------------------------------- /JamfProObjectBackup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ##Capture the script information to use for logging purposes. 4 | echo $$ > /tmp/JSS_Backup.pid 5 | ScriptPID=$(cat /tmp/JSS_Backup.pid) 6 | ScriptName=$(basename "$0") 7 | 8 | ## Define Global Variables 9 | ## Any references to the JSS are due to historical reasons but JSS = Jamf Pro Server 10 | ## The order of variable assignments is important 11 | 12 | ##Unset the GetInfoFlag so the getJamfInformation function runs 13 | unset GetInfoFlag 14 | 15 | ## Objects in a Jamf Pro Server available for download 16 | JSSAllObjectArray+=("scripts" "policies" "computer_groups" "advanced_computer_searches" "os_x_configuration_profiles" "computer_extension_attributes" "restricted_software" "accounts" "categories" "departments") 17 | 18 | # How many days to keep object backups 19 | DaysOlderThan="30" 20 | 21 | ## Location of a singular Jamf instance or file containing multiple instances 22 | InstanceList="" 23 | 24 | ## Path to folder where backup items will be stored 25 | BackupFolder="" 26 | 27 | ## If defining a user name and password in the script (not recommended) do so here 28 | ## and uncomment the following 2 lines (this will override any environmental variables): 29 | apiUser='' 30 | apiPass='' 31 | 32 | ## JAMF Pro User with privileges to update the corresponding API endpoints. Setting any empty variables to null will be important later. 33 | apiUser=${apiUser:-null} 34 | ## Password for the user specified 35 | apiPass=${apiPass:-null} 36 | 37 | ##If using the apiParams file to set the credentials create a script like the below. 38 | ## The apiParms will then export API user name and password so they don't have to be entered on the command line. 39 | ## Script should be in a format like the next 4 lines (without the comments): 40 | ## #!/bin/bash 41 | ## 42 | ## apiUser='' 43 | ## apiPass='' 44 | 45 | ## Then uncomment the below line and enter a path to the file as the value. This will override any previous credentials set or exported. 46 | #apiParams="" 47 | 48 | ScriptLogging(){ 49 | ## Function to provide logging of the script's actions either to the console or the log file specified. 50 | ## Developed by Rich Trouton https://github.com/rtrouton 51 | local LogStamp=$(date +%Y-%m-%d\ %H:%M:%S) 52 | if [[ -n "$2" ]] 53 | then 54 | LOG="$2" 55 | else 56 | LOG="PATH TO LOG FILE HERE" 57 | fi 58 | 59 | ## To output to a log file append ' >> $LOG' to the below echo statement 60 | echo "$LogStamp [$ScriptName]:" " $1" 61 | } 62 | ScriptLogging "Jamf Backup running with PID: $ScriptPID" 63 | 64 | getJamfInformation(){ 65 | 66 | ## Check to see if the information gathering function has already run and if so skip it 67 | if [[ "$GetInfoFlag" == "set" ]] 68 | then 69 | return 70 | fi 71 | 72 | ##Get API credential variables. 73 | if [[ -s "$apiParams" ]] 74 | then 75 | ScriptLogging "API user name and password file found. Sourcing." 76 | ## Run the script found for apiParms to populate the variables 77 | ## Use dot notation so it says within the same process 78 | . "$apiParams" 79 | elif [[ "$apiUser" == "null" ]] && [[ "$apiPass" == "null" ]] 80 | then 81 | ScriptLogging "Jamf API credentials not found. Prompting user." 82 | read -r -p "Please enter a Jamf API account name: " apiUser 83 | read -r -s -p "Please enter the password for the account: " apiPass 84 | elif [[ "$apiUser" == "null" ]] 85 | then 86 | ScriptLogging "Jamf API user not found. Prompting." 87 | read -r -p "Please enter a JAMF API account name: " apiUser 88 | elif [[ "$apiPass" == "null" ]] 89 | then 90 | ScriptLogging "Jamf API password not found. Prompting." 91 | read -r -p "Please enter a password for the JAMF API account: " apiPass 92 | else 93 | ScriptLogging "API Credentials found. Continuing." 94 | fi 95 | 96 | ## If the InstanceList variable is empty prompt for an instance of list of instances 97 | if [[ -z "$InstanceList" ]] 98 | then 99 | ScriptLogging "No Jamf Instances specified to backup. Prompting." 100 | read -r -p "Please enter a Jamf URL beginning with https:// or a file containing a list of instances to backup: " InstanceList 101 | fi 102 | 103 | ## If the BackupFolder variable is empty prompt for a location to store backup files 104 | if [[ -z "$BackupFolder" ]] 105 | then 106 | ScriptLogging "No backup location specified. Prompting." 107 | read -r -p "Please enter location to store backup items from Jamf: " BackupFolder 108 | fi 109 | 110 | ## Cleanup file paths dragged in from the terminal by removing any \ escape characters. 111 | InstanceList=$(echo "$InstanceList" | awk '{gsub(/\\/,""); print $0}') 112 | BackupFolder=$(echo "$BackupFolder" | awk '{gsub(/\\/,""); print $0}') 113 | 114 | # Create a local temporary folder in the backup folder the script is using 115 | BackupFolderTemp="$BackupFolder"/.tmp 116 | if [[ ! -d "$BackupFolderTemp" ]] 117 | then 118 | mkdir -p "$BackupFolderTemp" 119 | fi 120 | 121 | ##Cleanup any old instances if lying around so they don't geyt added to the instance file on the next script run 122 | if [[ -e "$BackupFolderTemp"/JSS_instances.txt ]] 123 | then 124 | rm -rf "$BackupFolderTemp"/JSS_instances.txt 125 | fi 126 | 127 | ## Write out any inputted instances to a file so the loops in the script process consistently. 128 | if [[ ! -f "$InstanceList" ]] 129 | then 130 | ScriptLogging "Writing out instance list to $BackupFolderTemp/JSS_instances.txt." 131 | printf "$InstanceList" '%s\n' >> "$BackupFolderTemp"/JSS_instances.txt 132 | InstanceList="$BackupFolderTemp"/JSS_instances.txt 133 | fi 134 | 135 | ##Set the get info flag so this function only runs once 136 | GetInfoFlag="set" 137 | } 138 | 139 | createBackupFolder(){ 140 | DownloadType="$1" 141 | 142 | # The folder where the script is running from. 143 | BackupScriptFolder=$(dirname "$0") 144 | # The date the script started running 145 | ScriptDate=$(date +%m-%d-%Y) 146 | 147 | #Folders for the downloaded objects to go to. 148 | if [[ -z "$BackupFolder" ]] 149 | then 150 | ObjectFolder="$BackupScriptFolder/$ScriptDate/$DomainName/$ObjectType/$DownloadType" 151 | else 152 | ObjectFolder="$BackupFolder/$ScriptDate/$DomainName/$ObjectType/$DownloadType" 153 | fi 154 | 155 | ##Temporary Folder for storing cache and other temp files for a particular object 156 | ##Variable expansion removes the last folder in the variable path (to remove more keep adding /*) 157 | TempFolder=${ObjectFolder%/*}/.tmp 158 | 159 | ##Create the folders needed to store the downloaded objects 160 | ScriptLogging "Creating Backup Folder: $ObjectFolder" 161 | mkdir -p "$ObjectFolder" 162 | mkdir -p "$TempFolder" 163 | 164 | if [[ "$ContentPrompt" == "y" ]] 165 | then 166 | mkdir -p "$ObjectFolder"/"$ObjectType"_Contents_NotEncoded 167 | ScriptLogging "$(echo $ObjectType | tr '[:lower:]' '[:upper:]') Contents [CODE] selected for download." 168 | else 169 | ScriptLogging "Contents [CODE ONLY] Not selected for download." 170 | fi 171 | } 172 | 173 | checkDependencies(){ 174 | 175 | dataType="$1" 176 | systemType=$(uname) 177 | archType=$(/usr/bin/arch) 178 | 179 | if [[ "$systemType" == "Darwin" ]] && [[ "$dataType" == "xml" ]] 180 | then 181 | ScriptLogging "macOS Detected. XML Components installed natively." 182 | elif [[ "$systemType" == "Linux" ]] && [[ "$dataType" == "xml" ]] 183 | then 184 | ScriptLogging "Linux Detected. Installing xml components if needed." 185 | apt-get install libxml2 186 | elif [[ "$systemType" == "Darwin" ]] && [[ "$dataType" == "json" ]] 187 | then 188 | ScriptLogging "macOS Detected. Checking for installed json tooling." 189 | jq=$(which jq) 190 | if [[ -z "$jq" ]] && [[ "$archType" == "arm64" ]] 191 | then 192 | ScriptLogging "JQ Not found. Installing ARM version into archive temp folder" 193 | JQ_LATEST=$(curl -s https://api.github.com/repos/jqlang/jq/releases/latest | grep "browser_download_url.*macos-arm64" | awk {'print $2'}) 194 | JQ_LATEST=$(echo "$JQ_LATEST" | sed -e 's/^"//' -e 's/"$//') 195 | curl -L -s "$JQ_LATEST" -o "$BackupFolderTemp"/jq 196 | chmod 755 "$BackupFolderTemp"/jq 197 | jq="$BackupFolderTemp"/jq 198 | elif [[ -z "$jq" ]] && [[ "$archType" != "arm64" ]] 199 | then 200 | ScriptLogging "JQ Not found. Installing AMD version into archive temp folder" 201 | JQ_LATEST=$(curl -s https://api.github.com/repos/jqlang/jq/releases/latest | grep "browser_download_url.*macos-amd64" | awk {'print $2'}) 202 | JQ_LATEST=$(echo "$JQ_LATEST" | sed -e 's/^"//' -e 's/"$//') 203 | curl -L -s "$JQ_LATEST" -o "$BackupFolderTemp"/jq 204 | chmod 755 "$BackupFolderTemp"/jq 205 | jq="$BackupFolderTemp"/jq 206 | elif [[ -n "$jq" ]] 207 | then 208 | ScriptLogging "Installation of jq found." 209 | fi 210 | elif [[ "$systemType" == "Linux" ]] && [[ "$dataType" == "json" ]] 211 | then 212 | ScriptLogging "Linux Detected. Installing json components if needed." 213 | sudo apt-get install jq 214 | fi 215 | } 216 | 217 | ## Function to generate an auth token on those instances running 10.35 or higher 218 | makeAuthHeader(){ 219 | local APIUser="$1" 220 | local APIPass="$2" 221 | local JSS_URL="$instanceName" 222 | 223 | ## Warn if APIUser or APIPass has no value 224 | if [[ -z "$APIUser" ]] 225 | then 226 | ScriptLogging "WARNING: NO API User was specified"> /dev/stderr 227 | elif [[ -z "$APIPass" ]] 228 | then 229 | ScriptLogging "WARNING: NO API Password was specified"> /dev/stderr 230 | elif [[ -z "$instanceName" ]] 231 | then 232 | ScriptLogging "WARNING: NO Instances found to backup"> /dev/stderr 233 | fi 234 | 235 | ## Get Jamf version from the URL using = and - as field delimiters. This allows us to get the major.minor version without any extraneous info. 236 | JSSVersion=$(curl -s "$JSS_URL"/JSSCheckConnection | awk -F "-" '{print $1}' ) 237 | 238 | ## Do to limitations of string comparison we'll have to strip off the major version of Jamf to first check if the version is below 9 since if we 239 | ## were to check full versions only with this method 10 would always be less than 9. Using this method the major version check must ALWAYS be performed 240 | ## first. Since we only care about versions 9 or less we can get away using this one major version comparison. Any versions 10 and above will then now 241 | ## compare their versions correctly. 242 | JSSMajorVersion=$(echo $JSSVersion | awk -F . '{print $1}') 243 | 244 | ## If the major version of Jamf is below 9 exit otherwise if the full version of Jamf is 10.0 or above but less than 10.35 use a different method of basic authentication. 245 | if echo "$JSSMajorVersion 9" | awk '{exit $1<=$2?0:1}' 246 | then 247 | ScriptLogging "Jamf Version 9 or lower detected exiting. " 248 | elif echo "$JSSVersion 10.35" | awk '{exit $1<$2?0:1}' 249 | then 250 | ScriptLogging "Executing api authorization function for Jamf 10 to 10.34" 251 | ## If the current version of Jamf is less than 10.35.0 BUT it is greater than or equal to 10.0 then use Basic Auth with base 64 encoded credentials 252 | ## and then echo them out with the authorization type so the API call can add this information to its headers. 253 | apiAuth=$(echo "Authorization: Basic $(echo -n ${APIUser}:${APIPass} | base64)") 254 | elif echo "$JSSVersion 10.35" | awk '{exit $1>=$2?0:1}' 255 | then 256 | ScriptLogging "Executing api token authorization function for Jamf 10.35 or higher." 257 | 258 | ## First check to see if the current token's expiration date exists and is valid so we don't generate a new one unnecessarily. 259 | ## Get the current time in epoch form 260 | CurrentDateEpoch=$(date "+%s") 261 | 262 | ## If the current date is greater than or equal to the time of token expiration we must generate a new token. 263 | ## Checking for greater than as opposed to less than will also allow the check to work correctly if $TokenExpirationEpoch has invalid or no data 264 | ## since then the equation would evaluate to true. This keeps the code cleaner and more compact (except for these comments). 265 | ## We'll also make sure we use mathematical comparison to avoid string comparison pitfalls. 266 | ## If the instance passed in (instanceName) is not the same as the instance from the last instance passed in (current_JSS_URL) also generate a new token 267 | if (( CurrentDateEpoch >= TokenExpirationEpoch )) || [[ "$current_JSS_URL" != "$instanceName" ]] 268 | then 269 | ScriptLogging "There is currently not a valid API token. Generating a new one." 270 | ## Invalidate any current tokens just in case 271 | Token=$(curl -s -H "$apiAuth" "${JSS_URL}/api/v1/auth/invalidate-token" -X POST) 272 | ## Generate a new token 273 | Encoded_Credentials=$(printf "${APIUser}:${APIPass}" | iconv -t ISO-8859-1 | base64 -i -) 274 | Token=$(curl -k -s -H "Authorization: Basic $Encoded_Credentials" -H "accept: application/json" "${JSS_URL}/api/v1/auth/token" -X POST) 275 | 276 | ## Currently the API Bearer token is issued based on the server time zone while the current date is determined on the client side. 277 | ## Therefore unless the server and client are adjusted to be in the same time zone getting the token expiration epoch and then using this formula 278 | ## TimeDifference=$(( (CurrentDateEpoch - TokenExpiryEpoch) / 60 )) to find the time difference in minutes and then checking if that difference is greater 279 | ## then 30 will not work but could be used if the time zone differences were accounted for. In which case you could get the appropriate values like so: 280 | ## TokenExpiry=$(echo $Token | awk -F \" '{print $8}') or in zsh TokenExpiry=$(echo $Token | awk -F \" '/expires/{print $4}') 281 | ## TokenExpiryEpoch=$(date -jf "%Y-%m-%dT%H:%M:%SZ" "${TokenExpiry%%.*}Z" "+%s") 282 | 283 | ## However an easy work around is to get the time when the token is created and add 29 minutes (in seconds) to it which will give us the time of token expiration in epoch form. 284 | ## Even though bearer tokens expire in 30 mins will play it safe a subtract a minute to account for a less than accurate time comparison and potential delays in the script. 285 | ## This can also be moved to the Jamf API endpoint to check if the token is still valid, but this way reduces API calls. 286 | TokenExpirationEpoch=$(date -v+"1740"S "+%s") 287 | 288 | ## Once a token is generated the actual token must also be extracted from the API response. 289 | AuthToken=$(echo $Token | awk -F \" '/token/{print $4}') 290 | ## Set the instance passed in to the current Jamf Instance being used 291 | current_JSS_URL="$instanceName" 292 | ## Warn if the authorization token was not captured. 293 | if [[ -z "$AuthToken" ]] 294 | then 295 | ScriptLogging "WARNING: Authorization Token has no value." 296 | ScriptLogging "WARNING: API calls will not execute for this $current_JSS_URL" 297 | continue 298 | else 299 | apiAuth=$(echo "Authorization: Bearer $AuthToken") 300 | fi 301 | ## Explicitly set the token value so that data is not visible anymore 302 | Token="" 303 | else 304 | ScriptLogging "Current API Token still valid not renewing." 305 | fi 306 | else 307 | ScriptLogging "No JSS Version detected" 308 | fi 309 | } 310 | 311 | downloadJSONObjects(){ 312 | 313 | for instanceName in $(cat $InstanceList) 314 | do 315 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 316 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 317 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to express 318 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 319 | InstanceExists=$(curl --silent "$instanceName/healthCheck.html") 320 | 321 | if [[ "$InstanceExists" == "[]" ]] 322 | then 323 | ##Add the API prefix the the entered instance(s) 324 | DomainName=$(echo $instanceName | awk -F "//" '{print $2}') 325 | JamfClassicAPIURL="$instanceName/JSSResource" 326 | ScriptLogging "Backing up objects from $instanceName in JSON format." 327 | 328 | for ObjectTypeName in "${JSSObjectArray[@]}" 329 | do 330 | #Some Jamf API records include an underscore which we pass in as the name above BUT the associated api endpoint does not have 331 | #an underscore nor do we want it for certain naming conventions so we remove it. 332 | ObjectType=$(echo "$ObjectTypeName" | sed -e 's/_//g') 333 | ScriptLogging "$(echo $ObjectTypeName | tr '[:lower:]' '[:upper:]') now backing up in JSON format." 334 | 335 | ## Reset the content warning flag so we can warn (only once) if an object does not have separate content available for download 336 | ContentWarningFlag="on" 337 | 338 | #Run the API Authorization Header function to either get or check for a valid token 339 | makeAuthHeader "$apiUser" "$apiPass" 340 | if [[ -z "$AuthToken" ]] 341 | then 342 | continue 343 | fi 344 | 345 | ## Create the backup folders 346 | createBackupFolder json 347 | 348 | ## Get the total number of objects to download 349 | ObjectSize=$(curl -s -H "$apiAuth" -X GET "$JamfClassicAPIURL/$ObjectType" -H "accept: text/xml" | xmllint --xpath "$ObjectTypeName/size/text()" -) 350 | 351 | ##Get the object names and id numbers 352 | if [[ "$ObjectTypeName" == "accounts" ]] 353 | then 354 | curl -s -H "$apiAuth" -X GET "$JamfClassicAPIURL/accounts" -H "accept: application/json" > "$TempFolder"/JSS_"$ObjectType"_TEMP.txt 2>/dev/null 355 | "$jq" -r ".accounts.users[] | .name + \"_API_SEPARATOR_\" + (.id|tostring) + \"_API_SEPARATOR_\" + \"userid\"" "$TempFolder"/JSS_"$ObjectType"_TEMP.txt > "$TempFolder"/JSS_"$ObjectType".txt 2>/dev/null 356 | "$jq" -r ".accounts.groups[] | .name + \"_API_SEPARATOR_\" + (.id|tostring) + \"_API_SEPARATOR_\" + \"groupid\"" "$TempFolder"/JSS_"$ObjectType"_TEMP.txt >> "$TempFolder"/JSS_"$ObjectType".txt 2>/dev/null 357 | else 358 | curl -s -H "$apiAuth" -X GET "$JamfClassicAPIURL/$ObjectType" -H "accept: application/json" | "$jq" -r ".$ObjectTypeName[] | .name + \"_API_SEPARATOR_\" + (.id|tostring)" > "$TempFolder"/JSS_"$ObjectType".txt 2>/dev/null 359 | fi 360 | ScriptLogging "Cleaning up and formatting JSON files for: $(echo $ObjectTypeName | tr '[:lower:]' '[:upper:]')" 361 | while read row 362 | do 363 | ObjectName=$(echo "$row" | awk -F "_API_SEPARATOR_" '{gsub(/\//,"|");gsub(/\:/,"-"); print $1}') 364 | Objectid=$(echo "$row" | awk -F "_API_SEPARATOR_" '{print $2}') 365 | AccountType=$(echo "$row" | awk -F "_API_SEPARATOR_" '{print $3}') 366 | 367 | #https://www.ditig.com/jq-recipes 368 | if [[ "$ObjectTypeName" == "computer_groups" ]] 369 | then 370 | curl -s -H "$apiAuth" -H "accept: application/json" -X GET "$JamfClassicAPIURL/$ObjectType/id/$Objectid" | "$jq" '(.. | select(type == "object")) |= (if .is_smart|tostring == "true" then del(.computers[]) else . end) | del(..|nulls)' > "$TempFolder"/"$ObjectName"-"$Objectid".json 2>/dev/null 371 | elif [[ "$ObjectTypeName" == "advanced_computer_searches" ]] 372 | then 373 | curl -s -H "$apiAuth" -H "accept: application/json" -X GET "$JamfClassicAPIURL/$ObjectType/id/$Objectid" | "$jq" '(.. | select(type == "object")) |= (if .advanced_computer_search != "" then del(.computers) else . end) | del(..|nulls)' > "$TempFolder"/"$ObjectName"-"$Objectid".json 2>/dev/null 374 | elif [[ "$ObjectTypeName" == "accounts" ]] 375 | then 376 | #Delete the id key value pair anywhere only from 3 places in the accounts json file so we preserve the id of the LDAP server if it exists. Also delete the hashed password from accounts 377 | curl -s -H "$apiAuth" -H "accept: application/json" -X GET "$JamfClassicAPIURL/accounts/$AccountType/$Objectid" | "$jq" 'del( .account.id, .group.id, .group.site.id, .account.password_sha256 )' > "$ObjectFolder"/"$ObjectName"-"$Objectid".json 378 | else 379 | curl -s -H "$apiAuth" -H "accept: application/json" -X GET "$JamfClassicAPIURL/$ObjectType/id/$Objectid" > "$TempFolder"/"$ObjectName"-"$Objectid".json 2>/dev/null 380 | fi 381 | 382 | #Delete the id key value pair in the json file 383 | if [[ "$ObjectTypeName" != "accounts" ]] 384 | then 385 | #Delete the id key value pair anywhere in the json file 386 | "$jq" '(.. | select(type == "object")) |= del (.id)' "$TempFolder"/"$ObjectName"-"$Objectid".json > "$ObjectFolder"/"$ObjectName"-"$Objectid".json 387 | fi 388 | 389 | ## Download just the code without the json data and store it separately. This could prob be cleaner. 390 | if [[ "$ContentPrompt" == "y" ]] 391 | then 392 | if [[ "$ObjectTypeName" == "computer_extension_attributes" ]] 393 | then 394 | DownloadPath=".computer_extension_attribute.input_type.script" 395 | curl -s -H "$apiAuth" -H "accept: application/json" -X GET "$JamfClassicAPIURL/$ObjectType/id/$Objectid" | "$jq" -r "$DownloadPath" > "$ObjectFolder"/"$ObjectType"_Contents_NotEncoded/"$ObjectName"-"$Objectid".sh 396 | elif [[ "$ObjectTypeName" == "scripts" ]] 397 | then 398 | DownloadPath=".script.script_contents" 399 | curl -s -H "$apiAuth" -H "accept: application/json" -X GET "$JamfClassicAPIURL/$ObjectType/id/$Objectid" | "$jq" -r "$DownloadPath" > "$ObjectFolder"/"$ObjectType"_Contents_NotEncoded/"$ObjectName"-"$Objectid".sh 400 | elif [[ "$ContentWarningFlag" == "on" ]] 401 | then 402 | ScriptLogging "$(echo $ObjectTypeName | tr '[:lower:]' '[:upper:]') does not have separate content available for download." 403 | ContentWarningFlag="off" 404 | fi 405 | fi 406 | ## End the individual object download while loop 407 | done < "$TempFolder"/JSS_"$ObjectType".txt 408 | 409 | ## Object Temporary folder cleanup 410 | rm -rf "$TempFolder" 411 | 412 | ## End object download for loop 413 | done 414 | 415 | else 416 | ScriptLogging "$instanceName is currently unreachable." 417 | continue 418 | fi 419 | ## End instance check for loop 420 | done 421 | 422 | } 423 | 424 | downloadXMLObjects(){ 425 | 426 | ## Use a here doc to Create an xslt template file that is used to format the XMLoutputed from the JSS. 427 | cat < "$BackupFolderTemp"/JSS_template.xslt 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | _API_SEPARATOR_ 436 | 437 | 438 | 439 | _API_SEPARATOR_ 440 | userid 441 | 442 | 443 | _API_SEPARATOR_ 444 | groupid 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | EOF 453 | 454 | 455 | cat < "$BackupFolderTemp"/JSS_template2.xslt 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | EOF 472 | 473 | for instanceName in $(cat $InstanceList) 474 | do 475 | instanceName=$(echo "$instanceName" | tr '[:upper:]' '[:lower:]') 476 | ## Check an instances's header codes to see if it is actually up. If curl returns any header information 477 | ## we assume the instance is up if not we assume the instance is down. We use the silent option to express 478 | ## any extraneous curl output and pipe the results to only return the first line of the head so it can fit in a variable neatly 479 | InstanceExists=$(curl --silent "$instanceName/healthCheck.html") 480 | 481 | if [[ "$InstanceExists" == "[]" ]] 482 | then 483 | ##Add the API prefix the the entered instance(s) 484 | DomainName=$(echo $instanceName | awk -F "//" '{print $2}') 485 | JamfClassicAPIURL="$instanceName/JSSResource" 486 | ScriptLogging "Backing up objects from $instanceName in XML format." 487 | 488 | for ObjectTypeName in "${JSSObjectArray[@]}" 489 | do 490 | #Some Jamf API records include an underscore which we pass in as the name above BUT the associated api endpoint does not have 491 | #an underscore nor do we want it for certain naming conventions so we remove it. 492 | ObjectType=$(echo "$ObjectTypeName" | sed -e 's/_//g') 493 | ScriptLogging "$(echo $ObjectTypeName | tr '[:lower:]' '[:upper:]') now backing up in XML format." 494 | 495 | ## Reset the content warning flag so we can warn (only once) if an object does not have separate content available for download 496 | ContentWarningFlag="on" 497 | 498 | #Run the API Authorization Header function to either get or check for a valid token 499 | makeAuthHeader "$apiUser" "$apiPass" 500 | if [[ -z "$AuthToken" ]] 501 | then 502 | continue 503 | fi 504 | 505 | ## Create the backup folders 506 | createBackupFolder xml 507 | 508 | ## Get the total number of objects to download 509 | ObjectSize=$(curl -s -H "$apiAuth" -X GET "$JamfClassicAPIURL/$ObjectType" -H "accept: text/xml" | xmllint --xpath "$ObjectTypeName/size/text()" - 2>/dev/null) 510 | 511 | ## Get the object names and id numbers 512 | curl -s -H "$apiAuth" -H "accept: text/xml" "$JamfClassicAPIURL/$ObjectType" -X GET > "$TempFolder"/JSS_"$ObjectType".xml 2>/dev/null 513 | 514 | ## Apply the second XSLT template which deletes unnecessary objects in the XML 515 | ScriptLogging "Cleaning up and formatting XML files for: $(echo $ObjectTypeName | tr '[:lower:]' '[:upper:]')" 516 | xsltproc "$BackupFolderTemp"/JSS_template.xslt "$TempFolder"/JSS_"$ObjectType".xml > "$TempFolder"/JSS_"$ObjectType".txt 2>/dev/null 517 | 518 | while read row 519 | do 520 | ObjectName=$(echo "$row" | awk -F "_API_SEPARATOR_" '{gsub(/\//,"|");gsub(/\:/,"-"); print $1}') 521 | Objectid=$(echo "$row" | awk -F "_API_SEPARATOR_" '{print $2}') 522 | AccountType=$(echo "$row" | awk -F "_API_SEPARATOR_" '{print $3}') 523 | 524 | if [[ -z "$AccountType" ]] 525 | then 526 | curl -s -H "$apiAuth" -H "accept: text/xml" -X GET "$JamfClassicAPIURL/$ObjectType/id/$Objectid" > "$TempFolder"/"$ObjectName"-"$Objectid".xml 527 | else 528 | curl -s -H "$apiAuth" -H "accept: text/xml" -X GET "$JamfClassicAPIURL/$ObjectType/$AccountType/$Objectid" > "$TempFolder"/"$ObjectName"-"$Objectid".xml 529 | fi 530 | 531 | ##Remove any elements (as specified by template2) from the XML files and copy the resulting and final XML to a new file. 532 | xsltproc "$BackupFolderTemp"/JSS_template2.xslt "$TempFolder"/"$ObjectName"-"$Objectid".xml > "$ObjectFolder"/"$ObjectName"-"$Objectid".xml 2>/dev/null 533 | 534 | ## Download just the code without the json data and store it separately. This could prob be cleaner. 535 | 536 | if [[ "$ContentPrompt" == "y" ]] 537 | then 538 | if [[ "$ObjectTypeName" == "computer_extension_attributes" ]] 539 | then 540 | DownloadPath="string(/computer_extension_attribute/input_type/script)" 541 | curl -s -H "$apiAuth" -H "accept: text/xml" -X GET "$JamfClassicAPIURL/$ObjectType/id/$Objectid" | xmllint --xpath "$DownloadPath" - > "$ObjectFolder"/"$ObjectType"_Contents_NotEncoded/"$ObjectName"-"$Objectid".sh 542 | elif [[ "$ObjectTypeName" == "scripts" ]] 543 | then 544 | DownloadPath="string(/script/script_contents)" 545 | curl -s -H "$apiAuth" -H "accept: text/xml" -X GET "$JamfClassicAPIURL/$ObjectType/id/$Objectid" | xmllint --xpath "$DownloadPath" - > "$ObjectFolder"/"$ObjectType"_Contents_NotEncoded/"$ObjectName"-"$Objectid".sh 546 | elif [[ "$ContentWarningFlag" == "on" ]] 547 | then 548 | ScriptLogging "$(echo $ObjectTypeName | tr '[:lower:]' '[:upper:]') does not have separate content available for download." 549 | ContentWarningFlag="off" 550 | fi 551 | fi 552 | ## End the individual object download while loop 553 | done < "$TempFolder"/JSS_"$ObjectType".txt 554 | 555 | ## Object Temporary folder cleanup 556 | rm -rf "$TempFolder" 557 | 558 | ## End object download for loop 559 | done 560 | 561 | else 562 | ScriptLogging "$instanceName is currently unreachable." 563 | continue 564 | fi 565 | ## End instance check for loop 566 | done 567 | } 568 | 569 | ## This function prints out the how to use the script and its associated options. Will be displayed on error or if no options are specified. 570 | usage(){ 571 | echo "" 572 | echo " Example usage: /path/to/script.sh -c -x scripts computer_groups" 573 | echo " Downloads all the scripts and computer groups from Jamf Pro as well as the contents (actual script) associated with the script objects." 574 | echo "" 575 | echo " A folder to backup objects to, a Jamf URL (or multiple URLs), and a Jamf API user name and password must be defined" 576 | echo " within the script or as environmental variables otherwise the script will prompt for these items when run." 577 | echo "" 578 | echo " Available options: -c -a -x -j -h" 579 | echo " -c: When selected downloads the script content for available objects such as scripts and computer extension attributes." 580 | echo " This content is downloaded to an additional folder inside the associated object folder." 581 | echo " -a: Download all available objects from Jamf Pro. Must be followed by -x or -j (or both)" 582 | echo " -x: Download specified objects from your Jamf servers in XML format." 583 | echo " -j: Download specified objects from your Jamf servers in JSON format." 584 | echo " -h: displays this message" 585 | echo "" 586 | echo " -x and -j must be followed by which object is to be downloaded. Multiple objects may be specified on the command line." 587 | echo " Available objects to download from Jamf are \"$(for i in "${JSSAllObjectArray[@]}"; do printf "$i " ; done)\"" 588 | echo "" 589 | echo " ** Options -c and -a must be specified before any other option. Both only need to be only specified once** " 590 | echo " -x and -j may be specified in the same run of the script but each must be followed by an object or objects to download." 591 | echo "" 592 | echo " Certain options may be strung together. For example \"-cax\" will download all objects and their associated content in XML format." 593 | echo "" 594 | echo " All downloaded object files are named with their name in Jamf as well as their object ID. All files are placed in folders based on the" 595 | echo " date they were downloaded, the server they were downloaded from, their object type, and format." 596 | echo "" 597 | } 598 | 599 | ## Used to handle options that might not have arguments when combined with other options (such as when download all is selected) 600 | ## https://stackoverflow.com/questions/11517139/optional-option-argument-with-getopts/57295993#57295993 601 | ## https://stackoverflow.com/questions/7529856/retrieving-multiple-arguments-for-a-single-option-using-getopts-in-bash 602 | getopts_get_optional_argument() { 603 | #Start with a blank JSS Object Array 604 | unset JSSObjectArray 605 | ## Keep reading in arguments to script options until another option is found or there are no more arguments. 606 | until [[ $(eval "echo \${$OPTIND}") =~ ^-.* ]] || [ -z $(eval "echo \${$OPTIND}") ] 607 | do 608 | JSSObjectArray+=($(eval "echo \${$OPTIND}")) 609 | OPTIND=$((OPTIND + 1)) 610 | done 611 | } 612 | 613 | ## Set the no arguments value to true. If an argument is passed to getopts it will not only run the functions/options in the while loop 614 | ## but it will later set this value to false which will case the usage function to run instead of any code inside the getopts while loop. 615 | no_arguments="true" 616 | 617 | while getopts "jxcah" opt; do 618 | ## Run this function before any functions or options associated with arguments, which will prompt the user to enter their Jamf information if not found. 619 | ## Having this function here means it won't run if there is an error sending options to the script. 620 | ## Then set a flag so this function only runs once per script. 621 | case $opt in 622 | c) 623 | ## User has selected to download associated content with an object 624 | ContentPrompt="y" 625 | ContentWarningFlag="on" 626 | ;; 627 | a) 628 | ## User has selected to download all objects in a Jamf instance. 629 | DownloadAll="yes" 630 | ScriptLogging "All available objects selected for download." 631 | ;; 632 | j) 633 | ## Run the function to check for multiple arguments, download objcts as JSON and if no arguments found (the array is empty) for JSON download display an error 634 | getopts_get_optional_argument $@ 635 | if [[ "$DownloadAll" == "yes" ]] 636 | then 637 | ScriptLogging "Downloading all objects in JSON format." 638 | JSSObjectArray=(${JSSAllObjectArray[@]}) 639 | elif [[ -z "${JSSObjectArray[@]}" ]] 640 | then 641 | ScriptLogging "-j requires at least one argument from: \"$(for i in "${JSSAllObjectArray[@]}"; do printf "$i " ; done)\"" 642 | exit 0 643 | fi 644 | getJamfInformation 645 | checkDependencies json 646 | downloadJSONObjects 647 | ;; 648 | x) 649 | ## Run the function to check for multiple arguments, download objcts as JSON and if no arguments found (the array is empty) for JSON download display an error 650 | getopts_get_optional_argument $@ 651 | if [[ "$DownloadAll" == "yes" ]] 652 | then 653 | ScriptLogging "Downloading all objects in XML format." 654 | JSSObjectArray=(${JSSAllObjectArray[@]}) 655 | elif [[ -z "${JSSObjectArray[@]}" ]] 656 | then 657 | ScriptLogging "-x requires at least one argument from: \"$(for i in "${JSSAllObjectArray[@]}"; do printf "$i " ; done)\"" 658 | exit 0 659 | fi 660 | getJamfInformation 661 | checkDependencies xml 662 | downloadXMLObjects 663 | ;; 664 | h) 665 | ## Run the help function 666 | usage 667 | exit 0 668 | ;; 669 | \?) 670 | ScriptLogging "Invalid Function Argument." 671 | usage 672 | exit 0 673 | ;; 674 | *) 675 | ## Displays a message if no argument specified for certain functions 676 | ScriptLogging "This option requires at least one argument from: \"$(for i in "${JSSAllObjectArray[@]}"; do printf "$i " ; done)\"" 677 | exit 0 678 | ;; 679 | esac 680 | no_arguments="false" 681 | done 682 | 683 | ## No options were passed to the script so we display the usage function 684 | if [[ "$no_arguments" == "true" ]] 685 | then 686 | echo "" 687 | echo "This script requires an option." 688 | usage 689 | else 690 | ## If no_arguments has been set to false that means the code inside the getopts while loop has run which means removing old folders and temp items can be run 691 | ## Remove old backup folders 692 | ScriptLogging "Removing folders older than $DaysOlderThan Days" 693 | find "$BackupFolder"/ -type d -mtime +"$DaysOlderThan" -exec rm -rf {} \; 694 | 695 | ## Invalidate any current api tokens just in case 696 | Token=$(curl -s -H "$apiAuth" "${JSS_URL}/api/v1/auth/invalidate-token" -X POST) 697 | AuthToken="" 698 | 699 | ## Clean up Temp files 700 | rm -rf "$BackupFolderTemp" 701 | rm -rf /tmp/JSS* 702 | fi 703 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A collection of scripts for use with JAMF Pro 2 | 3 | ## The API_Scripts folder contains scripts used to manipulate Jamf objects via the api 4 | 5 | ## The macOS Scripts should are intended to be used as follows: 6 | 1. macOSUpgrades_CheckCachedInstaller: Used to stage and validate a macOS installer package on a client computer for future installation. 7 | 2. macOSUpgrades: Used to prompt a user to install a cached macOS installer package and force the installation after x number of attempts. 8 | 3. macOSUpgrades_SelfService: Allows a user to install a cached macOS installer package from Self Service. Can be used in conjunction with macOSUpgrades or by itself. 9 | 10 | #### Once running macOSUpgrades and macOSUpgrades_SelfService administrators should stop running or disable macOSUpgrades_CheckCachedInstaller 11 | #### These scripts assume the macOS Installer is wrapped in a pkg for initial download/caching. While other packaging methods such as DMGs can be used they will require additional steps in order to be supported by these scripts. 12 | -------------------------------------------------------------------------------- /SetComputerName: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -e /etc/ComputerNamed.txt ] 4 | then 5 | 6 | #Get the current logged User which is assumed to be the run running Splash Buddy 7 | #We should change this to the Python method 8 | 9 | loggedInUser=$(/bin/ls -l /dev/console | /usr/bin/awk '{ print $3 }') 10 | loggedInUID=$(id -u "$loggedInUser") 11 | 12 | sleep .5 13 | 14 | while [[ "$loggedInUID" -le 500 ]] 15 | do 16 | echo "Current Console user not found." 17 | loggedInUser=$(/bin/ls -l /dev/console | /usr/bin/awk '{ print $3 }') 18 | loggedInUID=$(id -u "$loggedInUser") 19 | done 20 | 21 | sleep 1 22 | 23 | osascript <<'EOF' 24 | ##########!/usr/bin/osascript 25 | 26 | # Intialize the ComputerNameLength to false 27 | set ComputerNameLength to false 28 | 29 | # Prompt the user to enter a name for the computer which gets stored as text in the variable computer name. Format text box with an icon and a button for the user to click to continue. 30 | set computer_name to text returned of (display dialog "Begining Custom DEP Setup:\n\nEnter Computer Name:" default answer "" with title "Set Computer Name" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:com.apple.imac-unibody-27-no-optical.icns" buttons {"OK"} default button 1) 31 | 32 | #Get the count of inputed characters for the computer name. 33 | set Character_Count to count (computer_name) 34 | 35 | # If the inputed computer name is greater than 15 characters or blank we keep prompting the user appropriately to re enter the name. 36 | # Once an acceptable name is entered we set the ComputerNameLength variable to true to break out of the while loop. 37 | repeat while ComputerNameLength is false 38 | if Character_Count is greater than 15 then 39 | set computer_name to text returned of (display dialog "Computer Name must be less then 15 Characters" default answer "" with title "Computer Name" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:com.apple.imac-unibody-27-no-optical.icns" buttons {"OK"} default button 1) 40 | else if Character_Count is equal to 0 then 41 | set computer_name to text returned of (display dialog "Computer Name cannot be blank" default answer "" with title "Computer Name" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:com.apple.imac-unibody-27-no-optical.icns" buttons {"OK"} default button 1) 42 | else 43 | set ComputerNameLength to true 44 | end if 45 | # Check to see if the computer name contains the word macbook which we will assume means it has a generic name. 46 | # The positioning of this repeat loop is important inside the name length check. This way it still checks for an invalid length while 47 | # checking for an invalid name 48 | repeat while computer_name contains "macbook" 49 | set computer_name to text returned of (display dialog "Computer Name must not contain the word Macbook" default answer "" with title "Computer Name" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:com.apple.imac-unibody-27-no-optical.icns" buttons {"OK"} default button 1) 50 | end repeat 51 | # Check to see if the computer name is set to only imac (all case variations) which we will assume means it has a generic name. 52 | # We do a check explicitedly for just imac because some computers are actually named with imac in their name. 53 | # The positioning of this repeat loop is also important inside the name length check. This way it still checks for an invalid length while 54 | # checking for an invalid name 55 | repeat while computer_name is equal to "imac" 56 | set computer_name to text returned of (display dialog "Computer Name must not only be iMac" default answer "" with title "Computer Name" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:com.apple.imac-unibody-27-no-optical.icns" buttons {"OK"} default button 1) 57 | end repeat 58 | # We set the character count variable name again so the while loop/if statements 59 | # can continue if the name is still empty or greater than 15 characters. 60 | set Character_Count to count (computer_name) 61 | end repeat 62 | 63 | 64 | # For the do shell script commands we need the full path to the scutil binary which means it should be enclosed in quotes 65 | # so AppleScript interpets the leading / properly. We also have to remember to leave a space after the end of the command because AppleScript 66 | # doesn't insert spaces between the command and the variable automatically. 67 | # We make sure we use the quoted form of the computer_name variable just in case and in order to properly set the name we need to run the scipt with admin privs. 68 | 69 | do shell script "/usr/sbin/scutil --set HostName " & quoted form of computer_name with administrator privileges 70 | 71 | do shell script "/usr/sbin/scutil --set LocalHostName " & quoted form of computer_name with administrator privileges 72 | 73 | do shell script "/usr/sbin/scutil --set ComputerName " & quoted form of computer_name with administrator privileges 74 | 75 | do shell script "touch /etc/ComputerNamed.txt" with administrator privileges 76 | 77 | do shell script "/usr/local/bin/jamf recon" with administrator privileges 78 | EOF 79 | 80 | else 81 | echo "Computer already named via DEP Script" 82 | echo "Removing cached packages if they exist." 83 | ## This is to remove the Splash Buddy Package if it downloaded on a previous run as we expect a new one to get installed 84 | ## after this script completes so this way there will be no worries about over writing it. 85 | /bin/rm -rf /Library/Application\ Support/JAMF/Waiting\ Room/* &> /dev/null 86 | fi 87 | -------------------------------------------------------------------------------- /macOSUpgrades_CheckCachedInstaller: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Version 3 4 | ## 4/21/22 5 | ## Tom Rice (Macadmins: trice Github: trice81384) 6 | 7 | ### This script is designed to be run once a day from Jamf and check for a properly cached macOS installer. 8 | ### If the installer exists and is the correct name and size the script exits otherwise it attempts to cache the installer. 9 | ### A dummy receipt is created once the package is successfully cached to be used in conjunction with a smart group to present the installer in Self Service. 10 | ### The purpose for caching the package is so that it can be used for future macOS Upgrades when ready. 11 | ### A policy to cache the installer must exist in Jamf so it can be called by the execution triggers in this script. 12 | 13 | ScriptLogging(){ 14 | # Function to provide logging of the script's actions either to the console or the log file specified. 15 | ### Developed by Rich Trouton 16 | ### https://github.com/rtrouton 17 | local LogStamp=$(date +%Y-%m-%d\ %H:%M:%S) 18 | if [[ -n "$2" ]] 19 | then 20 | LOG="$2" 21 | else 22 | LOG="PATH TO LOG FILE HERE" 23 | fi 24 | 25 | ## To output to a log file append ' >> $LOG' to the below echo statement 26 | echo "$LogStamp" " $1" 27 | } 28 | 29 | ################### Define Global Variables ############################################# 30 | 31 | ## Parameter 4 passed in from Jamf will set the name of the macOS to be installed. 32 | ## This value is used in the case statement below to define parameters for the OS as well 33 | ## as elsewhere throughout the script such as Jamf Helper dialogs. 34 | macOSName="$4" 35 | 36 | ## Fill in the values as needed for the version of the OS you'll be installing while adding additional OS Versions as needed. 37 | ## Some possible values are: 38 | ## Mojave: Version: 10.14.6, Build: 18G84 39 | ## Catalina: Version: 10.15.7, Build: 19H15 40 | ## Big Sur: Version: 11.6, Build: 20G164 41 | ## Monterey: Version: 12.3.1, Build: 21E258, Installer Version: 17303 42 | 43 | case "$macOSName" in 44 | "Mojave" ) 45 | #Version of the macOS to be installed 46 | macOSVersion="" 47 | 48 | #Version of the macOS Installer Application (CFBundleVersion) 49 | InstallerVersion="" 50 | 51 | #Build of the macOS to be installed 52 | macOSBuild="" 53 | 54 | #Name of the OS Installer Package 55 | PackageName="" 56 | 57 | ##Location of cached Installer 58 | CachedmacOSFile="/Library/Application Support/Jamf/Waiting Room/$PackageName" 59 | 60 | #Expected Size of the Cached installer 61 | CachedFileSize="" 62 | 63 | ## Free space needed to cache package measured in Gigibytes 64 | ## Found by taking the amount of GB needed, converting to Gi, and rounding to the next whole number 65 | ## GB * (1000^3) / (1024^3) 66 | needed_free_space="" 67 | 68 | #Catalina Cache Trigger 69 | cachemacOS="cache$macOSName" 70 | ;; 71 | 72 | "Catalina" ) 73 | #Version of the macOS to be installed 74 | macOSVersion="" 75 | 76 | #Version of the macOS Installer Application (CFBundleVersion) 77 | InstallerVersion="" 78 | 79 | #Build of the macOS to be installed 80 | macOSBuild="" 81 | 82 | #Name of the OS Installer Package 83 | PackageName="" 84 | 85 | #Location of cached Installer 86 | CachedmacOSFile="/Library/Application Support/Jamf/Waiting Room/$PackageName" 87 | 88 | #Expected Size of the Cached installer 89 | CachedFileSize="" 90 | 91 | ## Free space needed to cache package measured in Gigibytes 92 | ## Found by taking the amount of GB needed, converting to Gi, and rounding to the next whole number 93 | ## GB * (1000^3) / (1024^3) 94 | needed_free_space="" 95 | 96 | #Catalina Cache Trigger 97 | cachemacOS="cache$macOSName" 98 | ;; 99 | 100 | "BigSur" ) 101 | #Version of the macOS to be installed 102 | macOSVersion="" 103 | 104 | #Version of the macOS Installer Application (CFBundleVersion) 105 | InstallerVersion="" 106 | 107 | #Build of the macOS to be installed 108 | macOSBuild="" 109 | 110 | #Name of the OS Installer Package 111 | PackageName="" 112 | 113 | #Location of cached Installer 114 | CachedmacOSFile="/Library/Application Support/Jamf/Waiting Room/$PackageName" 115 | 116 | #Expected Size of the Cached installer 117 | CachedFileSize="" 118 | 119 | ## Free space needed for install measured in Gigibytes 120 | ## Found normally by taking the amount of GB needed for the pkg, converting to Gi, and rounding to the next whole number 121 | ## GB * (1000^3) / (1024^3) 122 | ## However Apple requires much more free space for Big Sur. Apple recommends 26GB but historically more is required. 123 | needed_free_space="45" 124 | 125 | #Big Sur Cache Trigger 126 | cachemacOS="cache$macOSName" 127 | ;; 128 | 129 | "Monterey" ) 130 | #Version of the macOS to be installed 131 | macOSVersion="" 132 | 133 | #Version of the macOS Installer Application (CFBundleVersion) 134 | InstallerVersion="" 135 | 136 | #Build of the macOS to be installed 137 | macOSBuild="" 138 | 139 | #Name of the OS Installer Package 140 | PackageName="" 141 | 142 | #Location of cached Installer 143 | CachedmacOSFile="/Library/Application Support/Jamf/Waiting Room/$PackageName" 144 | 145 | #Expected Size of the Cached installer 146 | CachedFileSize="" 147 | 148 | ## Free space needed for install measured in Gigibytes 149 | ## Found normally by taking the amount of GB needed for the pkg, converting to Gi, and rounding to the next whole number 150 | ## GB * (1000^3) / (1024^3) 151 | ## However Apple requires much more free space for Monterey. Apple recommends 26GB but historically more is required. 152 | needed_free_space="35" 153 | 154 | #Monterey Cache Trigger 155 | cachemacOS="cache$macOSName" 156 | ;; 157 | 158 | *) 159 | echo "Unknown OS input in parameter 4, exiting with error...." 160 | exit 1 161 | ;; 162 | esac 163 | 164 | ## Get the major version of macOS that computer is upgrading to 165 | macOSUpgradeVersionMajor=$( echo "$macOSVersion" | cut -d. -f1 ) 166 | ## Get the major/minor version of macOS that computer is upgrading to 167 | macOSUpgradeVersion=$( echo "$macOSVersion" | cut -d. -f1,2 ) 168 | ## Get major version of OS X currently running on Mac 169 | osVersMajor=$( sw_vers -productVersion | cut -d. -f1 ) 170 | ## Get minor version of OS X currently running on Mac 171 | osVersMinor=$( sw_vers -productVersion | cut -d. -f2 ) 172 | ## Get major/minor version of OS X currently running on Mac 173 | osVersFull=$( sw_vers -productVersion | cut -d. -f1,2 ) 174 | ## Variable to see if the installed is cached 175 | macOSIsCached="" 176 | 177 | ## Free space on target disk measured in Gigibytes 178 | available_free_space=$(df -g / | tail -1 | awk '{print $4}') 179 | 180 | ## JAMF and Cocoa Dialog Stuff 181 | JAMFHelperPath="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper" 182 | JAMFHelperIcon="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertStopIcon.icns" 183 | JAMFHelperTitle="WPP: macOS $macOSName Update" 184 | JAMFHelperHeading="$macOSName Update" 185 | JAMFHelperTextAlignment="left" 186 | JAMFHelperHeaderAlignment="left" 187 | 188 | ## Location of the macOS Upgrade deferral counter 189 | UpdateAttemptsFile="/etc/.macOSUpdateAttempts.txt" 190 | 191 | ## Check to see if any remnants from a previous OS upgrade deferral process exist. Remove them if they do. 192 | ScriptLogging "Removing previous OS upgrade deferral file." 193 | if [ -e "$UpdateAttemptsFile" ] 194 | then 195 | rm -rf $UpdateAttemptsFile 196 | fi 197 | 198 | ######################### JAMF Binary Check ############################################ 199 | 200 | if [ -e /usr/local/jamf/bin/jamf ] 201 | then 202 | # JAMF Binary found at 9.81 or later location 203 | ScriptLogging "JAMF Binary found at 9.81 or later location" 204 | jamfBinary="/usr/local/jamf/bin/jamf" 205 | # 206 | elif [ -e /usr/local/bin/jamf ] 207 | then 208 | # Alias to the JAMF Binary found 209 | ScriptLogging "Alias to the JAMF Binary found" 210 | jamfBinary="/usr/local/bin/jamf" 211 | # 212 | else 213 | ScriptLogging "JAMF Binary not found" 214 | fi 215 | 216 | ######################### Existing macOS Downloads Check ############################### 217 | ## Check to see if previous macOS installers have been downloaded and if so remove them. 218 | ## by checking to see if the version of the installer matches the version of the OS we want to install 219 | ## so we don't delete a valid installer unnecessarily. 220 | 221 | for OSInstaller in /Applications/*Install\ macOS* 222 | do 223 | if [[ -e "$OSInstaller" ]] 224 | then 225 | InstallerBundleVersion=$(defaults read "$OSInstaller"/Contents/Info.plist CFBundleVersion) 226 | if [[ "$InstallerBundleVersion" != "$InstallerVersion" ]] 227 | then 228 | ScriptLogging "Old $OSInstaller found. Deleting." 229 | rm -rf "$OSInstaller" 230 | fi 231 | fi 232 | done 233 | 234 | ######################### Free Space Check ############################################### 235 | FreeSpaceCheck() 236 | { 237 | ## If installing macOS 11 or greater, the needed_free_space value is kept as indicated in the case statement 238 | ## at the top of this script. Otherwise, it is set below as follows: 239 | ## If the machine is currently running Yosemite or lower than it needs 19GB of free space prior to 240 | ## upgrading otherwise it needs 13GB. 241 | 242 | if [[ "$macOSUpgradeVersionMajor" -lt "11" ]] 243 | then 244 | if [[ "$osVersMinor" -le "10" ]] 245 | then 246 | needed_free_space="19" 247 | else 248 | needed_free_space="13" 249 | fi 250 | fi 251 | 252 | ## Get the size of the cached macOS Installer in GB. 253 | ## Since this simplified division on returns whole numbers we'll pad the result by 1 GB account for files 254 | ## that night be a bit bigger. This also accounts for some disparities between the Finder and the shell 255 | CachedGBSize=$(( (CachedFileSize / 1024 / 1024 / 1024) + 1 )) 256 | 257 | ## Now add this number to the amount of free space needed to install macOS so we can determine the additional 258 | ## amount of free space needed to cache the installer and then install it. 259 | cached_free_space=$(( CachedGBSize + needed_free_space )) 260 | 261 | ## Free space on target disk measured in Gigibytes 262 | available_free_space=$(df -g / | tail -1 | awk '{print $4}') 263 | 264 | ## Check if sufficient space for caching macOS installation. Checking for enough free space to both store the cached package AND run it 265 | ## later ensures that there is enough space if installation will take place shortly after the package is cached locally. 266 | if [[ "$available_free_space" -ge "$cached_free_space" ]] 267 | then 268 | ScriptLogging "Needed free space to cache and upgrade from macOS $osVersMajor.$osVersMinor to $macOSVersion set to $cached_free_space GB." 269 | ScriptLogging "$available_free_space gigabytes found as free space on boot drive." 270 | ScriptLogging "Sufficient free disk to cache $macOSVersion." 271 | else 272 | ScriptLogging "Needed free space to cache and upgrade from macOS $osVersMajor.$osVersMinor to $macOSVersion set to $cached_free_space GB." 273 | ScriptLogging "$available_free_space gigabytes found as free space on boot drive." 274 | ScriptLogging "Not enough free space to cache macOS. Displaying message to user." 275 | DiskSpaceMSG="Your Mac does not have enough 276 | free space to upgrade to 277 | macOS $macOSName $macOSVersion 278 | 279 | At least $cached_free_space GB of free space is needed. 280 | 281 | Please back up and remove files that are 282 | no longer needed so that the installer may run." 283 | UserResponse=$("$JAMFHelperPath" -windowType utility -title "$JAMFHelperTitle" \ 284 | -icon "$JAMFHelperIcon" -heading "$JAMFHelperHeading" -alignHeading left -description "$DiskSpaceMSG" \ 285 | -alignDescription left -button1 "Quit" ) 286 | if [ $UserResponse == 0 ] || [ $UserResponse == 239 ] 287 | then 288 | ScriptLogging "User acknowledged disk space alert. Exiting with error code...." 289 | exit 1 290 | fi 291 | fi 292 | } 293 | 294 | ######################### Cache Check ################################################### 295 | checkmacOSCache() 296 | { 297 | 298 | ## Boot Drive Format Check 299 | ## macOS Upgrades from 10.13+ require the drive that it is to be installed on (for the purposes of this script 300 | ## that is the boot drive) to be formatted as APFS. Their installers have also removed the ability to convert the drive 301 | ## during unattended installs. If the current boot drive is not formatted as APFS we don't cache the installer and notify the user. 302 | 303 | BootDriveFormat=$(/usr/libexec/PlistBuddy -c "print :FilesystemType" /dev/stdin <<< $(diskutil info -plist /)) 304 | 305 | if [[ "$BootDriveFormat" == "apfs" ]] 306 | then 307 | ScriptLogging "Boot drive is formatted as $BootDriveFormat." 308 | else 309 | ScriptLogging "Boot drive is formatted as $BootDriveFormat. Unable to upgrade to $macOSVersion." 310 | FormatMSG="Unable to upgrade to macOS $macOSName $macOSVersion 311 | 312 | The boot drive on this Mac is 313 | currently formatted as \"$BootDriveFormat\". 314 | 315 | It must be reformatted to APFS. 316 | 317 | Please contact your local support team to assist." 318 | UserResponse=$("$JAMFHelperPath" -windowType utility -title "$JAMFHelperTitle" \ 319 | -icon "$JAMFHelperIcon" -heading "$JAMFHelperHeading" -alignHeading left -description "$FormatMSG" \ 320 | -alignDescription left -button1 "Quit" ) 321 | if [ $UserResponse == 0 ] || [ $UserResponse == 239 ] 322 | then 323 | ScriptLogging "User acknowledged format alert. Exiting with error code...." 324 | exit 1 325 | fi 326 | fi 327 | 328 | if [ -e "$CachedmacOSFile" ] 329 | then 330 | if [[ `stat -f %z "$CachedmacOSFile"` -ge "$CachedFileSize" ]] 331 | then 332 | ScriptLogging "$macOSName $macOSVersion Cached Fully" 333 | macOSIsCached="Yes" 334 | ## Added to create a dummy receipt to allow users to install from Self Service if they chose to defer the update. 335 | ## Dummy receipt depends on if macOS is cached correctly. 336 | if [ ! -e /Library/Application\ Support/JAMF/Receipts/"$macOSName"-"$macOSVersion"_SS.pkg ]; then 337 | ScriptLogging "Creating dummy receipt for Self Service Policy." 338 | touch /Library/Application\ Support/JAMF/Receipts/"$macOSName"-"$macOSVersion"_SS.pkg 339 | ScriptLogging "Running a recon." 340 | "$jamfBinary" recon 341 | fi 342 | ## Exit the script here so the additional cache check is not performed. 343 | exit 0 344 | else 345 | ScriptLogging "$macOSName $macOSVersion not cached correctly. Removing failed download attempts and recaching" 346 | rm -r "$CachedmacOSFile" 347 | rm -r "$CachedmacOSFile.cache.xml" &> /dev/null 348 | macOSIsCached="No" 349 | "$jamfBinary" policy -event "$cachemacOS" 350 | fi 351 | else 352 | ScriptLogging "$macOSName $macOSVersion installer not Found. Caching." 353 | macOSIsCached="No" 354 | "$jamfBinary" policy -event "$cachemacOS" 355 | fi 356 | 357 | ##Check to see if the cache policy finished and if so create a dummy receipt to use for a Self Service install, 358 | if [[ `stat -f %z "$CachedmacOSFile"` -ge "$CachedFileSize" ]] 359 | then 360 | ScriptLogging "$macOSName $macOSVersion was cached successfully." 361 | macOSIsCached="Yes" 362 | ScriptLogging "Creating dummy receipt for Self Service Policy." 363 | touch /Library/Application\ Support/JAMF/Receipts/"$macOSName"-"$macOSVersion"_SS.pkg 364 | ScriptLogging "Running a recon." 365 | "$jamfBinary" recon 366 | fi 367 | } 368 | 369 | ## Check to see if the computer is running the same major version of macOS as the update (in case it was updated by other means). 370 | ## Then check if the computer has an OS Installer in the applications folder. Which would mean it is the correct 371 | ## version of the OS update to be run (an earlier versions did not get removed). 372 | ## If either of those conditions are true update the computer's status in Jamf. If not check to see if the macOS installer is cached. 373 | 374 | ## Due to changes in Apple's number scheme the major version number starting in macOS 11 is the first digit so we must account for that. 375 | if [[ "$macOSUpgradeVersionMajor" -ge "11" ]] 376 | then 377 | macOSUpgradeVersion="$macOSUpgradeVersionMajor" 378 | fi 379 | 380 | if echo "$osVersFull $macOSUpgradeVersion" | awk '{exit $1>=$2?0:1}' 381 | then 382 | ScriptLogging "This Mac is already running macOS $osVersFull which is the same major version as the upgrade." 383 | "$jamfBinary" recon 384 | elif [[ -e "/Applications/Install macOS $macOSName.app" ]] 385 | then 386 | ScriptLogging "This Mac has the correct version of \"Install macOS $macOSName.app\" in the Applications folder." 387 | "$jamfBinary" recon 388 | else 389 | ScriptLogging "Checking to see if $PackageName is cached." 390 | FreeSpaceCheck 391 | checkmacOSCache 392 | fi 393 | --------------------------------------------------------------------------------