mirror of
https://github.com/munki/munki.git
synced 2026-01-08 23:50:18 -06:00
if munki updates are available, they are applied, and Apple Updates are ignored. Apple Updates are presented and acted upon only if InstallAppleSoftwareUpdates preference is set and there are no available munki updates. git-svn-id: http://munki.googlecode.com/svn/trunk@375 a4e17f2e-e282-11dd-95e1-755cbddbdd66
432 lines
17 KiB
Python
Executable File
432 lines
17 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
|
|
# convert from Nanoseconds
|
|
return int(int(ioregProcess.stdout.read()) / 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()
|
|
|
|
# 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
|
|
need_to_restart = installer.run()
|
|
# 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?
|
|
need_to_restart = appleupdates.installAppleUpdates()
|
|
|
|
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 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 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 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.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('--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.manualcheck:
|
|
# triggered by Managed Software Update.app
|
|
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
|
|
|
|
# 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.message("Checking for available updates")
|
|
# 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', "-1"]
|
|
retcode = subprocess.call(cmd)
|
|
# 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 not munkicommon.verbose == 0 :
|
|
print "Managed Software Update Tool"
|
|
print "Copyright 2009 The Munki Project"
|
|
print "http://code.google.com/p/munki\n"
|
|
|
|
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)
|
|
|
|
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 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 munkicommon.getconsoleuser():
|
|
# someone is logged in, so notify
|
|
notifyUserOfUpdates()
|
|
elif getIdleSeconds() > 10:
|
|
# no console user, system is idle, so install
|
|
mustrestart = doInstallTasks()
|
|
else:
|
|
pass
|
|
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()
|