mirror of
https://github.com/munki/munki.git
synced 2026-01-07 06:59:57 -06:00
git-svn-id: http://munki.googlecode.com/svn/trunk@254 a4e17f2e-e282-11dd-95e1-755cbddbdd66
315 lines
13 KiB
Python
Executable File
315 lines
13 KiB
Python
Executable File
#!/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
|
|
applesaysrestart = munkisaysrestart = False
|
|
clearLastNotifiedDate()
|
|
|
|
# are we supposed to handle Apple Software Updates?
|
|
if munkicommon.pref('InstallAppleSoftwareUpdates'):
|
|
applesaysrestart = appleupdates.installAppleUpdates()
|
|
|
|
if applesaysrestart:
|
|
# 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
|
|
munkisaysrestart = installer.run()
|
|
|
|
if (applesaysrestart or munkisaysrestart):
|
|
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.percent(-1)
|
|
if appleupdates.appleSoftwareUpdatesAvailable():
|
|
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()
|