Files
munki/code/client/managedsoftwareupdate
2017-04-14 10:47:58 -07:00

1061 lines
42 KiB
Python
Executable File

#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2017 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
#
# https://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 optparse
import os
import re
import signal
import subprocess
import sys
import time
import traceback
# Disable PyLint complaining about 'invalid' camelCase names
# pylint: disable=C0103
# Do not place any imports with ObjC bindings above this!
try:
# PyLint cannot properly find names inside Cocoa libraries, so issues bogus
# No name 'Foo' in module 'Bar' warnings. Disable them.
# pylint: disable=E0611
from Foundation import NSDate
from Foundation import NSDistributedNotificationCenter
from Foundation import NSNotificationDeliverImmediately
from Foundation import NSNotificationPostToAllSessions
# pylint: enable=E0611
except ImportError:
# 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, err:
print >> sys.stderr, str(err)
sys.exit(200)
else:
from munkilib import appleupdates
from munkilib import authrestart
from munkilib import constants
from munkilib import display
from munkilib import info
from munkilib import installer
from munkilib import installinfo
from munkilib import launch
from munkilib import munkilog
from munkilib import munkistatus
from munkilib import osinstaller
from munkilib import osutils
from munkilib import prefs
from munkilib import processes
from munkilib import reports
from munkilib import updatecheck
from munkilib import utils
from munkilib import FoundationPlist
def signal_handler(signum, dummy_frame):
"""Handle any signals we've been told to.
Right now just handle SIGTERM so clean up can happen, like
garbage collection, which will trigger object destructors and
kill any launchd processes we've started."""
if signum == signal.SIGTERM:
sys.exit()
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, dummy_err) = proc.communicate()
ioreglines = str(output).splitlines()
idle_time = 0
regex = re.compile(r'"?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, dummy_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']:
return True
return False
def clearLastNotifiedDate():
"""Clear the last date the user was notified of updates."""
prefs.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 = prefs.pref('ManagedInstallDir')
manifestsdir = os.path.join(ManagedInstallDir, 'manifests')
catalogsdir = os.path.join(ManagedInstallDir, 'catalogs')
iconsdir = os.path.join(ManagedInstallDir, 'icons')
cachedir = os.path.join(ManagedInstallDir, 'Cache')
logdir = os.path.join(ManagedInstallDir, 'Logs')
if not createDirsIfNeeded([ManagedInstallDir, manifestsdir, catalogsdir,
iconsdir, cachedir, logdir]):
display.display_error('Could not create needed directories '
'in %s' % ManagedInstallDir)
return False
else:
return True
def runScript(script, display_name, runtype):
"""Run an external script. Do not run if the permissions on the external
script file are weaker than the current executable."""
result = 0
if os.path.exists(script):
display.display_status_minor('Performing %s tasks...' % display_name)
else:
return result
try:
utils.verifyFileOnlyWritableByMunkiAndRoot(script)
except utils.VerifyFilePermissionsError, err:
# preflight/postflight is insecure, but if the currently executing
# file is insecure too we are no worse off.
try:
utils.verifyFileOnlyWritableByMunkiAndRoot(__file__)
except utils.VerifyFilePermissionsError, err:
# OK, managedsoftwareupdate is insecure anyway - warn & execute.
display.display_warning(
'Multiple munki executable scripts have insecure file '
'permissions. Executing %s anyway. Error: %s'
% (display_name, err))
else:
# Just the preflight/postflight is insecure. Do not execute.
display.display_warning(
'Skipping execution of %s due to insecure file permissions. '
'Error: %s' % (display_name, err))
return result
try:
result, stdout, stderr = utils.runExternalScript(
script, allow_insecure=True, script_args=[runtype])
if result:
display.display_info(
'%s return code: %d' % (display_name, result))
if stdout:
display.display_info('%s stdout: %s' % (display_name, stdout))
if stderr:
display.display_info('%s stderr: %s' % (display_name, stderr))
except utils.ScriptNotFoundError:
pass # script is not required, so pass
except utils.RunExternalScriptError, err:
display.display_warning(unicode(err))
return result
def doInstallTasks(do_apple_updates, only_unattended=False):
"""Perform our installation/removal tasks.
Args:
do_apple_updates: Boolean. If True, install Apple updates
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()
munki_need_to_restart = False
apple_need_to_restart = False
if munkiUpdatesAvailable():
# install munki updates
try:
munki_need_to_restart = installer.run(
only_unattended=only_unattended)
except:
display.display_error('Unexpected error in munkilib.installer:')
munkilog.log(traceback.format_exc())
reports.savereport()
raise
if not only_unattended and munkiUpdatesContainStartOSInstallItem():
reports.savereport()
# install macOS
try:
success = osinstaller.run(finishing_tasks=doFinishingTasks)
except:
display.display_error(
'Unexpected error in munkilib.osinstaller:')
munkilog.log(traceback.format_exc())
reports.savereport()
raise
if success:
# startosinstall will be restarting; get out of the way
sys.exit()
if do_apple_updates:
# install Apple updates
try:
apple_need_to_restart = appleupdates.installAppleUpdates(
only_unattended=only_unattended)
except:
display.display_error(
'Unexpected error in appleupdates.installAppleUpdates:')
munkilog.log(traceback.format_exc())
reports.savereport()
raise
reports.savereport()
return munki_need_to_restart or apple_need_to_restart
def doFinishingTasks():
'''A collection of tasks to do as we finish up'''
# finish our report
reports.report['EndTime'] = reports.format_time()
reports.report['ManagedInstallVersion'] = info.get_version()
reports.report['AvailableDiskSpace'] = info.available_disk_space()
reports.report['ConsoleUser'] = osutils.getconsoleuser() or '<None>'
reports.savereport()
# store the current pending update count and other data for munki-notifier
update_info = installinfo.get_pending_update_info()
prefs.set_pref('PendingUpdateCount', update_info['PendingUpdateCount'])
installinfo.save_pending_update_times()
prefs.set_pref('OldestUpdateDays', update_info['OldestUpdateDays'])
prefs.set_pref('ForcedUpdateDueDate', update_info['ForcedUpdateDueDate'])
# save application inventory data
info.saveappdata()
# run the postflight script if it exists
scriptdir = os.path.realpath(os.path.dirname(sys.argv[0]))
postflightscript = os.path.join(scriptdir, 'postflight')
try:
localruntype = runtype
except NameError:
# runtype is not defined -- we're being called by osinstall
localruntype = 'osinstall'
runScript(postflightscript, 'postflight', localruntype)
# we ignore the result of the postflight
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
display.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.'
munkilog.log(restartMessage)
if display.munkistatusoutput:
munkistatus.hideStopButton()
munkistatus.message(restartMessage)
munkistatus.detail('')
munkistatus.percent(-1)
else:
display.display_info(restartMessage)
# check current console user
consoleuser = osutils.getconsoleuser()
if not consoleuser or consoleuser == u'loginwindow':
# no-one is logged in or we're at the loginwindow
time.sleep(5)
authrestart.do_authorized_or_normal_restart()
else:
if display.munkistatusoutput:
# someone is logged in and we're using Managed Software Center.
# We need to notifiy the active user that a restart is required.
# We actually should almost never get here; generally Munki knows
# a restart is needed before even starting the updates and forces
# a logout before applying the updates
display.display_info(
'Notifying currently logged-in user to restart.')
munkistatus.activate()
munkistatus.restartAlert()
# Managed Software Center will trigger a restart
# when the alert is dismissed. If a user gets clever and subverts
# this restart (perhaps by force-quitting the app),
# that's their problem...
else:
print 'Please restart immediately.'
def munkiUpdatesAvailable():
"""Return count of available updates."""
updatesavailable = 0
install_info = os.path.join(
prefs.pref('ManagedInstallDir'), 'InstallInfo.plist')
if os.path.exists(install_info):
try:
plist = FoundationPlist.readPlist(install_info)
updatesavailable = (len(plist.get('removals', [])) +
len(plist.get('managed_installs', [])))
except (AttributeError,
FoundationPlist.NSPropertyListSerializationException):
display.display_error(
'Install info at %s is invalid.' % install_info)
return updatesavailable
def munkiUpdatesContainStartOSInstallItem():
"""Return True if there is a startosinstall item in the list of updates"""
install_info = os.path.join(
prefs.pref('ManagedInstallDir'), 'InstallInfo.plist')
if os.path.exists(install_info):
try:
plist = FoundationPlist.readPlist(install_info)
except FoundationPlist.NSPropertyListSerializationException:
display.display_error(
'Install info at %s is invalid.' % install_info)
else:
# check managed_installs for startosinstall items
for item in plist.get('managed_installs', []):
if item.get('installer_type') == 'startosinstall':
return True
return False
def munkiUpdatesContainAppleItems():
"""Return True if there are any Apple items in the list of updates"""
install_info = os.path.join(
prefs.pref('ManagedInstallDir'), 'InstallInfo.plist')
if os.path.exists(install_info):
try:
plist = FoundationPlist.readPlist(install_info)
except FoundationPlist.NSPropertyListSerializationException:
display.display_error(
'Install info at %s is invalid.' % install_info)
else:
# check managed_installs
for item in plist.get('managed_installs', []):
if item.get('apple_item'):
return True
# check removals
for item in plist.get('removals', []):
if item.get('apple_item'):
return True
return False
def recordUpdateCheckResult(result):
"""Record last check date and result"""
now = NSDate.new()
prefs.set_pref('LastCheckDate', now)
prefs.set_pref('LastCheckResult', result)
def sendDistributedNotification(notification_name, userInfo=None):
'''Sends a NSDistributedNotification'''
dnc = NSDistributedNotificationCenter.defaultCenter()
dnc.postNotificationName_object_userInfo_options_(
notification_name,
None,
userInfo,
NSNotificationDeliverImmediately + NSNotificationPostToAllSessions)
def sendUpdateNotification():
'''Sends an update notification via NSDistributedNotificationCenter
MSU.app registers to receive these events.'''
userInfo = {'pid': os.getpid()}
sendDistributedNotification(
'com.googlecode.munki.managedsoftwareupdate.updateschanged',
userInfo)
def sendDockUpdateNotification():
'''Sends an update notification via NSDistributedNotificationCenter
MSU.app's docktileplugin registers to receive these events.'''
userInfo = {'pid': os.getpid()}
sendDistributedNotification(
'com.googlecode.munki.managedsoftwareupdate.dock.updateschanged',
userInfo)
def sendStartNotification():
'''Sends a start notification via NSDistributedNotificationCenter'''
userInfo = {'pid': os.getpid()}
sendDistributedNotification(
'com.googlecode.munki.managedsoftwareupdate.started',
userInfo)
def sendEndNotification():
'''Sends an ended notification via NSDistributedNotificationCenter'''
userInfo = {'pid': os.getpid()}
sendDistributedNotification(
'com.googlecode.munki.managedsoftwareupdate.ended',
userInfo)
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 = prefs.pref('LastNotifiedDate')
try:
daysBetweenNotifications = int(
prefs.pref('DaysBetweenNotifications'))
except ValueError:
display.display_warning(
'DaysBetweenNotifications is not an integer: %s'
% prefs.pref('DaysBetweenNotifications'))
# continue with the default DaysBetweenNotifications
daysBetweenNotifications = 1
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
prefs.set_pref('LastNotifiedDate', now)
munkilog.log('Notifying user of available updates.')
munkilog.log('LastNotifiedDate was %s' % lastNotifiedString)
# notify user of available updates using LaunchAgent to launch
# munki-notifier.app in the user context.
launchfile = '/var/run/com.googlecode.munki.munki-notifier'
f = open(launchfile, 'w')
f.close()
time.sleep(5)
if os.path.exists(launchfile):
os.unlink(launchfile)
user_was_notified = True
return user_was_notified
def warn_if_server_is_default(server):
'''Munki defaults to using http://munki/repo as the base URL.
This is useful as a bootstrapping default, but is insecure.
Warn the admin if Munki is using an insecure default.'''
# server can be either ManifestURL or SoftwareRepoURL
if server.rstrip('/') in ['http://munki/repo',
'http://munki/repo/manifests']:
display.display_warning(
'Client is configured to use the default repo, which is insecure. '
'Client could be trivially compromised when off your '
'organization\'s network. '
'Consider using a non-default URL, and preferably an https:// URL.')
def main():
"""Main"""
if launch.get_socket_fd():
munkilog.log('Launched via socket')
time.sleep(10)
sys.exit()
# install handler for SIGTERM
signal.signal(signal.SIGTERM, signal_handler)
# 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. Not tested or supported '
'with other options.')
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 Center.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 manifest retrieval')
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, dummy_arguments = p.parse_args()
if options.version:
print info.get_version()
exit(0)
# check to see if we're root
if os.geteuid() != 0:
print >> sys.stderr, 'You must run this as root!'
exit(constants.EXIT_STATUS_ROOT_REQUIRED)
# check to see if another instance of this script is running
myname = os.path.basename(sys.argv[0])
other_managedsoftwareupdate_pid = osutils.pythonScriptRunning(myname)
if other_managedsoftwareupdate_pid:
# another instance of this script is running, so we should quit
munkilog.log('*' * 60)
munkilog.log('%s launched as pid %s' % (myname, os.getpid()))
munkilog.log('Another instance of %s is running as pid %s.'
% (myname, other_managedsoftwareupdate_pid))
munkilog.log('pid %s exiting.' % os.getpid())
munkilog.log('*' * 60)
print >> sys.stderr, (
'Another instance of %s is running. Exiting.' % myname)
osutils.cleanUpTmpDir()
exit(0)
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.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 60 seconds for the network
# interfaces to come up before continuing
display.display_status_minor('Waiting for network...')
for dummy_i in range(60):
if networkUp():
break
time.sleep(1)
else:
# delete triggerfile if _not_ checkandinstallatstartup
os.unlink(filename)
if not user_triggered:
# no trigger file was found -- how'd we get launched?
osutils.cleanUpTmpDir()
exit(0)
if options.installwithnologout:
# typically invoked by Managed Software Center.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 Center.app
launchdtriggerfile = \
'/private/tmp/.com.googlecode.munki.updatecheck.launchd'
if os.path.exists(launchdtriggerfile):
try:
launch_options = FoundationPlist.readPlist(launchdtriggerfile)
options.munkipkgsonly = launch_options.get(
'SuppressAppleUpdateCheck')
except FoundationPlist.FoundationPlistException:
pass
# 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(constants.EXIT_STATUS_INVALID_PARAMETERS)
# set munkicommon globals
display.munkistatusoutput = options.munkistatusoutput
display.verbose = options.verbose
# Set environment variable for verbosity
os.environ['MUNKI_VERBOSITY_LEVEL'] = str(options.verbose)
if options.installonly:
# we're only installing, not checking, so we should copy
# some report values from the prior run
reports.readreport()
# start a new report
reports.report['StartTime'] = reports.format_time()
reports.report['RunType'] = runtype
# Clearing arrays must be run before any call to display_warning/error.
reports.report['Errors'] = []
reports.report['Warnings'] = []
if prefs.pref('LogToSyslog'):
munkilog.configure_syslog()
munkilog.log("### Starting managedsoftwareupdate run: %s ###" % runtype)
if options.verbose:
print 'Managed Software Update Tool'
print 'Copyright 2010-2017 The Munki Project'
print 'https://github.com/munki/munki\n'
display.display_status_major('Starting...')
sendStartNotification()
# run the preflight script if it exists
preflightscript = os.path.join(scriptdir, 'preflight')
result = runScript(preflightscript, 'preflight', runtype)
if result:
# non-zero return code means don't run
display.display_info(
'managedsoftwareupdate run aborted by preflight script: %s'
% result)
# record the check result for use by Managed Software Center.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)
# tell status app we're done sending status
munkistatus.quit_app()
osutils.cleanUpTmpDir()
exit(result)
# Force a prefs refresh, in case preflight modified the prefs file.
prefs.reload_prefs()
# create needed directories if necessary
if not initMunkiDirs():
exit(constants.EXIT_STATUS_MUNKI_DIRS_FAILURE)
applesoftwareupdatesonly = (prefs.pref('AppleSoftwareUpdatesOnly')
or options.applesuspkgsonly)
skip_munki_check = (options.installonly or applesoftwareupdatesonly)
if not skip_munki_check:
# check to see if we are using an insecure default
server = (prefs.pref('ManifestURL') or
prefs.pref('SoftwareRepoURL'))
warn_if_server_is_default(server)
# reset our errors and warnings files, rotate main log if needed
munkilog.reset_errors()
munkilog.reset_warnings()
munkilog.rotate_main_log()
# archive the previous session's report
reports.archive_report()
if applesoftwareupdatesonly and options.verbose:
print ('NOTE: managedsoftwareupdate is configured to process Apple '
'Software Updates only.')
updatecheckresult = None
if not skip_munki_check:
try:
updatecheckresult = updatecheck.check(
client_id=options.id.decode('UTF-8'))
except:
display.display_error('Unexpected error in updatecheck:')
munkilog.log(traceback.format_exc())
reports.savereport()
raise
if updatecheckresult is not None:
recordUpdateCheckResult(updatecheckresult)
updatesavailable = munkiUpdatesAvailable()
appleupdatesavailable = 0
# should we do Apple Software updates this run?
if applesoftwareupdatesonly:
# admin told us to only do Apple updates this run
should_do_apple_updates = True
elif options.munkipkgsonly:
# admin told us to skip Apple updates for this run
should_do_apple_updates = False
elif munkiUpdatesContainAppleItems():
# shouldn't run Software Update if we're doing Apple items
# with Munki items
should_do_apple_updates = False
munkilog.log('Skipping Apple Software Updates because items to be '
'installed from the Munki repo contain Apple items.')
# if there are force_install_after_date items in a pre-existing
# AppleUpdates.plist this means we are blocking those updates.
# we need to delete AppleUpdates.plist so that other code doesn't
# mistakenly alert for forced installs it isn't actually going to
# install.
appleupdates_plist = os.path.join(
prefs.pref('ManagedInstallDir'), 'AppleUpdates.plist')
try:
os.unlink(appleupdates_plist)
except OSError:
pass
else:
# check the normal preferences
should_do_apple_updates = prefs.pref('InstallAppleSoftwareUpdates')
if should_do_apple_updates:
if not options.installonly and not processes.stop_requested():
force_update_check = False
force_catalog_refresh = False
if options.manualcheck or runtype == 'checkandinstallatstartup':
force_update_check = True
if runtype == 'custom' and applesoftwareupdatesonly:
force_update_check = True
force_catalog_refresh = True
try:
appleupdatesavailable = \
appleupdates.appleSoftwareUpdatesAvailable(
forcecheck=force_update_check, client_id=options.id,
forcecatalogrefresh=force_catalog_refresh)
except:
display.display_error('Unexpected error in appleupdates:')
munkilog.log(traceback.format_exc())
reports.savereport()
raise
if applesoftwareupdatesonly:
# normally we record the result of checking for Munki updates
# but if we are only doing Apple updates, we should record the
# result of the Apple updates check
if appleupdatesavailable:
recordUpdateCheckResult(1)
else:
recordUpdateCheckResult(0)
if options.installonly:
# 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, client_id=options.id)
except:
display.display_error('Unexpected error in appleupdates:')
munkilog.log(traceback.format_exc())
reports.savereport()
raise
# display any available update information
if updatecheckresult:
installinfo.display_update_info()
if appleupdatesavailable:
appleupdates.displayAppleUpdateInfo()
# send a notification event so MSC can update its display if needed
sendUpdateNotification()
mustrestart = False
mustlogout = False
notify_user = False
force_action = None
if updatesavailable or appleupdatesavailable:
if options.installonly or options.logoutinstall:
# just install
mustrestart = doInstallTasks(appleupdatesavailable)
# reset our count of available updates (it might not actually
# be zero, but we want to clear the badge on the Dock icon;
# it can be updated to the "real" count on the next Munki run)
updatesavailable = 0
appleupdatesavailable = 0
# send a notification event so MSU can update its display
# if needed
sendUpdateNotification()
elif options.auto:
if not osutils.currentGUIusers(): # no GUI users
if prefs.pref('SuppressAutoInstall'):
# admin says we can never install packages
# without user approval/initiation
munkilog.log('Skipping auto install because '
'SuppressAutoInstall is true.')
elif prefs.pref('SuppressLoginwindowInstall'):
# admin says we can't install pkgs at loginwindow
# unless they don't require a logout or restart
# (and are marked with unattended_install = True)
#
# check for packages that need to be force installed
# soon and convert them to unattended_installs if they
# don't require a logout
dummy_action = installinfo.force_install_package_check()
# now install anything that can be done unattended
munkilog.log('Installing only items marked unattended '
'because SuppressLoginwindowInstall is true.')
dummy_restart = doInstallTasks(
appleupdatesavailable, only_unattended=True)
elif getIdleSeconds() < 10:
munkilog.log(
'Skipping auto install at loginwindow because system '
'is not idle (keyboard or mouse activity).')
elif processes.is_app_running(
'/System/Library/CoreServices/FileSyncAgent.app'):
munkilog.log(
'Skipping auto install at loginwindow because '
'FileSyncAgent.app is running '
'(HomeSyncing a mobile account on login?).')
else:
# no GUI users, system is idle, so we can install
# but first, enable status output over login window
display.munkistatusoutput = True
munkilog.log('No GUI users, installing at login window.')
munkistatus.launchMunkiStatus()
mustrestart = doInstallTasks(appleupdatesavailable)
# reset our count of available updates
updatesavailable = 0
appleupdatesavailable = 0
else: # there are GUI users
if prefs.pref('SuppressAutoInstall'):
munkilog.log('Skipping unattended installs because '
'SuppressAutoInstall is true.')
else:
# check for packages that need to be force installed
# soon and convert them to unattended_installs if they
# don't require a logout
dummy_action = installinfo.force_install_package_check()
# install anything that can be done unattended
dummy_restart = doInstallTasks(
appleupdatesavailable, only_unattended=True)
# send a notification event so MSC can update its display
# if needed
sendUpdateNotification()
force_action = installinfo.force_install_package_check()
# 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 and
# AppleUpdates.plist again
updatesavailable = munkiUpdatesAvailable()
if appleupdatesavailable:
# there were Apple updates available, but we might have
# installed some unattended
try:
appleupdatesavailable = (
appleupdates.appleSoftwareUpdatesAvailable(
suppresscheck=True, client_id=options.id))
except:
display.display_error(
'Unexpected error in appleupdates:')
munkilog.log(traceback.format_exc())
reports.savereport()
raise
if appleupdatesavailable or updatesavailable:
# set a flag to notify the user of available updates
# after we conclude this run.
notify_user = 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)
display.display_status_major('Finishing...')
doFinishingTasks()
sendDockUpdateNotification()
sendEndNotification()
munkilog.log("### Ending managedsoftwareupdate run ###")
if options.verbose:
print 'Done.'
if notify_user:
# it may have been more than a minute since we ran our original
# updatecheck so tickle the updatecheck time so MSC.app knows to
# display results immediately
recordUpdateCheckResult(1)
if force_action:
notifyUserOfUpdates(force=True)
time.sleep(2)
startLogoutHelper()
elif osutils.getconsoleuser() == u'loginwindow':
# someone is logged in, but we're sitting at the loginwindow due to
# to fast user switching so do nothing
pass
elif not prefs.pref('SuppressUserNotification'):
notifyUserOfUpdates()
else:
munkilog.log('Skipping user notification because '
'SuppressUserNotification is true.')
osutils.cleanUpTmpDir()
if mustlogout:
# not handling this currently
pass
if mustrestart:
doRestart()
else:
# tell status app we're done sending status
munkistatus.quit_app()
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 osutils.currentGUIusers():
# no-one is logged in
idleseconds = getIdleSeconds()
if 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:
# we're still not idle.
# if the trigger file is present when we exit, we'll
# be relaunched by launchd, so we need to remove it
# to prevent automatic relaunch.
munkilog.log(
'System not idle -- '
'removing trigger file to prevent relaunch')
try:
os.unlink(checkandinstallatstartupflag)
except OSError:
pass
if __name__ == '__main__':
main()