How do you deploy software updates???

ooshnoo
Valued Contributor

Just wanted to get a general consensus as to what method people prefer when running software updates on their clients.

Options I've seen so far are:

  1. Download the packages from Apple and push them out like any other policy.
  2. Enable the "Software Update" payload in a policy and install that way
  3. Run the softwareupdate -i -a command via script or as part of the "files and processes" payload in a policy.

Your preference???

23 REPLIES 23

jwojda
Valued Contributor II

There is:
Patchoo - https://jamfnation.jamfsoftware.com/discussion.html?id=10636

Munki - https://code.google.com/p/munki/
And various scripts on this site...

sean
Valued Contributor

Internal software update server.
Touch a flag file on the machines, checked at logout.
Use iHook to inform the user what is going on.

ctangora
Contributor III

Twice a day we will display a message and open Self Service for the end-user to install any cached packages and ASU's. Once they install all cached updates and all software updates, we stop harassing them.

Of course if there is a security issue, we just push it and let them know we are pushing it.

donmontalvo
Esteemed Contributor III

@ooshnoo FWIW, might want to peruse this thread... :)

Will JSS 10 finally bring us easy patch management?
https://jamfnation.jamfsoftware.com/discussion.html?id=10961

--
https://donmontalvo.com

ooshnoo
Valued Contributor

Thanks fellas... I've seen Munki and Patchoo, but unfortunately I'm a one man show here, managing over 500 Macs and 3000 Windows machines using SCCM, so my time is very limited and have not been able to implement, let alone research either of the two. So I'm kind of at the mercy of the built in Casper tools at this point.

kish_jayson
Contributor

We do software and security patching once every 30 days, so we created a Once Per Computer policy that's scoped to the All Managed Clients smart group. Using that policy we direct all of the client machines to an internal Software Update Server (Mac mini, Software Update Service enabled, managed manually).

This allows us to automatically download a local copy of all updates Apple releases to the server, but only enable the ones that we want our clients to get during that time. Once we're ready, we simply flush the policy log and the Casper agent does the rest.

donmontalvo
Esteemed Contributor III

@ooshnoo Using a supported product has a tendency to free up more of your time for other stuff.

Thanks fellas... I've seen Munki and Patchoo, but unfortunately I'm a one man show here, managing over 500 Macs and 3000 Windows machines using SCCM, so my time is very limited and have not been able to implement, let alone research either of the two. So I'm kind of at the mercy of the built in Casper tools at this point.
--
https://donmontalvo.com

jwojda
Valued Contributor II

I'd use the built in tools for now as a stop gap, but definitely look at the 3rd party stuff for long term.
There's quite a bit you can do to automate things, even automate the automated package building (Jenkins and AutoPKG). A few hours up front spent can save you countless hours tomorrow.

yadin
Contributor

Thread necromancy I know, but I'm having way too much trouble finding what should be a simple answer and this thread does not give me hope. Where is the simple option that really has to exist that is "every day at noon check for software updates and install them if available"? I mean, this is basic system management built into Windows GP, I can't believe for a second you can't do this on Mac with an expensive MDM...

stevewood
Honored Contributor II
Honored Contributor II

@ebonweaver short answer, there isn't one. You could get close to noon, or whatever time, by using the Client Side Limitations of a policy. But the jamf binary randomizes the time that it runs, so it may not be right at noon, it may be shortly after noon, or before.

The better answer would be to use a LaunchDaemon on the system that is scheduled to run right at noon. This would be like running a cron job in the old days before LaunchD.

mm2270
Legendary Contributor III

You can use a script to do this calling the softwareupdate binary, but be aware some updates naturally require restarts. The problem is, when installed from command line softwareupdate, the Mac won't just restart after it installed. It states a restart is needed after installing any updates that require that, but it will be up to you to ensure that actually happens. Otherwise any Macs that install such updates will be left in an odd state that could lead to stability issues for them.

But for basic simplicity, the following command will install all available updates, if any

/usr/sbin/softwareupdate -ia

You can drop that into the Execute Command field in the Files and Processes tab of a policy, as an example, or you could build a more involved script that first checks to see what's available and takes things from there. All this of course requires your Macs to be pointed to some kind of Software Update service, whether something you run internally, or just the default Apple one.

yadin
Contributor

Thanks guys. Real disappointing to hear this simply does not exist. I was baffled why under the Software Update policy/profile settings there simply were no settings, now I know. Other than if you ran your own SUS, which as I understand has been removed from Server... well that pretty much makes this a useless non-setting only there to confuse and disappoint. I guess I shouldn't be too shocked since there's no ability to manage computer names at all, negating the entire purpose of smart groups... but I digress.

signetmac
Contributor

We are waiting on approval for a policy that checks available critical software updates and splashes a jamfhelper dialog box up to the end user. If the employee clicks "Cancel" it executes again the next day. If they cancel too many times, it executes on the hour until they comply.

6ecce13e00ed4d49a4f1831aa5e13fe8

If the update requires a reboot, once the employee clicks "Install" they will get a second dialog box urging them not to reboot their computer until that dialog box quits.

0a19c860aab74bf4a212410dc9053e9a

yadin
Contributor

Signet can you provide information on how you did that? I've never seen anything like that in JAMF, and see no options that would support what you describe.

stevewood
Honored Contributor II
Honored Contributor II

@ebonweaver they are using jamfhelper to provide the dialog boxes: /Library/Application Support/JAMF/bin/jamfhelper.app/Contents/MacOS/jamfhelper

There is a helper GUI app that you can use to build the code necessary:

jHelper-GUI

yadin
Contributor

Steve, I'm referring more to the rest of what he said.
"a policy that checks available critical software updates and splashes a jamfhelper dialog box up to the end user"
How is this policy crafted? What commands are being used in what way? Since you can't seem to do this with Software Update, is this all in a script that is being called from the Policy? Or are other functions being used?
"If the update requires a reboot"
Again, how is this determined? Where does the branching logic happen? If it's all one shell script, can we see said script? If there are multiple moving parts, how do they fit together?

It looks and sounds like an elegant solution, but based on other info I'm not getting how it was created.
Thanks!

signetmac
Contributor

Here you go @ebonweaver

My apologies for the delayed response. I took vacation last Friday to bring my kid back home after his freshman year at CalPoly.

The following script does the trick. The function "execution_period_check" writes a file and adds to it every day the policy runs for the first X days as defined in the global DAILY_LIMIT. Set the policy execution to hourly, and limit the hours to work/school/daylight hours appropriately so the prompt doesn't come up at 1AM. If it detects that it has already been run that day, and it is still on the "daily run cycle" it will exit before any further action is taken. After $DAILY_LIMIT days, it will just run the full script hourly until the person takes action.

The function "remove_from_scope" requires that you have created a blank Integer based Extension Attrib beforehand and given it's ID number and name in the globals. Then create a Smart Group whose membership is computers with numeric values of this EA that are greater than the number you set. To set the number in the Smart Group to compare the EA to when you make the policy live by feeding it the epoch date at that moment

date +%s

...and exclude from the softwareupdate policy members of that Smart Group. On the client computer, the script will write to JSS with an API call the epoch date upon the employee/student successfully executing an install, and thus the computer will drop out of scope.

#!/bin/sh
#set -x
# Written Feb, 2018 by: 
# Mac Scott: [email redacted] and Chris Collins: [email redacted]
# PURPOSE: A good natured, limitless deferral prompt to the computer user to remind them that
#          there are critical security updates that need to be applied.
# Method:  Set the frequency of your policy to hourly. Set the DAILY_LIMIT variable to the 
#          number of days you are willing to prompt only once per day. After the policy has run
#          out the DAILY_LIMIT variable, it will be allowed to execute hourly.

############################
##### Global Variables #####
UPDATES_NO_RESTART=""
RESTART_REQUIRED=""
JAMFHELPER_PATH="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper"

# Set the following for your environment
DAILY_LIMIT=X                                                       # Number of days before going hourly
RUNINDEX="/var/root/Library/Preferences/execution_period"           # Path to file tracking executions
ICON_PATH="/Library/Application Support/JAMF/bin/XXXX.png"          # Corp logo branding option for dialog boxes
UPDATE_TRIGGER="/usr/local/bin/jamf policy -event XXX"              # jamf policy event command to run updates
CONTACT_INFO="IT Support at XXX.XXX.XXXX or it@yourorg.com"         # Contact info for your IT Support center
JSSURL="https://jss.yourorg.com:8443"                               # For curl to record successful execution
EPOCH_EXT_ATTRIB_ID=XX                                              # Extension Attrib ID number for recording success
EPOCH_EXT_ATTRIB_NAME="XXXXXXXX XXXXXXX XXXXXXXX"                   # Extension Attrib name for recording success
API_AUTH="Basic XXXXXXXXXXXXXXXXXXXXXXXXXXX"

############################
######### Functions ########
get_logged_in_user() {
  /usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "
");'
}

execution_period_check() {
  day=$(date '+%d')

  # Check if an RUNINDEX file has been started by a previous execution
  if [[ -e $RUNINDEX ]]; then
    executions=$(wc -l $RUNINDEX | awk '{print $1}')
    last_run=$(tail -1 $RUNINDEX)

    # If it hasn't executed DAILY_LIMIT times, and has executed today, exit 0
    if [[ $executions -le $DAILY_LIMIT ]] || [[ $day -eq $last_run ]]; then
      echo "Has already run today and we are still in the 'daily' cycle. Exiting"
      exit 0
    else
      echo "Hasn't been run in the appropriate period. Executing upgrade check."
    fi
  else
    echo "Hasn't ever been run. Executing upgrade check."
  fi

  # If this execution is within the DAILY_LIMIT then register that it has been run today
  if [[ $executions -lt $DAILY_LIMIT ]]; then
    echo $day >> $RUNINDEX
  fi
}

get_rec_updates() {
  rec_updates=$(softwareupdate --list --recommended)
  UPDATES_NO_RESTART=$(grep recommended <<"$rec_updates" | grep -v restart)
  RESTART_REQUIRED=$(grep restart <<<"$rec_updates" 
    | grep -v '*' 
    | cut -d , -f 1)

  # If there are no recommended updates, quit
  if [[ "$UPDATES_NO_RESTART" == "" ]] && [[ "$RESTART_REQUIRED" == "" ]]; then
    echo "No updates at this time"

    # Computer has no pending recommended updates, so remove computer from scope to prevent repitition
    remove_from_scope
    rm "$RUNINDEX"
    exit 0
  fi
}

decide_updates() {
  local current_user=$1
  model_id=$(sysctl hw.model | awk -F'MacBookPro|,' '{print $2}')

  # if there is no one logged in, just run the updates
  if [[ "$current_user" == "" ]]; then
    $UPDATE_TRIGGER

    # Software updates have finally run, remove computer from scope to prevent repitition
    remove_from_scope
    rm "$RUNINDEX"
    shutdown -r now
  else
    # Someone is logged in. prompt if any updates require a restart
    if [[ "$RESTART_REQUIRED" != "" ]]; then

      # Preconfigure message to end user for dialog box
      # If this is a TouchBar Mac, warn user that update will take longer
      if [[ $model_id -gt 12 ]]; then
        description_text="These updates will require you to restart your Mac and could take 30 minutes. Please click 'Install' if you would like to do this now. If you have concerns, please contact $CONTACT_INFO"
      else
        description_text="These updates will require you to restart your Mac. Please 
click 'Install' if you would like to do this now. If you have concerns, please contact $CONTACT_INFO"
      fi

      # Execute jamfhelper dialog box, and collect result to variable
      install_choice=$("$JAMFHELPER_PATH" -windowType hud -icon "$ICON_PATH" -heading "Critical Security Updates are Available" -description "$description_text

The following updates require a restart of your Mac: 
$RESTART_REQUIRED" -button1 "Install" -button2 "Cancel" -cancelButton "2" -defaultButton 2)

      if [[ "$install_choice" == "0" ]]; then
        # Restart is required, so throw up another dialog box telling user that
        # the dialog box will disappear when updates are staged/complete, and 
        # they should reboot the computer at that time.
        run_updates
      else
        exit 1
      fi
    else
      $UPDATE_TRIGGER 

      # Software updates have finally run, remove computer from scope to prevent repitition
      remove_from_scope
      rm "$RUNINDEX"
    fi
  fi
}

run_updates() {

   "$JAMFHELPER_PATH" -windowType hud -lockHUD -heading "Security Updates have begun" -description "Your security updates are being applied. Please do not turn off this computer until this dialog box disappears. This message will go away when updates are complete. Note: the initial restart may take longer depending on the updates applied." -icon "$ICON_PATH" > /dev/null 2>&1 &

    # We'll need the pid of jamfHelper to kill it once the updates are complete
    jamf_helper_PID=$(echo "$!")

    $UPDATE_TRIGGER & 
    # Get the Process ID of the last command run in the background ($!) 
    # and wait for it to complete (wait)
    softw_upd_PID=$(echo "$!")
    wait $softw_upd_PID

    # Kill the jamfHelper. If a restart is needed, the user will be prompted. 
    # If not the hud will just go away 
    kill -s KILL $jamf_helper_PID

    # Software updates have finally run, remove computer from scope to prevent repitition
    remove_from_scope
    rm "$RUNINDEX"
    exit 0
}

remove_from_scope() {
  # Solves the problem of "How do I run a policy at a continuous interval until first successful completion?"
  # This function writes the epoch date of script's SUCCESSFUL execution to an ext attrib on JSS defined as an
  # Integer value. If you define a Smart Group whose members are computers with a value higher than the epoch
  # of the moment you enable a policy, then you can exclude members of this group from a policy's scope, such
  # that the policy will continue to run at whatever frequency until successful, and then drop out of scope. 

  # Globals are in ALL_CAPS and need to be defined either in this function or in the script's globals.
  epoch=$(date +%s)
  udid=$(system_profiler SPHardwareDataType 
        | grep UUID 
        | awk '{print $3}')

  curl -k -s -X "PUT" "$JSSURL/JSSResource/computers/udid/$udid/subset/extension_attributes" 
    -H "Content-Type: application/xml" 
    -H "Authorization: $API_AUTH" 
    -H "Accept: application/xml" 
    -d "<computer><extension_attributes><extension_attribute><id>$EPOCH_EXT_ATTRIB_ID</id><name>$EPOCH_EXT_ATTRIB_NAME</name><type>String</type><value>$epoch</value></extension_attribute></extension_attributes></computer>" 
    & > /dev/null 2>&1
}

main() {
  execution_period_check
  get_rec_updates
  logged_in_user=$(get_logged_in_user)
  decide_updates $logged_in_user
}

############################
######## Execution #########

main

rderewianko
Valued Contributor II

We take an approach of. We give our staff a weeks notice to do os updates on their own.. And after that we'll assist them by giving them notice Fri eve and to the nest week that we're updating their machine. The notice gives them 10 minutes to finish off their work before it kicks off a software install.

The following is what we use, this is just the notifier, we automatically install any updates that don't require a reboot, and then upon any of our staff rebooting their macs we update it then. This only furthers that process when they aren't going to reboot.

#!/bin/bash
###################################################################################
## Forced reboot for Software that needs updating
## “Lisa, I’d like to buy your rock.”
## Created: Dec 2017
## Last Edit: Dec 01, 2017
## Created by Ross Derewianko
###################################################################################


/usr/local/bin/jamf recon



echo "prompting user for reboot"
curl -o /var/tmp/<templogo> <logourl>
sudo -u $(ls -l /dev/console | awk '{print $3}')  /Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper -windowType utility -title "Software Update Notification" -timeout 600 -countdown -description "Software Update(s) are ready to install on your machine.  Please save all your work and then click RESTART to proceed.  You have ten minutes to save your work before this window auto-closes and the update begins." -icon "/var/tmp/<templogo>" -button1 "RESTART"
echo "notifying user"
sudo -u $(ls -l /dev/console | awk '{print $3}') osascript -e 'tell application "System Events" to restart'
osascript -e 'tell application "System Events" to tell process "SecurityAgent" to click button "Restart" of window 1'
    sleep 600
    echo "restarting"
    shutdown -r now

      exit 0

StuartMathieson
New Contributor II

@signetmac
This looks like it something we have been looking for but we are a little lost on how to start. Could you explain which policies you have running? How many policies are required? Do we have to fill in the EA manually, then scope the exclusion to anything higher than that EA integer value? Do we need to put our API password in the script in plain text, not too happy about doing this? Anymore info would be great.
Cheers.

signetmac
Contributor

Hi @StuartMathieson

No you don't have to preset the EA. Leave it blank the first time you run it. The Smart Group is where you define the epoch date in order to control the scope. The epoch date as I write this to you is:

US-ML:~ macscott$ date +%s 1535998930

When the Smart Group is scoped for the first time, the blank value will cause the if statement to fail and then the computer will run the policy and set it's own epoch value after it successfully runs software updates, dropping the computer out of scope because that EA value it writes will be higher than the one you defined in the Smart Group.

You don't have to include the API pass in the actual script. The workflow in the script above relies on a hash of the password for simplicity's sake... when I posted this script, I didn't want to make it overly complicated and difficult to understand. But we actually use the workflow defined here by JAMF: Encrypted Script Parameters
You could alternatively (and less securely than the JAMF workflow cited above) simply pass the hashed password when you call the script from the policy in one of the script variables.

kdean
New Contributor III

@signetmac this is awesome, I am testing this out today, I am a little confused about EPOCH. Am I just assigning a random number to EPOCH_EXT_ATTRIB_ID like the number 01 for example? And then for EPOCH_EXT_ATTRIB_NAME the name of the attribute I created in Jamf? And for API_AUTH, can I just use my password here? Am I supposed to put username/password?

kdean
New Contributor III

I put the name of the policy in EPOCH_EXT_ATTRIB_NAME and that worked fine. I left EPOCH_EXT_ATTRIB_ID blank and left API_AUTH and the deferral prompt worked. I deferred for one day. I am not sure if I need to do anything to EPOCH_EXT_ATTRIB_ID and API_AUTH? I guess I don't understand the importance of EPOCH in this script. But this works like a charm and is not intrusive until the end user reaches their daily_limit. This is awesome. Ty @signetmac you are awesome!!!

Edit: Also when did you set the policy to execute? Once per Day, Ongoing, etc?

DaK3ll3r
New Contributor II

@signetmac I know this is almost 2 years old now, but I wanted to adapt and simplify it for our environment. I removed the API call since we'd handle the trigger by scope (ie once a machine has upgraded to Catalina, the policy will be excluded).

I had a questions about these lines, though:

# If it hasn't executed DAILY_LIMIT times, and has executed today, exit 0 if [[ $executions -le $DAILY_LIMIT ]] || [[ $day -eq $last_run ]]; then

Shouldnt it be "if [[ $executions -le $DAILY_LIMIT ]] && [[ $day -eq $last_run ]]; then" instead, since your if statement is inclusive (If x AND Y)?

I also noticed some issues with string substitutions, so needed to add quotes around the $RUNINDEX variable in the following locations:
executions=$(wc -l "$RUNINDEX" | awk '{print $1}')
last_run=$(tail -1 "$RUNINDEX")