1#!/bin/bash
2
3
4# While static groups within JAMF PRO are typically thought of as
5# less-uesful than their more dynamic counterparts known as smart
6# groups, I have found they still serve a critical role. This is
7# especially so for those who extract the most from JAMF PRO via a
8# series of API scripts. Those API script often return a list of
9# computer IDs or names. But those lists usually need to be acted
10# upon in some way. Being able to easily create a static group within
11# JAMF PRO can make that happen.
12
13# That is why I wrote this script. It allows for the quick creation
14# and/or re-population of a static group based on a string of computer
15# IDs or names
16
17# Author: Andrew Thomson
18# Date: 02-10-2017
19# GitHub: https://github.com/thomsontown
20# Version: 1.02 (12-11-2019)
21
22
23# uncomment to override variables
24# JSS_USER=""
25# JSS_PASSWORD=""
26# JSS_AUTH=""
27
28
29function addGroupMembers() {
30
31 local GROUP_ID="$1"
32 local GROUP_NAME="$2"
33 local COUNT=1
34
35 # create opening xml for replacing members of the static group
36 XML_COMPUTER_TEMPLATE="<computer_group><id>$GROUP_ID</id><name>$GROUP_NAME</name><computers>"
37
38 # enumerate computers to re-populate static group
39 for COMPUTER in ${COMPUTERS[@]}; do
40
41 progressBar "${#COMPUTERS[@]}" "$((COUNT++))"
42
43 # derermine if computer id (integer) or name (alpha) was provided
44 if isInteger "$COMPUTER"; then
45 XML_COMPUTER=$(/usr/bin/curl -X GET -H "Content-Type: application/xml" -H "Authorization: Basic $JSS_AUTH" -s "${JSS_URL%/}/JSSResource/computers/id/$COMPUTER/subset/general" 2> /dev/null)
46
47 COMPUTER_NAME=$(echo $XML_COMPUTER | /usr/bin/xpath "/computer/general/name/text()" 2> /dev/null)
48 COMPUTER_SN=$(echo $XML_COMPUTER | /usr/bin/xpath "/computer/general/serial_number/text()" 2> /dev/null)
49 COMPUTER_ID="$COMPUTER"
50 else
51 XML_COMPUTER=$(/usr/bin/curl -X GET -H "Content-Type: application/xml" -H "Authorization: Basic $JSS_AUTH" -s "${JSS_URL%/}/JSSResource/computers/name/$COMPUTER/subset/general" 2> /dev/null)
52
53 COMPUTER_ID=$(echo $XML_COMPUTER | /usr/bin/xpath "/computer/general/id/text()" 2> /dev/null)
54 COMPUTER_SN=$(echo $XML_COMPUTER | /usr/bin/xpath "/computer/general/serial_number/text()" 2> /dev/null)
55 COMPUTER_NAME="$COMPUTER"
56 COMPUTER_NAME=$(encodeUrl "$COMPUTER_NAME")
57 fi
58
59 # verify computer details were found
60 if [ -z "$COMPUTER_SN" ] || [ -z "$COMPUTER_ID" ] || [ -z "$COMPUTER_NAME" ]; then
61 echo -e "
62ERROR: Unable to retrieve info for computer [$COMPUTER]." >&2
63 continue
64 fi
65
66 # insert xml requied for each computer be a member of the static group
67 XML_COMPUTER_TEMPLATE+="<computer><id>$COMPUTER_ID</id><name>$COMPUTER_NAME</name><serial_number>$COMPUTER_SN</serial_number></computer>"
68 done
69
70 # close out the xml for re-populating members of the static group
71 XML_COMPUTER_TEMPLATE+="</computers></computer_group>"
72
73 # verify xml formatting before submitting
74 if ! XML_COMPUTER_TEMPLATE=$(echo $XML_COMPUTER_TEMPLATE | /usr/bin/xmllint --format -); then
75 echo "ERROR: Imporperly formatted data structure found." >&2
76 return $LINENO
77 fi
78
79 # upload xml to re-populate the computers in the static group
80 HTTP_CODE=$(/usr/bin/curl -X PUT -H "Content-Type: application/xml" -w "%{http_code}" -H "Authorization: Basic $JSS_AUTH" -d "$XML_COMPUTER_TEMPLATE" -o /dev/null -s "${JSS_URL%/}/JSSResource/computergroups/id/$GROUP_ID" 2> /dev/null)
81 if [ "$HTTP_CODE" -ne "201" ]; then
82 echo "ERROR: Unable to replace computers in static group." >&2
83 return $LINENO
84 fi
85}
86
87
88function createStaticGroup() {
89
90 # $1 = Name of static group to create. (My Amazing Group)
91 local GROUP_NAME="$1"
92
93 # query jss for existing group name to get id
94 GROUP_ID=$(/usr/bin/curl -X GET -H "Accept: application/xml" -H "Authorization: Basic $JSS_AUTH" -s "${JSS_URL%/}/JSSResource/computergroups/name/$GROUP_NAME" | /usr/bin/xpath "//computer_group/id/text()" 2> /dev/null)
95
96 # create new group if no existing one can be found
97 if [ -z "$GROUP_ID" ]; then
98
99 # minimal xml required to create static group
100 XML_GROUP_TEMPLATE="<computer_group><id>0</id><name>$GROUP_NAME</name><is_smart>false</is_smart></computer_group>"
101
102 # upload xml to create static group
103 GROUP_ID=$(/usr/bin/curl -X POST -H "Content-Type: application/xml" -H "Authorization: Basic $JSS_AUTH" -d "${XML_GROUP_TEMPLATE}" -s "${JSS_URL%/}/JSSResource/computergroups/id/0" | /usr/bin/xpath "//computer_group/id/text()" 2> /dev/null)
104
105 # display error or return group id
106 if [ -z "$GROUP_ID" ]; then
107 echo "ERROR: Unable to create JSS computer group." >&2
108 return $LINENO
109 fi
110 fi
111
112 echo "$GROUP_ID"
113 return 0
114}
115
116
117function encodeBasicAuthorization() {
118
119 # $1 = User name for JSS query. (jsmith)
120 # $2 = Password for JSS query. (itisasecret)
121 local USERNAME="$1"
122 local PASSWORD="$2"
123
124 if [ -z "$USERNAME" ] || [ -z "$PASSWORD" ]; then
125 writeLog "ERROR: Missing parameter(s)." >&2
126 return $LINENO
127 fi
128
129 if ! AUTHORIZATION=$(/usr/bin/printf "$USERNAME:$PASSWORD" | /usr/bin/iconv -t ISO-8859-1 2> /dev/null | /usr/bin/base64 -i - 2> /dev/null); then
130 writeLog "ERROR: Unable to encode authorization credentials [$?]." >&2
131 return $LINENO
132 else
133 echo "$AUTHORIZATION"
134 return 0
135 fi
136}
137
138
139function encodeUrl() {
140
141 # $1 = Universal Resource Locator to be encoded. (https://www.company.com/some/path/index.htm)
142 local URL="$1"
143 local PREFIX=$(echo $URL | /usr/bin/grep -Eoi "^(afp|file|ftp|http|https|smb)://" 2> /dev/null)
144 local LENGTH=${#PREFIX}
145 local SUFFIX=${URL:$LENGTH}
146 local ENCODED=""
147
148 if [ -z "$SUFFIX" ]; then
149 echo "ERROR: Specified URL is incomplete."
150 return $LINENO
151 fi
152
153 while [ -n "$SUFFIX" ]; do
154 TAIL=${SUFFIX#?}
155 HEAD=${SUFFIX%$TAIL}
156 case $HEAD in
157 [-._~0-9A-Za-z/:]) ENCODED+=$(/usr/bin/printf %c "$HEAD");; # encoding not required
158 *) ENCODED+=$(/usr/bin/printf %%%02x "'$HEAD") # encoding required
159 esac
160 SUFFIX=$TAIL
161 done
162 echo "${PREFIX}$ENCODED"
163}
164
165
166function initJamf() {
167
168 # verify encoded authorization
169 if [ -z "$JSS_AUTH" ]; then
170
171 # verify username and password for jss
172 if [ -n "$JSS_USER" ] || [ -z "$JSS_PASSWORD" ]; then
173 echo "ERROR: A username or password to access to the JSS was not specified." >&2
174 return $LINENO
175 fi
176
177 if declare -f encodeBasicAuthorization &> /dev/null; then
178 if ! JSS_AUTH=$(encodeBasicAuthorization "$JSS_USER" "$JSS_PASSWORD"); then
179 return $LINENO
180 fi
181 else
182 echo "ERROR: Unable to encode username and password to access the JSS." >&2
183 return $LINENO
184 fi
185 fi
186
187 # get jss url
188 JSS_URL=$(/usr/bin/defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url 2> /dev/null)
189 if [ -z "JSS_URL" ]; then
190 echo "ERROR: A a valid URL to the JSS was not specified [JSS_URL]." >&2
191 return $LINENO
192 fi
193
194 # verify jss is available
195 JSS_CONNECTION=$(/usr/bin/curl --connect-timeout 10 -H "Authorization: Basic $JSS_AUTH" -sw "%{http_code}" ${JSS_URL%/}/JSSCheckConnection -o /dev/null)
196 if [ $JSS_CONNECTION -ne 200 ] && [ $JSS_CONNECTION -ne 403 ] ; then
197 echo "ERROR: Unable to connect to JSS [$JSS_URL]." >&2
198 return $LINENO
199 fi
200}
201
202
203function isInteger() {
204
205 return $([ "$@" -eq "$@" ] 2> /dev/null)
206}
207
208
209function main() {
210
211 while [ -n "$1" ]; do
212 case $1 in
213 -n | --name ) shift;GROUP_NAME="$1" ;;
214 -f | --file ) shift;FILE_PATH="$1" ;;
215 -u | --user ) shift;JSS_USER="$1" ;;
216 -p | --pass ) shift;JSS_PASSWORD="$1" ;;
217 -a | --auth ) shift;JSS_AUTH="$1" ;;
218 -h | --help ) shift;usage ;;
219 * ) COMPUTERS+=( "$1" )
220 esac
221 shift
222 done
223
224 # load common source variables
225 if [ -f ~/.bash_source ]; then
226 source ~/.bash_source
227 fi
228
229 # prompt for password if missing
230 if [ -z "$JSS_AUTH" ] && [ -n "$JSS_USER" ] && [ -z "$JSS_PASSWORD" ]; then
231 echo "Please enter JSS password for account [$JSS_USER]: "
232 read -s JSS_PASSWORD
233 fi
234
235 # verify user and password or encoded authorization
236 if [ -z "$JSS_USER" ] || [ -z "$JSS_PASSWORD" ] && [ -z "$JSS_AUTH" ]; then
237 echo "ERROR: A JSS user name and password are required if no encoded authorization is provided." >&2
238 usage
239 exit $LINENO
240 fi
241
242 # verify group name was specified
243 if [ -z "$GROUP_NAME" ]; then
244 echo "Please enter a name for the group you wish to create/populate: "
245 read GROUP_NAME
246 if [ -z "$GROUP_NAME" ]; then
247 echo "ERROR: No group name was specified." >&2
248 exit $LINENO
249 fi
250 fi
251
252 # verify member source
253 if [ ! -r "$FILE_PATH" ] && [ "${#COMPUTERS[@]}" -eq "0" ]; then
254 echo "Please drag-n-drop or enter the path to a line-seperated file containing computer names or IDs: "
255 read FILE_PATH
256 if [ ! -r "$FILE_PATH" ]; then
257 echo "ERROR: The path to the specified readable file cound not be found [$FILE_PATH]." >&2
258 exit $LINENO
259 fi
260 fi
261
262 # populate array with computer names from file
263 if [ -r "$FILE_PATH" ]; then
264 while IFS= read -r COMPUTER; do
265 COMPUTERS+=($COMPUTER)
266 done < "$FILE_PATH"
267 fi
268
269 # obsure password for debug
270 for ((i=1; i<${#JSS_PASSWORD}; i++)); do
271 JSS_OBSURED+="◦"
272 done
273
274 # display parameters for debug
275 if $DEBUG; then
276 echo -e "NAME: $GROUP_NAME
277FILE: $FILE_PATH
278USER: $JSS_USER
279PASS: $JSS_OBSURED
280AUTH: $JSS_AUTH
281COMPUTERS: ${COMPUTERS[@]}"
282 fi
283
284 # initialize and verify jamf connection
285 if ! initJamf; then exit $LINENO; fi
286
287 # create/get computer group id
288 if ! GROUP_ID=$(createStaticGroup "$GROUP_NAME"); then exit $LINENO; fi
289
290 # add each computer to group
291 addGroupMembers "$GROUP_ID" "$GROUP_NAME"
292
293 # play sound to signal completion
294 playSound
295}
296
297
298function playSound() {
299
300 # $1 = Path to sound file. (/System/Library/Sounds/Glass.aiff)
301 local SOUND_FILE_PATH="$1"
302
303 if [ -z "$SOUND_FILE_PATH" ] && [ -f "/System/Library/Sounds/Glass.aiff" ]; then
304 local SOUND_FILE_PATH="/System/Library/Sounds/Glass.aiff"
305 fi
306
307 if [ -f "$SOUND_FILE_PATH" ]; then
308 /usr/bin/afplay "$SOUND_FILE_PATH" &> /dev/null
309 fi
310}
311
312
313function progressBar() {
314
315 # $1 = Maximum count of progress. (75)
316 # $2 = Current count of progress. (10)
317 local MAX="$1"
318 local COUNT="$2"
319
320 # verify required parameters
321 if [ -z "$MAX" ] || [ -z "$COUNT" ]; then
322 echo "ERROR: Missing parameter."
323 return $LINENO
324 fi
325
326 # verify parameters are integers
327 if [ "$MAX" -ne "$MAX" ] 2> /dev/null || [ "$COUNT" -ne "$COUNT" ] 2> /dev/null; then
328 echo "ERROR: Integer expression expected." >&2
329 return $LINENO
330 fi
331
332 if [ "$COUNT" -lt "$MAX" ]; then
333 # display progress
334 PERCENT_COMPLETE=$(echo "(100/$MAX)*$COUNT" | /usr/bin/bc -l | /usr/bin/awk '{print int($1+0.5)}')
335 PROGRESS_DONE=$(echo "$PERCENT_COMPLETE/2" | /usr/bin/bc -l | /usr/bin/awk '{print int($1+0.5)}')
336 PROGRESS_LEFT=$(( 50 - $PROGRESS_DONE ))
337 DONE_PATTERN=$(/usr/bin/printf "%${PROGRESS_DONE}s") # % number "done" of blanks
338 LEFT_PATTERN=$(/usr/bin/printf "%${PROGRESS_LEFT}s") # % number "left" of blanks
339
340 /usr/bin/printf "
341Processing: [${DONE_PATTERN// /#}${LEFT_PATTERN// /-}] ${PERCENT_COMPLETE}%%" # replace blanks with patterns
342 else
343 /usr/bin/printf "
344Processing: [##################################################] 100%%
345"
346 fi
347}
348
349
350function usage() {
351
352 echo
353 echo "Usage: ./${0##*/} -n "NAME" -f "FILE" -u "USER" -p "PASSWORD" -a "AUTHENTICATION" -h <computer_name> <computer_name>... OR <computer_id> <computer_id>..."
354 echo
355 echo -e " -n | --name Specify a static group name to create (required)"
356 echo -e " -f | --file Specify a line-seperated file containing computer names or ids (optional)"
357 echo -e " -u | --user Specify a JSS user name with access to create computer groups (required)"
358 echo -e " -p | --pass Specify a JSS password for the user name specified above (optional)"
359 echo -e " -a | --auth Specify encoded JSS authorization credentials (optional)"
360 echo -e " If --auth is spcified then user and password are optional."
361 echo -e " <computer_name> Computer names can be included as parameters on the command line (optional)"
362 echo -e " <computer_id> Computer ids can be included as parameters on the command line (optional)"
363 echo -e " -h | --help Display help"
364}
365
366
367# run main if called directly
368if [[ "$BASH_SOURCE" == "$0" ]]; then
369 main $@
370fi