#!/usr/bin/python # # Copyright 2009 Greg Neagle. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ managedsoftwareupdate """ import sys import os import optparse import datetime import dateutil.parser import subprocess import time from munkilib import munkicommon from munkilib import updatecheck from munkilib import installer from munkilib import munkistatus from munkilib import appleupdates from munkilib import FoundationPlist def getIdleSeconds(): # stolen from Karl Kuehn -- thanks, Karl! # I'd like to Python-ize it a bit better; calling awk seems unPythonic, but it works. commandString = "/usr/sbin/ioreg -c IOHIDSystem -d 4 | /usr/bin/awk '/Idle/ { print $4 }'" ioregProcess = subprocess.Popen([commandString], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if ioregProcess.wait() != 0: return 0 return int(int(ioregProcess.stdout.read()) / 1000000000) # convert from Nanoseconds def clearLastNotifiedDate(): try: pl = FoundationPlist.readPlist("/Library/Preferences/ManagedInstalls.plist") if pl: if 'LastNotifiedDate' in pl: cmd = ['/usr/bin/defaults', 'delete', '/Library/Preferences/ManagedInstalls', 'LastNotifiedDate'] retcode = subprocess.call(cmd) except: pass def createDirsIfNeeded(dirlist): for directory in dirlist: if not os.path.exists(directory): try: os.mkdir(directory) except: print >>sys.stderr, "Could not create %s" % directory return False return True def initMunkiDirs(): managedinstallprefs = munkicommon.prefs() ManagedInstallDir = managedinstallprefs['ManagedInstallDir'] manifestsdir = os.path.join(ManagedInstallDir, "manifests") catalogsdir = os.path.join(ManagedInstallDir, "catalogs") cachedir = os.path.join(ManagedInstallDir, "Cache") logdir = os.path.join(ManagedInstallDir, "Logs") if not createDirsIfNeeded([ManagedInstallDir, manifestsdir, catalogsdir, cachedir, logdir]): # can't use logerror function since logdir might not exist yet errormessage = "Could not create needed directories in %s" % ManagedInstallDir print >>sys.stderr, errormessage munkicommon.errors = errormessage return False return True def doInstallTasks(): # first, clear the last notified date # so we can get notified of new changes after this round # of installs apple_says_restart = False munki_says_restart = False clearLastNotifiedDate() # are we supposed to handle Apple Software Updates? if munkicommon.pref('InstallAppleSoftwareUpdates'): apple_says_restart = appleupdates.installAppleUpdates() if apple_says_restart: # if Apple updates require a restart, skip the munki installs # until after the restart cmd = ['/usr/bin/touch', '/Users/Shared/.com.googlecode.munki.installatstartup'] retcode = subprocess.call(cmd) else: # install munki updates munki_says_restart = installer.run() if (apple_says_restart or munki_says_restart): munkicommon.log("Software installed or removed requires a restart.") if munkicommon.munkistatusoutput: munkistatus.hideStopButton() munkistatus.message("Software installed or removed requires a restart.") munkistatus.percent(-1) else: print "Software installed or removed requires a restart." sys.stdout.flush() if munkicommon.getconsoleuser() == None: time.sleep(5) retcode = subprocess.call(["/sbin/shutdown", "-r", "now"]) else: if munkicommon.munkistatusoutput: # someone is logged in and we're using munkistatus print "Notifying currently logged-in user to restart." munkistatus.activate() munkistatus.restartAlert() munkicommon.osascript('tell application "System Events" to restart') else: print "Please restart immediately." def notifyUserOfUpdates(manualcheck): # someone is logged in, and we have updates. # if we haven't notified in a while, notify: lastNotifiedString = munkicommon.pref('LastNotifiedDate') daysBetweenNotifications = munkicommon.pref('DaysBetweenNotifications') nowString = munkicommon.NSDateNowString() now = dateutil.parser.parse(nowString) nextNotifyDate = now if lastNotifiedString: lastNotifiedDate = dateutil.parser.parse(lastNotifiedString) interval = datetime.timedelta(days=daysBetweenNotifications) if daysBetweenNotifications > 0: # we make this adjustment so a "daily" notification # doesn't require 24 hours to elapse interval = interval - datetime.timedelta(hours=6) nextNotifyDate = lastNotifiedDate + interval if now >= nextNotifyDate or manualcheck: # record current notification date cmd = ['/usr/bin/defaults', 'write', '/Library/Preferences/ManagedInstalls', 'LastNotifiedDate', '-date', now.ctime()] retcode = subprocess.call(cmd) # notify user of available updates result = munkicommon.osascript('tell application "Managed Software Update" to activate') def main(): # check to see if we're root if os.geteuid() != 0: print >>sys.stderr, "You must run this as root!" exit(-1) # check to see if another instance of this script is running myname = os.path.basename(sys.argv[0]) if munkicommon.pythonScriptRunning(myname): # another instance of this script is running, so we should quit print >>sys.stderr, "Another instance of %s is running. Exiting." % myname exit(0) p = optparse.OptionParser() p.add_option('--auto', '-a', action='store_true', help='''Used by launchd LaunchAgent for scheduled runs. No user feedback or intervention. All other options ignored.''') p.add_option('--logoutinstall', '-l', action='store_true', help='Used by launchd LaunchAgent when running at the loginwindow.') p.add_option('--manualcheck', action='store_true', help='Used by launchd LaunchAgent when checking manually.') p.add_option('--munkistatusoutput', '-m', action='store_true', help='Uses MunkiStatus.app for progress feedback when installing.') p.add_option('--id', default='', help='Alternate identifier for catalog retreival') p.add_option('--quiet', '-q', action='store_true', help='''Quiet mode. Logs messages, but nothing to stdout. --verbose is ignored if --quiet is used.''') p.add_option('--verbose', '-v', action='count', default=1, help='More verbose output. May be specified multiple times.') p.add_option('--checkonly', action='store_true', help="Check for updates, but don't install them.") p.add_option('--installonly', action='store_true', help='Skip checking and install any pending updates.') options, arguments = p.parse_args() if options.auto: # typically invoked by a launch daemon periodically # munkistatusoutput is false for checking, but true for installing options.munkistatusoutput = False options.quiet = True options.checkonly = False options.installonly = False if options.logoutinstall: # typically invoked by launchd agent # running in the LoginWindow context options.munkistatusoutput = True options.quiet = True options.checkonly = False options.installonly = True # if we're running at the loginwindow, let's make sure the user triggered # the update before logging out, or we triggered it before restarting. user_triggered = False triggerfiles = ["/private/tmp/com.googlecode.munki.installatlogout", "/Users/Shared/.com.googlecode.munki.installatstartup"] for f in triggerfiles: if os.path.exists(f): os.unlink(f) user_triggered = True if not user_triggered: exit(0) if options.manualcheck: # triggered by Managed Software Update.app options.munkistatusoutput = True options.quiet = True options.checkonly = True options.installonly = False if options.quiet: options.verbose = 0 if options.checkonly and options.installonly: print >>sys.stderr, "--checkonly and --installonly options are mutually exclusive!" exit(-1) # set munkicommon globals munkicommon.munkistatusoutput = options.munkistatusoutput munkicommon.verbose = options.verbose # create needed directories if necessary if not initMunkiDirs(): exit(-1) if not munkicommon.verbose == 0 : print "Managed Software Update Tool" print "Copyright 2009 The Munki Project" print "http://code.google.com/p/munki\n" updatesavailable = False if not options.installonly: result = updatecheck.check(id=options.id) # record last check date and result nowString = munkicommon.NSDateNowString() cmd = ['/usr/bin/defaults', 'write', '/Library/Preferences/ManagedInstalls', 'LastCheckDate', '-date', nowString] retcode = subprocess.call(cmd) cmd = ['/usr/bin/defaults', 'write', '/Library/Preferences/ManagedInstalls', 'LastCheckResult', '-int', str(result)] retcode = subprocess.call(cmd) if result > 0: updatesavailable = True if result == -1: # there were errors checking for updates. # let's check to see if there's a InstallInfo.plist with waiting updates from # an earlier successful run installinfo = os.path.join(munkicommon.ManagedInstallDir(), 'InstallInfo.plist') if os.path.exists(installinfo): try: pl = munkicommon.readPlist(installinfo) removalcount = installer.getRemovalCount(pl.get('removals',[])) installcount = installer.getInstallCount(pl.get('managed_installs',[])) if removalcount or installcount: updatesavailable = True except: print >>sys.stderr, "Invalid %s" % installinfo if options.auto or options.manualcheck: # are we supposed to check for and install Apple Software Updates? if munkicommon.pref('InstallAppleSoftwareUpdates'): if options.manualcheck: munkistatus.message("Checking for available Apple Software Updates...") munkistatus.detail("") munkistatus.percent(-1) if appleupdates.appleSoftwareUpdatesAvailable(options.manualcheck): updatesavailable = True if options.manualcheck: # don't need MunkiStatus any more... munkistatus.quit() if options.auto: # when --auto, munkistatusoutput is false for checking, but true for installing munkicommon.munkistatusoutput = True if updatesavailable or options.installonly or options.manualcheck: if options.auto or options.manualcheck: if munkicommon.getconsoleuser() == None: if getIdleSeconds() > 10: doInstallTasks() else: notifyUserOfUpdates(options.manualcheck) elif options.installonly: doInstallTasks() elif not options.quiet: print "\nRun %s --installonly to install the downloaded updates." % myname if munkicommon.munkistatusoutput: munkistatus.quit() if munkicommon.tmpdir: munkicommon.cleanUpTmpDir() if __name__ == '__main__': main()