Files
munki/code/client/managedsoftwareupdate
Greg Neagle f87d3b78e8 Removed some munkistatus workaround code.
git-svn-id: http://munki.googlecode.com/svn/trunk@464 a4e17f2e-e282-11dd-95e1-755cbddbdd66
2010-02-18 00:53:54 +00:00

513 lines
20 KiB
Python
Executable File

#!/usr/bin/python
#
# Copyright 2009-2010 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
import traceback
from Foundation import NSDate
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():
'''Gets the number of seconds since the last mouse or keyboard event'''
cmd = ['/usr/sbin/ioreg', '-c', 'IOHIDSystem', '-d', '4']
p = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
ioreglines = output.splitlines()
for line in ioreglines:
if "Idle" in line:
parts = line.split()
return int(int(parts[3])/1000000000)
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 FoundationPlist.NSPropertyListSerializationException:
pass
def createDirsIfNeeded(dirlist):
for directory in dirlist:
if not os.path.exists(directory):
try:
os.mkdir(directory)
except (OSError, IOError):
print >>sys.stderr, "ERROR: 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]):
munkicommon.display_error("Could not create needed directories "
"in %s" % ManagedInstallDir)
return False
else:
return True
def doInstallTasks():
# first, clear the last notified date
# so we can get notified of new changes after this round
# of installs
clearLastNotifiedDate()
need_to_restart = False
# munki updates take priority over Apple Updates, because
# a munki install or (especially) removal could make a
# pending Apple update no longer necessary or even complicate
# or prevent the removal of another item.
# Therefore we only install Apple updates if there are no
# pending munki updates.
if munkiUpdatesAvailable():
# install munki updates
try:
need_to_restart = installer.run()
except:
munkicommon.display_error("Unexpected error in "
" munkilib.installer:")
munkicommon.display_error(traceback.format_exc())
munkicommon.savereport()
exit(-1)
# clear any Apple update info since it may no longer
# be relevant
appleupdates.clearAppleUpdateInfo()
elif munkicommon.pref('InstallAppleSoftwareUpdates'):
# are we supposed to handle Apple Software Updates?
try:
need_to_restart = appleupdates.installAppleUpdates()
except:
munkicommon.display_error("Unexpected error in "
" installAppleUpdates:")
munkicommon.display_error(traceback.format_exc())
munkicommon.savereport()
exit(-1)
munkicommon.savereport()
return need_to_restart
def doRestart():
restartMessage = "Software installed or removed requires a restart."
munkicommon.log(restartMessage)
if munkicommon.munkistatusoutput:
munkistatus.hideStopButton()
munkistatus.message(restartMessage)
munkistatus.detail("")
munkistatus.percent(-1)
else:
print restartMessage
sys.stdout.flush()
if not munkicommon.currentGUIusers():
# no-one is logged in and we're at the loginwindow
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 munkiUpdatesAvailable():
updatesavailable = False
installinfo = os.path.join(munkicommon.pref('ManagedInstallDir'),
'InstallInfo.plist')
if os.path.exists(installinfo):
try:
pl = FoundationPlist.readPlist(installinfo)
updatesavailable = len(pl.get('removals',[])) or \
len(pl.get('managed_installs',[]))
except (AttributeError,
FoundationPlist.NSPropertyListSerializationException):
munkicommon.display_error("Install info at %s is invalid." %
installinfo)
return updatesavailable
def recordUpdateCheckResult(result):
# record last check date and result
now = NSDate.new()
cmd = ['/usr/bin/defaults', 'write',
'/Library/Preferences/ManagedInstalls',
'LastCheckDate', '-date', str(now)]
retcode = subprocess.call(cmd)
cmd = ['/usr/bin/defaults', 'write',
'/Library/Preferences/ManagedInstalls',
'LastCheckResult', '-int', str(result)]
retcode = subprocess.call(cmd)
return
def notifyUserOfUpdates(manualcheck=False):
# 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')
now = NSDate.new()
nextNotifyDate = now
if lastNotifiedString:
lastNotifiedDate = NSDate.dateWithString_(lastNotifiedString)
interval = daysBetweenNotifications * (24 * 60 * 60)
if daysBetweenNotifications > 0:
# we make this adjustment so a "daily" notification
# doesn't require 24 hours to elapse
# subtract 6 hours
interval = interval - (6 * 60 * 60)
nextNotifyDate = lastNotifiedDate.dateByAddingTimeInterval_(interval)
if now.timeIntervalSinceDate_(nextNotifyDate) > 0 or manualcheck:
# record current notification date
cmd = ['/usr/bin/defaults', 'write',
'/Library/Preferences/ManagedInstalls',
'LastNotifiedDate', '-date', str(now)]
retcode = subprocess.call(cmd)
# notify user of available updates using LaunchAgent to start
# Managed Software Update.app in the user context
launchfile = "/var/run/com.googlecode.munki.ManagedSoftwareUpdate"
cmd = ['/usr/bin/touch', launchfile]
retcode = subprocess.call(cmd)
time.sleep(0.1)
if os.path.exists(launchfile):
os.unlink(launchfile)
def main():
# check to see if we're root
if os.geteuid() != 0:
print >>sys.stderr, "You must run this as root!"
exit(-1)
p = optparse.OptionParser()
p.set_usage("""Usage: %prog [options]""")
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('--installwithnologout', action='store_true',
help='''Used by Managed Software Update.app when user
triggers an install without logging out.''')
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.
This is the default behavior.''')
p.add_option('--installonly', action='store_true',
help='Skip checking and install any pending updates.')
p.add_option('--version', '-V', action='store_true',
help='Print the version of the munki tools and exit.')
options, arguments = p.parse_args()
runtype = "custom"
if options.version:
print munkicommon.get_version()
exit(0)
if options.auto:
# typically invoked by a launch daemon periodically.
# munkistatusoutput is false for checking, but true for installing
runtype = "auto"
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
runtype = "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 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",
"/Users/Shared/.com.googlecode.munki.checkandinstallatstartup"]
for f in triggerfiles:
if os.path.exists(f):
os.unlink(f)
user_triggered = True
if f.endswith("checkandinstallatstartup"):
options.installonly = False
options.auto = True
if not user_triggered:
exit(0)
if options.installwithnologout:
# typically invoked by Managed Software Update.app
# by user who decides not to logout
launchdtriggerfile = \
"/private/tmp/.com.googlecode.munki.managedinstall.launchd"
if os.path.exists(launchdtriggerfile):
# remove it so we aren't automatically relaunched
os.unlink(launchdtriggerfile)
runtype = "installwithnologout"
options.munkistatusoutput = True
options.quiet = True
options.checkonly = False
options.installonly = True
if options.manualcheck:
# triggered by Managed Software Update.app
launchdtriggerfile = \
"/private/tmp/.com.googlecode.munki.updatecheck.launchd"
if os.path.exists(launchdtriggerfile):
# remove it so we aren't automatically relaunched
os.unlink(launchdtriggerfile)
runtype = "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
# 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
if options.manualcheck:
# a manual update check was triggered
# (probably by Managed Software Update), but managedsoftwareupdate
# is already running. We should provide user feedback
munkistatus.activate()
munkistatus.message("Checking for available updates...")
while True:
# loop til the other instance exits
if not munkicommon.pythonScriptRunning(myname):
break
# or user clicks Stop
if munkistatus.getStopButtonState() == 1:
break
time.sleep(0.5)
# activate Managed Software Update.app
notifyUserOfUpdates(True)
munkistatus.quit()
else:
print >>sys.stderr, \
"Another instance of %s is running. Exiting." % myname
exit(0)
# create needed directories if necessary
if not initMunkiDirs():
exit(-1)
if not options.installonly:
# check to see if we can talk to the manifest server
server = munkicommon.pref('ManifestURL') or \
munkicommon.pref('SoftwareRepoURL')
result = updatecheck.checkServer(server)
if result != (0, 'OK'):
munkicommon.display_error("managedsoftwareupdate: "
"server check for %s failed: %s" %
(server, str(result)))
if options.manualcheck:
# magic incantations so Managed Software Update.app will
# display an appropriate message
# launch MunkiStatus so focus leaves
# Managed Software Update.app
munkistatus.activate()
munkistatus.message("Checking for available updates...")
recordUpdateCheckResult(-1)
# activate Managed Software Update.app
notifyUserOfUpdates(True)
munkistatus.quit()
exit(-1)
# reset our errors and warnings files, rotate main log if needed
munkicommon.reset_errors()
munkicommon.reset_warnings()
munkicommon.rotate_main_log()
munkicommon.archive_report()
# start a new report
munkicommon.report['StartTime'] = time.ctime()
munkicommon.report['RunType'] = runtype
if options.verbose:
print "Managed Software Update Tool"
print "Copyright 2010 The Munki Project"
print "http://code.google.com/p/munki\n"
updatecheckresult = None
if not options.installonly:
try:
updatecheckresult = updatecheck.check(id=options.id)
except:
munkicommon.display_error("Unexpected error in updatecheck:")
munkicommon.display_error(traceback.format_exc())
munkicommon.savereport()
exit(-1)
updatesavailable = munkiUpdatesAvailable()
if not updatesavailable and (options.auto or options.manualcheck):
# if there are no munki updates,
# 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(
forcecheck=options.manualcheck):
updatesavailable = True
if updatecheckresult is not None:
recordUpdateCheckResult(updatecheckresult)
if not updatesavailable and options.installonly and \
munkicommon.pref('InstallAppleSoftwareUpdates'):
# just look and see if there are already downloaded Apple updates
# to install; don't run softwareupdate to check with Apple
if appleupdates.appleSoftwareUpdatesAvailable(suppresscheck=True):
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
mustrestart = False
if options.manualcheck:
# let the user know the results of the check
if munkicommon.getconsoleuser():
notifyUserOfUpdates(True)
elif updatesavailable:
if options.installonly:
# just install
mustrestart = doInstallTasks()
elif options.auto:
if not munkicommon.currentGUIusers():
# no GUI users
if getIdleSeconds() > 10:
# no GUI users, system is idle, so install
mustrestart = doInstallTasks()
else:
# there are GUI users
consoleuser = munkicommon.getconsoleuser()
if consoleuser:
# someone is logged in.
if consoleuser == u"loginwindow":
# someone is logged in, but we're sitting at
# the loginwindow due to fast user switching
# so do nothing
pass
else:
# notify the current console user
notifyUserOfUpdates()
elif not options.quiet:
print ("\nRun %s --installonly to install the downloaded "
"updates." % myname)
else:
# no updates available
if options.installonly and not options.quiet:
print "Nothing to install or remove."
# finish our report
munkicommon.report['EndTime'] = time.ctime()
munkicommon.report['ManagedInstallVersion'] = munkicommon.get_version()
munkicommon.report['AvailableDiskSpace'] = \
munkicommon.getAvailableDiskSpace()
munkicommon.report['ConsoleUser'] = munkicommon.getconsoleuser() or \
"<None>"
munkicommon.savereport()
if munkicommon.tmpdir:
munkicommon.cleanUpTmpDir()
if mustrestart:
doRestart()
elif munkicommon.munkistatusoutput:
munkistatus.quit()
if __name__ == '__main__':
main()