#!/usr/bin/env 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 plistlib from munkilib import munkicommon from munkilib import updatecheck from munkilib import installer from munkilib import munkistatus 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(): 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) 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 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: # munkistatusoutput is false for checking, but true for installing options.munkistatusoutput = False options.quiet = True options.checkonly = False options.installonly = False if options.logoutinstall: 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 triggerfile = "/private/tmp/com.googlecode.munki.installatlogout" if os.path.exists(triggerfile): os.unlink(triggerfile) else: # no trigger file, so don't install exit(0) if options.manualcheck: 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) if options.manualcheck: munkistatus.quit() 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: # 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: # no-one is logged in, and # no keyboard or mouse movement for the past 10 seconds, # therefore no-one is in the middle of logging in (we hope!) # so just install # but first, clear the last notified date # so we can get notified of new changes after this round # of installs clearLastNotifiedDate() # now install installer.run() else: # 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 options.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 = munkistatus.osascript('tell application "Managed Software Update" to activate') else: if options.installonly: # install! # but first, clear the last notified date so we can # get notified of new changes after this round # of installs clearLastNotifiedDate() # now we can install installer.run() else: if 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()