mirror of
https://github.com/munki/munki.git
synced 2026-01-13 02:10:06 -06:00
This adds Force Install Notifications support to the MSU GUI, and logouthelper support to managedsoftwareupdate/launchd. Documentation on using the pkginfo force_install_after_date key to come.... This merge also includes localization fixes and on-the-fly updating of the MSU GUI when managedsoftwareupdate runs in the background while the GUI is open, changing InstallInfo. With this merge, the Munki version is increased to 0.8.0 and MSU GUI version to 3.2. git-svn-id: http://munki.googlecode.com/svn/trunk@1270 a4e17f2e-e282-11dd-95e1-755cbddbdd66
770 lines
30 KiB
Python
Executable File
770 lines
30 KiB
Python
Executable File
#!/usr/bin/python
|
|
# encoding: utf-8
|
|
#
|
|
# Copyright 2009-2011 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 grp
|
|
import optparse
|
|
import os
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import traceback
|
|
|
|
# Do not place any imports with ObjC bindings above this!
|
|
try:
|
|
from Foundation import NSDate
|
|
from Foundation import NSDistributedNotificationCenter
|
|
from Foundation import NSNotificationDeliverImmediately
|
|
from Foundation import NSNotificationPostToAllSessions
|
|
except:
|
|
# Python is missing ObjC bindings. Run external report script.
|
|
from munkilib import utils
|
|
print >> sys.stderr, 'Python is missing ObjC bindings.'
|
|
scriptdir = os.path.realpath(os.path.dirname(sys.argv[0]))
|
|
script = os.path.join(scriptdir, 'report_broken_client')
|
|
try:
|
|
result, stdout, stderr = utils.runExternalScript(script)
|
|
print >> sys.stderr, result, stdout, stderr
|
|
except utils.ScriptNotFoundError:
|
|
pass # script is not required, so pass
|
|
except utils.RunExternalScriptError, e:
|
|
print >> sys.stderr, str(e)
|
|
sys.exit(1)
|
|
|
|
from munkilib import munkicommon
|
|
from munkilib import updatecheck
|
|
from munkilib import installer
|
|
from munkilib import munkistatus
|
|
from munkilib import appleupdates
|
|
from munkilib import FoundationPlist
|
|
from munkilib import utils
|
|
|
|
|
|
def getIdleSeconds():
|
|
"""Returns the number of seconds since the last mouse
|
|
or keyboard event."""
|
|
cmd = ['/usr/sbin/ioreg', '-c', 'IOHIDSystem']
|
|
proc = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(output, unused_err) = proc.communicate()
|
|
ioreglines = str(output).splitlines()
|
|
idle_time = 0
|
|
regex = re.compile('"?HIDIdleTime"?\s+=\s+(\d+)')
|
|
for line in ioreglines:
|
|
idle_re = regex.search(line)
|
|
if idle_re:
|
|
idle_time = idle_re.group(1)
|
|
break
|
|
return int(int(idle_time)/1000000000)
|
|
|
|
|
|
def networkUp():
|
|
"""Determine if the network is up by looking for any non-loopback
|
|
internet network interfaces.
|
|
|
|
Returns:
|
|
Boolean. True if loopback is found (network is up), False otherwise.
|
|
"""
|
|
cmd = ['/sbin/ifconfig', '-a', 'inet']
|
|
proc = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(output, unused_err) = proc.communicate()
|
|
lines = str(output).splitlines()
|
|
for line in lines:
|
|
if 'inet' in line:
|
|
parts = line.split()
|
|
addr = parts[1]
|
|
if not addr in ['127.0.0.1', '0.0.0.0.0']:
|
|
return True
|
|
return False
|
|
|
|
|
|
def clearLastNotifiedDate():
|
|
"""Clear the last date the user was notified of updates."""
|
|
munkicommon.set_pref('LastNotifiedDate', None)
|
|
|
|
|
|
def createDirsIfNeeded(dirlist):
|
|
"""Create any missing directories needed by the munki tools.
|
|
|
|
Args:
|
|
dirlist: a sequence of directories.
|
|
Returns:
|
|
Boolean. True if all directories existed or were created,
|
|
False otherwise.
|
|
"""
|
|
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():
|
|
"""Figure out where data directories should be and create them if needed.
|
|
|
|
Returns:
|
|
Boolean. True if all data dirs existed or were created, False otherwise.
|
|
"""
|
|
ManagedInstallDir = munkicommon.pref('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(only_unattended=False):
|
|
"""Perform our installation/removal tasks.
|
|
|
|
Args:
|
|
only_unattended: Boolean. If True, only do unattended_(un)install items.
|
|
|
|
Returns:
|
|
Boolean. True if a restart is required, False otherwise.
|
|
"""
|
|
if not only_unattended:
|
|
# 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(only_unattended=only_unattended)
|
|
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
|
|
if not only_unattended:
|
|
appleupdates.clearAppleUpdateInfo()
|
|
elif ((munkicommon.pref('InstallAppleSoftwareUpdates') or
|
|
munkicommon.pref('AppleSoftwareUpdatesOnly'))
|
|
and not only_unattended):
|
|
# are we supposed to handle Apple Software Updates?
|
|
try:
|
|
need_to_restart = appleupdates.installAppleUpdates()
|
|
except:
|
|
munkicommon.display_error(
|
|
'Unexpected error in appleupdates.installAppleUpdates:')
|
|
munkicommon.display_error(traceback.format_exc())
|
|
munkicommon.savereport()
|
|
exit(-1)
|
|
|
|
munkicommon.savereport()
|
|
return need_to_restart
|
|
|
|
|
|
def startLogoutHelper():
|
|
"""Handle the need for a forced logout. Start our logouthelper"""
|
|
cmd = ['/bin/launchctl', 'start', 'com.googlecode.munki.logouthelper']
|
|
result = subprocess.call(cmd)
|
|
if result:
|
|
# some problem with the launchd job
|
|
munkicommon.display_error(
|
|
'Could not start com.googlecode.munki.logouthelper')
|
|
|
|
|
|
def doRestart():
|
|
"""Handle the need for a restart."""
|
|
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:
|
|
munkicommon.display_info(restartMessage)
|
|
|
|
# TODO: temporary fix for forced logout problem where we've killed
|
|
# loginwindow sessions, but munkicommon.currentGUIusers() still returns
|
|
# users. Need to find a better solution, though.
|
|
#if not munkicommon.currentGUIusers():
|
|
# # no-one is logged in and we're at the loginwindow
|
|
consoleuser = munkicommon.getconsoleuser()
|
|
if not consoleuser or consoleuser == u'loginwindow':
|
|
# no-one is logged in or we're at the loginwindow
|
|
time.sleep(5)
|
|
unused_retcode = subprocess.call(['/sbin/shutdown', '-r', 'now'])
|
|
else:
|
|
if munkicommon.munkistatusoutput:
|
|
# someone is logged in and we're using munkistatus
|
|
munkicommon.display_info(
|
|
'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():
|
|
"""Return True if there are available updates, False otherwise."""
|
|
updatesavailable = False
|
|
installinfo = os.path.join(munkicommon.pref('ManagedInstallDir'),
|
|
'InstallInfo.plist')
|
|
if os.path.exists(installinfo):
|
|
try:
|
|
plist = FoundationPlist.readPlist(installinfo)
|
|
updatesavailable = len(plist.get('removals', [])) or \
|
|
len(plist.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()
|
|
munkicommon.set_pref('LastCheckDate', now)
|
|
munkicommon.set_pref('LastCheckResult', result)
|
|
|
|
|
|
def sendUpdateNotification():
|
|
'''Sends an update notification via NSDistributedNotificationCenter
|
|
MSU.app registers to receive these events.'''
|
|
dnc = NSDistributedNotificationCenter.defaultCenter()
|
|
dnc.postNotificationName_object_userInfo_options_(
|
|
'com.googlecode.munki.ManagedSoftwareUpdate.update',
|
|
None, None,
|
|
NSNotificationDeliverImmediately + NSNotificationPostToAllSessions)
|
|
|
|
|
|
def notifyUserOfUpdates(force=False):
|
|
"""Notify the logged-in user of available updates.
|
|
|
|
Args:
|
|
force: bool, default False, forcefully notify user regardless
|
|
of LastNotifiedDate.
|
|
Returns:
|
|
Boolean. True if the user was notified, False otherwise.
|
|
"""
|
|
# called when options.auto == True
|
|
# someone is logged in, and we have updates.
|
|
# if we haven't notified in a while, notify:
|
|
user_was_notified = False
|
|
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 force or now.timeIntervalSinceDate_(nextNotifyDate) >= 0:
|
|
# record current notification date
|
|
munkicommon.set_pref('LastNotifiedDate', now)
|
|
|
|
# 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]
|
|
unused_retcode = subprocess.call(cmd)
|
|
time.sleep(1)
|
|
if os.path.exists(launchfile):
|
|
os.unlink(launchfile)
|
|
user_was_notified = True
|
|
return user_was_notified
|
|
|
|
|
|
def main():
|
|
"""Main"""
|
|
# check to see if we're root
|
|
if os.geteuid() != 0:
|
|
print >> sys.stderr, 'You must run this as root!'
|
|
exit(-1)
|
|
|
|
# save this for later
|
|
scriptdir = os.path.realpath(os.path.dirname(sys.argv[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('--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('--applesuspkgsonly', action='store_true',
|
|
help=('Only check/install Apple SUS packages, '
|
|
'skip Munki packages.'))
|
|
p.add_option('--munkipkgsonly', action='store_true',
|
|
help=('Only check/install Munki packages, '
|
|
'skip Apple SUS.'))
|
|
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'
|
|
|
|
checkandinstallatstartupflag = \
|
|
'/Users/Shared/.com.googlecode.munki.checkandinstallatstartup'
|
|
installatstartupflag = \
|
|
'/Users/Shared/.com.googlecode.munki.installatstartup'
|
|
installatlogoutflag = '/private/tmp/com.googlecode.munki.installatlogout'
|
|
|
|
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
|
|
flagfiles = [checkandinstallatstartupflag,
|
|
installatstartupflag,
|
|
installatlogoutflag]
|
|
for filename in flagfiles:
|
|
if os.path.exists(filename):
|
|
user_triggered = True
|
|
if filename == checkandinstallatstartupflag:
|
|
runtype = 'checkandinstallatstartup'
|
|
options.installonly = False
|
|
options.auto = True
|
|
# HACK: sometimes this runs before the network is up.
|
|
# we'll attempt to wait up to 10 seconds for the
|
|
# network interfaces to come up
|
|
# before continuing
|
|
munkicommon.display_status('Waiting for network...')
|
|
for i in range(5):
|
|
if networkUp():
|
|
break
|
|
time.sleep(2)
|
|
else:
|
|
# delete triggerfile if _not_ checkandinstallatstartup
|
|
os.unlink(filename)
|
|
if not user_triggered:
|
|
munkicommon.cleanUpTmpDir()
|
|
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
|
|
|
|
# run the preflight script if it exists
|
|
preflightscript = os.path.join(scriptdir, 'preflight')
|
|
if os.path.exists(preflightscript):
|
|
munkicommon.display_status('Performing preflight tasks...')
|
|
try:
|
|
result, stdout, stderr = utils.runExternalScript(
|
|
preflightscript, runtype)
|
|
if stdout:
|
|
munkicommon.display_info('preflight stdout: %s', stdout)
|
|
if stderr:
|
|
munkicommon.display_info('preflight stderr: %s', stderr)
|
|
except utils.ScriptNotFoundError:
|
|
result = 0
|
|
pass # script is not required, so pass
|
|
except utils.RunExternalScriptError, e:
|
|
result = 0
|
|
munkicommon.display_warning(str(e))
|
|
|
|
if result:
|
|
# non-zero return code means don't run
|
|
munkicommon.display_info(
|
|
'managedsoftwareupdate run aborted by preflight script: %s'
|
|
% result)
|
|
munkicommon.display_info(stderr)
|
|
# record the check result for use by Managed Software Update.app
|
|
# right now, we'll return the same code as if the munki server
|
|
# was unavailable. We need to revisit this and define additional
|
|
# update check results.
|
|
recordUpdateCheckResult(-2)
|
|
if options.munkistatusoutput:
|
|
# connect to socket and quit
|
|
munkistatus.activate()
|
|
munkistatus.quit()
|
|
munkicommon.cleanUpTmpDir()
|
|
exit(-1)
|
|
# Force a prefs refresh, in case preflight modified the prefs file.
|
|
munkicommon.reload_prefs()
|
|
|
|
# create needed directories if necessary
|
|
if not initMunkiDirs():
|
|
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
|
|
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 munkicommon.stopRequested():
|
|
break
|
|
time.sleep(0.5)
|
|
|
|
munkistatus.quit()
|
|
else:
|
|
print >> sys.stderr, \
|
|
'Another instance of %s is running. Exiting.' % myname
|
|
munkicommon.cleanUpTmpDir()
|
|
exit(0)
|
|
|
|
applesoftwareupdatesonly = (munkicommon.pref('AppleSoftwareUpdatesOnly')
|
|
or options.applesuspkgsonly)
|
|
|
|
if not options.installonly and not applesoftwareupdatesonly:
|
|
# 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:
|
|
# record our result
|
|
recordUpdateCheckResult(-1)
|
|
# connect to socket and quit
|
|
munkistatus.activate()
|
|
munkistatus.quit()
|
|
munkicommon.cleanUpTmpDir()
|
|
exit(-1)
|
|
|
|
# reset our errors and warnings files, rotate main log if needed
|
|
munkicommon.reset_errors()
|
|
munkicommon.reset_warnings()
|
|
munkicommon.rotate_main_log()
|
|
|
|
if options.installonly:
|
|
# we're only installing, not checking, so we should copy
|
|
# some report values from the prior run
|
|
munkicommon.readreport()
|
|
|
|
# archive the previous session's report
|
|
munkicommon.archive_report()
|
|
# start a new report
|
|
munkicommon.report['StartTime'] = munkicommon.format_time()
|
|
munkicommon.report['RunType'] = runtype
|
|
munkicommon.report['Errors'] = []
|
|
munkicommon.report['Warnings'] = []
|
|
|
|
munkicommon.log("### Starting managedsoftwareupdate run ###")
|
|
if options.verbose:
|
|
print 'Managed Software Update Tool'
|
|
print 'Copyright 2010-2011 The Munki Project'
|
|
print 'http://code.google.com/p/munki\n'
|
|
|
|
if applesoftwareupdatesonly and options.verbose:
|
|
print ('NOTE: managedsoftwareupdate is configured to process Apple '
|
|
'Software Updates only.')
|
|
|
|
updatecheckresult = None
|
|
if not options.installonly and not applesoftwareupdatesonly:
|
|
try:
|
|
updatecheckresult = updatecheck.check(client_id=options.id)
|
|
except:
|
|
munkicommon.display_error('Unexpected error in updatecheck:')
|
|
munkicommon.display_error(traceback.format_exc())
|
|
munkicommon.savereport()
|
|
exit(-1)
|
|
|
|
if updatecheckresult is not None:
|
|
recordUpdateCheckResult(updatecheckresult)
|
|
|
|
updatesavailable = munkiUpdatesAvailable()
|
|
appleupdatesavailable = False
|
|
if (not updatesavailable and not options.installonly and
|
|
not munkicommon.stopRequested()):
|
|
# if there are no munki updates,
|
|
# are we supposed to check for and install Apple Software Updates?
|
|
if ((munkicommon.pref('InstallAppleSoftwareUpdates') or
|
|
applesoftwareupdatesonly) and not options.munkipkgsonly):
|
|
try:
|
|
appleupdatesavailable = \
|
|
appleupdates.appleSoftwareUpdatesAvailable(
|
|
forcecheck=(options.manualcheck or
|
|
runtype == 'checkandinstallatstartup'))
|
|
except:
|
|
munkicommon.display_error('Unexpected error in appleupdates:')
|
|
munkicommon.display_error(traceback.format_exc())
|
|
munkicommon.savereport()
|
|
exit(-1)
|
|
|
|
if (not updatesavailable and options.installonly and
|
|
not options.munkipkgsonly and
|
|
(munkicommon.pref('InstallAppleSoftwareUpdates') or
|
|
applesoftwareupdatesonly)):
|
|
# just look and see if there are already downloaded Apple updates
|
|
# to install; don't run softwareupdate to check with Apple
|
|
try:
|
|
appleupdatesavailable = \
|
|
appleupdates.appleSoftwareUpdatesAvailable(suppresscheck=True)
|
|
except:
|
|
munkicommon.display_error('Unexpected error in appleupdates:')
|
|
munkicommon.display_error(traceback.format_exc())
|
|
munkicommon.savereport()
|
|
exit(-1)
|
|
|
|
# send a notification event so MSU can update its display if needed
|
|
sendUpdateNotification()
|
|
|
|
mustrestart = False
|
|
mustlogout = False
|
|
if options.manualcheck:
|
|
# just quit munkistatus; Managed Software Update will notify
|
|
munkistatus.quit()
|
|
elif updatesavailable or appleupdatesavailable:
|
|
if options.installonly or options.logoutinstall:
|
|
# just install
|
|
mustrestart = doInstallTasks()
|
|
elif options.auto:
|
|
if not munkicommon.currentGUIusers(): # no GUI users
|
|
if getIdleSeconds() > 10:
|
|
if not munkicommon.pref('SuppressAutoInstall'):
|
|
# no GUI users, system is idle, so install
|
|
# enable status output over login window
|
|
munkicommon.munkistatusoutput = True
|
|
mustrestart = doInstallTasks()
|
|
else:
|
|
munkicommon.log('Skipping auto install because '
|
|
'SuppressAutoInstall is true.')
|
|
else:
|
|
munkicommon.log('Skipping auto install because system is '
|
|
'not idle (keyboard or mouse activity).')
|
|
else: # there are GUI users
|
|
unused_force_action = updatecheck.checkForceInstallPackages()
|
|
doInstallTasks(only_unattended=True)
|
|
force_action = updatecheck.checkForceInstallPackages()
|
|
# if any installs are still requiring force actions, just
|
|
# initiate a logout to get started. blocking apps might
|
|
# have stopped even non-logout/reboot installs from
|
|
# occuring.
|
|
if force_action in ['now', 'logout', 'restart']:
|
|
mustlogout = True
|
|
|
|
# it's possible that we no longer have any available updates
|
|
# so we need to check InstallInfo.plist again
|
|
# however Apple Updates have not been affected by the
|
|
# unattended install tasks (so that check is still valid).
|
|
if appleupdatesavailable or munkiUpdatesAvailable():
|
|
consoleuser = munkicommon.getconsoleuser()
|
|
if consoleuser == u'loginwindow':
|
|
# someone is logged in, but we're sitting at
|
|
# the loginwindow due to fast user switching
|
|
# so do nothing
|
|
pass
|
|
elif force_action:
|
|
notifyUserOfUpdates(force=True)
|
|
time.sleep(2)
|
|
startLogoutHelper()
|
|
elif not munkicommon.pref('SuppressUserNotification'):
|
|
notifyUserOfUpdates()
|
|
else:
|
|
munkicommon.log('Skipping user notification because '
|
|
'SuppressUserNotification is true.')
|
|
|
|
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.'
|
|
if runtype == 'checkandinstallatstartup':
|
|
# we have nothing to do, so remove the
|
|
# checkandinstallatstartupflag file
|
|
# so we'll stop running at startup/logout
|
|
if os.path.exists(checkandinstallatstartupflag):
|
|
os.unlink(checkandinstallatstartupflag)
|
|
|
|
munkicommon.log("### Ending managedsoftwareupdate run ###")
|
|
# finish our report
|
|
munkicommon.report['EndTime'] = munkicommon.format_time()
|
|
munkicommon.report['ManagedInstallVersion'] = munkicommon.get_version()
|
|
munkicommon.report['AvailableDiskSpace'] = \
|
|
munkicommon.getAvailableDiskSpace()
|
|
munkicommon.report['ConsoleUser'] = munkicommon.getconsoleuser() or \
|
|
'<None>'
|
|
munkicommon.savereport()
|
|
|
|
# run the postflight script if it exists
|
|
postflightscript = os.path.join(scriptdir, 'postflight')
|
|
if os.path.exists(postflightscript):
|
|
munkicommon.display_status('Performing postflight tasks...')
|
|
try:
|
|
result, stdout, stderr = utils.runExternalScript(
|
|
postflightscript, runtype)
|
|
if result:
|
|
munkicommon.display_info('postflight return code: %d' % result)
|
|
if stdout:
|
|
munkicommon.display_info('postflight stdout: %s', stdout)
|
|
if stderr:
|
|
munkicommon.display_info('postflight stderr: %s', stderr)
|
|
except utils.ScriptNotFoundError:
|
|
pass # script is not required, so pass
|
|
except utils.RunExternalScriptError, e:
|
|
munkicommon.display_warning(str(e))
|
|
# we ignore the result of the postflight
|
|
|
|
munkicommon.cleanUpTmpDir()
|
|
if mustrestart:
|
|
doRestart()
|
|
#elif mustlogout:
|
|
# doForcedLogout() # indirectly done via logouthelper
|
|
elif munkicommon.munkistatusoutput:
|
|
munkistatus.quit()
|
|
|
|
if runtype == 'checkandinstallatstartup' and not mustrestart:
|
|
if os.path.exists(checkandinstallatstartupflag):
|
|
# we installed things but did not need to restart; we need to run
|
|
# again to check for more updates.
|
|
if not munkicommon.currentGUIusers():
|
|
# no-one is logged in
|
|
idleseconds = getIdleSeconds()
|
|
if not idleseconds > 10:
|
|
# system is not idle, but check again in case someone has
|
|
# simply briefly touched the mouse to see progress.
|
|
time.sleep(15)
|
|
idleseconds = getIdleSeconds()
|
|
if idleseconds > 10:
|
|
# no-one is logged in and the machine has been idle
|
|
# for a few seconds; kill the loginwindow
|
|
# (which will cause us to run again)
|
|
munkicommon.log(
|
|
'Killing loginwindow so we will run again...')
|
|
cmd = ['/usr/bin/killall', 'loginwindow']
|
|
unused_retcode = subprocess.call(cmd)
|
|
else:
|
|
munkicommon.log(
|
|
'System not idle -- skipping killing loginwindow')
|
|
|
|
if __name__ == '__main__':
|
|
main()
|