mirror of
https://github.com/munki/munki.git
synced 2026-01-01 04:00:10 -06:00
git-svn-id: http://munki.googlecode.com/svn/trunk@464 a4e17f2e-e282-11dd-95e1-755cbddbdd66
513 lines
20 KiB
Python
Executable File
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()
|