Files
munki/code/client/munkilib/appleupdates/au.py

944 lines
38 KiB
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.
"""
appleupdates.au
Created by Greg Neagle on 2017-01-06.
AppleUpdates object defined here
"""
import glob
import hashlib
import os
import subprocess
import time
import urllib2
# 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
# pylint: enable=E0611
from . import dist
from . import su_prefs
from . import sync
from ..updatecheck import catalogs
from .. import display
from .. import fetch
from .. import launchd
from .. import munkistatus
from .. import munkihash
from .. import munkilog
from .. import osutils
from .. import prefs
from .. import processes
from .. import reports
from .. import updatecheck
from .. import FoundationPlist
# Apple's index of downloaded updates
INDEX_PLIST = '/Library/Updates/index.plist'
class AppleUpdates(object):
"""Class for installation of Apple Software Updates within Munki.
This class handles update detection, as well as downloading and installation
of those updates.
"""
RESTART_ACTIONS = ['RequireRestart', 'RecommendRestart']
def __init__(self):
self._managed_install_dir = prefs.pref('ManagedInstallDir')
self.apple_updates_plist = os.path.join(
self._managed_install_dir, 'AppleUpdates.plist')
# fix things if somehow we died last time before resetting the
# original CatalogURL
os_version_tuple = osutils.getOsVersion(as_tuple=True)
if os_version_tuple in [(10, 9), (10, 10)]:
su_prefs.reset_original_catalogurl()
self.applesync = sync.AppleUpdateSync()
self._update_list_cache = None
# apple_update_metadata support
self.client_id = ''
self.force_catalog_refresh = False
def _display_status_major(self, message):
"""Resets MunkiStatus detail/percent, logs and msgs GUI.
Args:
message: str message to display to the user and log.
"""
# pylint: disable=no-self-use
display.display_status_major(message)
def restart_needed(self):
"""Returns True if any update requires an restart."""
try:
apple_updates = FoundationPlist.readPlist(self.apple_updates_plist)
except FoundationPlist.NSPropertyListSerializationException:
return True
for item in apple_updates.get('AppleUpdates', []):
if item.get('RestartAction') in self.RESTART_ACTIONS:
return True
# if we get this far, there must be no items that require restart
return False
def clear_apple_update_info(self):
"""Clears Apple update info.
This is called after performing munki updates because the Apple updates
may no longer be relevant.
"""
try:
os.unlink(self.apple_updates_plist)
except (OSError, IOError):
pass
def download_available_updates(self):
"""Downloads available Apple updates.
Returns:
Boolean. True if successful, False otherwise.
"""
msg = 'Checking for available Apple Software Updates...'
self._display_status_major(msg)
if os.path.exists(INDEX_PLIST):
# try to remove old/stale /Library/Updates/index.plist --
# in some older versions of OS X this can hang around and is not
# always cleaned up when /usr/sbin/softwareupdate finds no updates
try:
os.unlink(INDEX_PLIST)
except OSError:
pass
os_version_tuple = osutils.getOsVersion(as_tuple=True)
if os_version_tuple >= (10, 11):
catalog_url = None
else:
catalog_url = self.applesync.get_apple_catalogurl()
retcode = self._run_softwareupdate(
['-d', '-a'], catalog_url=catalog_url, stop_allowed=True)
if retcode: # there was an error
display.display_error('softwareupdate error: %s', retcode)
return False
# not sure all older OS X versions set LastSessionSuccessful, so
# react only if it's explicitly set to False
last_session_successful = su_prefs.pref(
'LastSessionSuccessful')
if last_session_successful is False:
display.display_error(
'softwareupdate reported an unsuccessful download session.')
return False
return True
def available_update_product_ids(self):
"""Returns a list of product IDs of available Apple updates.
Returns:
A list of string Apple update products ids.
"""
# pylint: disable=no-self-use
# first, try to get the list from com.apple.SoftwareUpdate preferences
recommended_updates = su_prefs.pref(
'RecommendedUpdates')
if recommended_updates:
return [item['Product Key'] for item in recommended_updates
if 'Product Key' in item]
# not in com.apple.SoftwareUpdate preferences, try index.plist
if not os.path.exists(INDEX_PLIST):
display.display_debug1('%s does not exist.' % INDEX_PLIST)
return []
try:
product_index = FoundationPlist.readPlist(INDEX_PLIST)
products = product_index.get('ProductPaths', {})
return products.keys()
except (FoundationPlist.FoundationPlistException,
KeyError, AttributeError), err:
display.display_error(
"Error processing %s: %s", INDEX_PLIST, err)
return []
def installed_apple_pkgs_changed(self):
"""Generates a SHA-256 checksum of the info for all packages in the
receipts database whose id matches com.apple.* and compares it to a
stored version of this checksum.
Returns:
Boolean. False if the checksums match, True if they differ."""
# pylint: disable=no-self-use
cmd = ['/usr/sbin/pkgutil', '--regexp', '--pkg-info-plist',
r'com\.apple\.*']
proc = subprocess.Popen(cmd, shell=False, bufsize=1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, dummy_err = proc.communicate()
current_apple_packages_checksum = hashlib.sha256(output).hexdigest()
old_apple_packages_checksum = prefs.pref(
'InstalledApplePackagesChecksum')
if current_apple_packages_checksum == old_apple_packages_checksum:
return False
else:
prefs.set_pref('InstalledApplePackagesChecksum',
current_apple_packages_checksum)
return True
def _force_check_necessary(self, original_hash):
"""Returns True if a force check is needed, False otherwise.
Args:
original_hash: the SHA-256 hash of the Apple catalog before being
redownloaded.
Returns:
Boolean. True if a force check is needed, False otherwise.
"""
new_hash = munkihash.getsha256hash(
self.applesync.apple_download_catalog_path)
if original_hash != new_hash:
munkilog.log('Apple update catalog has changed.')
return True
if self.installed_apple_pkgs_changed():
munkilog.log('Installed Apple packages have changed.')
return True
if not self.available_updates_downloaded():
munkilog.log('Downloaded updates do not match our list of '
'available updates.')
return True
return False
def check_for_software_updates(self, force_check=True):
"""Check if Apple Software Updates are available, if needed or forced.
Args:
force_check: Boolean. If True, forces a check, otherwise only checks
if the last check is deemed outdated.
Returns:
Boolean. True if there are updates, False otherwise.
"""
before_hash = munkihash.getsha256hash(
self.applesync.apple_download_catalog_path)
msg = 'Checking Apple Software Update catalog...'
self._display_status_major(msg)
try:
self.applesync.cache_apple_catalog()
except sync.CatalogNotFoundError:
return False
except (sync.ReplicationError, fetch.Error) as err:
display.display_warning(
'Could not download Apple SUS catalog:')
display.display_warning('\t%s', unicode(err))
return False
if not force_check and not self._force_check_necessary(before_hash):
display.display_info(
'Skipping Apple Software Update check '
'because sucatalog is unchanged, installed Apple packages are '
'unchanged and we recently did a full check.')
# return True if we have cached updates; False otherwise
return bool(self.software_update_info())
if self.download_available_updates(): # Success; ready to install.
prefs.set_pref('LastAppleSoftwareUpdateCheck', NSDate.date())
product_ids = self.available_update_product_ids()
if not product_ids:
# No updates found
self.applesync.clean_up_cache()
return False
try:
self.applesync.cache_update_metadata(product_ids)
except sync.ReplicationError as err:
display.display_warning(
'Could not replicate software update metadata:')
display.display_warning('\t%s', unicode(err))
return False
return True
else:
# Download error, allow check again soon.
display.display_error(
'Could not download all available Apple updates.')
prefs.set_pref('LastAppleSoftwareUpdateCheck', None)
return False
def update_downloaded(self, product_key):
"""Verifies that a given update appears to be downloaded.
Returns a boolean."""
# pylint: disable=no-self-use
product_dir = os.path.join('/Library/Updates', product_key)
if not os.path.isdir(product_dir):
munkilog.log(
'Apple Update product directory %s is missing'
% product_key)
return False
else:
pkgs = glob.glob(os.path.join(product_dir, '*.pkg'))
if not pkgs:
munkilog.log(
'Apple Update product directory %s contains no pkgs'
% product_key)
return False
return True
def available_updates_downloaded(self):
"""Verifies that applicable/available updates have been downloaded.
Returns:
Boolean. False if a product directory are missing,
True otherwise (including when there are no available
updates).
"""
apple_updates = self.software_update_info()
if not apple_updates:
return True
for update in apple_updates:
if not self.update_downloaded(update.get('productKey')):
return False
return True
def software_update_info(self):
"""Uses /Library/Preferences/com.apple.SoftwareUpdate.plist or
/Library/Updates/index.plist to generate the AppleUpdates.plist,
which records available updates in the format that
Managed Software Update.app expects.
Returns:
List of dictionary update data.
"""
update_display_names = {}
update_versions = {}
product_keys = []
english_su_info = {}
apple_updates = []
# first, try to get the list from com.apple.SoftwareUpdate preferences
recommended_updates = su_prefs.pref('RecommendedUpdates')
if recommended_updates:
for item in recommended_updates:
try:
update_display_names[item['Product Key']] = (
item['Display Name'])
except (TypeError, AttributeError, KeyError):
pass
try:
update_versions[item['Product Key']] = (
item['Display Version'])
except (TypeError, AttributeError, KeyError):
pass
try:
product_keys = [item['Product Key']
for item in recommended_updates]
except (TypeError, AttributeError, KeyError):
pass
if not product_keys:
# next, try to get the applicable/recommended updates from
# /Library/Updates/index.plist
if os.path.exists(INDEX_PLIST):
try:
product_index = FoundationPlist.readPlist(INDEX_PLIST)
products = product_index.get('ProductPaths', {})
product_keys = products.keys()
except (FoundationPlist.FoundationPlistException,
AttributeError, TypeError), err:
display.display_error(
"Error parsing %s: %s", INDEX_PLIST, err)
for product_key in product_keys:
if not self.update_downloaded(product_key):
display.display_warning(
'Product %s does not appear to be downloaded',
product_key)
continue
localized_dist = self.applesync.distribution_for_product_key(
product_key)
if not localized_dist:
display.display_warning(
'No dist file for product %s', product_key)
continue
if (not recommended_updates and
not localized_dist.endswith('English.dist')):
# we need the English versions of some of the data
# see (https://groups.google.com/d/msg/munki-dev/
# _5HdMyy3kKU/YFxqslayDQAJ)
english_dist = self.applesync.distribution_for_product_key(
product_key, 'English')
if english_dist:
english_su_info = dist.parse_su_dist(english_dist)
su_info = dist.parse_su_dist(localized_dist)
su_info['productKey'] = product_key
if su_info['name'] == '':
su_info['name'] = product_key
if product_key in update_display_names:
su_info['apple_product_name'] = (
update_display_names[product_key])
elif english_su_info:
su_info['apple_product_name'] = (
english_su_info['apple_product_name'])
if product_key in update_versions:
su_info['version_to_install'] = update_versions[product_key]
elif english_su_info:
su_info['version_to_install'] = (
english_su_info['version_to_install'])
apple_updates.append(su_info)
return apple_updates
def write_appleupdates_file(self):
"""Writes a file used by the MSU GUI to display available updates.
Returns:
Integer. Count of available Apple updates.
"""
apple_updates = self.software_update_info()
if apple_updates:
if not prefs.pref('AppleSoftwareUpdatesOnly'):
cataloglist = updatecheck.get_primary_manifest_catalogs(
self.client_id, force_refresh=self.force_catalog_refresh)
if cataloglist:
# Check for apple_update_metadata
display.display_detail(
'**Checking for Apple Update Metadata**')
for item in apple_updates:
# Find matching metadata item
metadata_item = catalogs.get_item_detail(
item['productKey'], cataloglist,
vers='apple_update_metadata')
if metadata_item:
display.display_debug1(
'Processing metadata for %s, %s...',
item['productKey'], item['display_name'])
self.copy_update_metadata(item, metadata_item)
plist = {'AppleUpdates': apple_updates}
FoundationPlist.writePlist(plist, self.apple_updates_plist)
return len(apple_updates)
else:
try:
os.unlink(self.apple_updates_plist)
except (OSError, IOError):
pass
return 0
def display_apple_update_info(self):
"""Prints Apple update information and updates ManagedInstallReport."""
try:
pl_dict = FoundationPlist.readPlist(self.apple_updates_plist)
except FoundationPlist.FoundationPlistException:
display.display_error(
'Error reading: %s', self.apple_updates_plist)
return
apple_updates = pl_dict.get('AppleUpdates', [])
if not apple_updates:
display.display_info('No available Apple Software Updates.')
return
reports.report['AppleUpdates'] = apple_updates
display.display_info(
'The following Apple Software Updates are available to '
'install:')
for item in apple_updates:
display.display_info(
' + %s-%s' % (
item.get('display_name', ''),
item.get('version_to_install', '')))
if item.get('RestartAction') in self.RESTART_ACTIONS:
display.display_info(' *Restart required')
reports.report['RestartRequired'] = True
elif item.get('RestartAction') == 'RequireLogout':
display.display_info(' *Logout required')
reports.report['LogoutRequired'] = True
def _run_softwareupdate(
self, options_list, catalog_url=None, stop_allowed=False,
mode=None, results=None):
"""Runs /usr/sbin/softwareupdate with options.
Provides user feedback via command line or MunkiStatus.
Args:
options_list: sequence of options to send to softwareupdate.
stopped_allowed:
mode:
results:
Returns:
Integer softwareupdate exit code.
"""
if results is None:
# we're not interested in the results,
# but need to create a temporary dict anyway
results = {}
# we need to wrap our call to /usr/sbin/softwareupdate with a utility
# that makes softwareupdate think it is connected to a tty-like
# device so its output is unbuffered so we can get progress info
#
# Try to find our ptyexec tool
# first look in the parent directory of the parent directory of this
# file's directory
# (../)
parent_dir = (
os.path.dirname(
os.path.dirname(
os.path.dirname(
os.path.abspath(__file__)))))
ptyexec_path = os.path.join(parent_dir, 'ptyexec')
if not os.path.exists(ptyexec_path):
# try absolute path in munki's normal install dir
ptyexec_path = '/usr/local/munki/ptyexec'
if os.path.exists(ptyexec_path):
cmd = [ptyexec_path]
else:
# fall back to /usr/bin/script
# this is not preferred because it uses way too much CPU
# checking stdin for input that will never come...
cmd = ['/usr/bin/script', '-q', '-t', '1', '/dev/null']
cmd.extend(['/usr/sbin/softwareupdate', '--verbose'])
os_version_tuple = osutils.getOsVersion(as_tuple=True)
if catalog_url:
# OS version-specific stuff to use a specific CatalogURL
if os_version_tuple < (10, 9):
cmd.extend(['--CatalogURL', catalog_url])
elif os_version_tuple in [(10, 9), (10, 10)]:
su_prefs.set_custom_catalogurl(catalog_url)
cmd.extend(options_list)
display.display_debug1('softwareupdate cmd: %s', cmd)
try:
job = launchd.Job(cmd)
job.start()
except launchd.LaunchdJobException as err:
display.display_warning(
'Error with launchd job (%s): %s', cmd, err)
display.display_warning('Skipping softwareupdate run.')
return -3
results['installed'] = []
results['download'] = []
results['failures'] = []
last_output = None
while True:
if stop_allowed and processes.stop_requested():
job.stop()
break
output = job.stdout.readline()
if not output:
if job.returncode() is not None:
break
else:
# no data, but we're still running
# sleep a bit before checking for more output
time.sleep(1)
continue
# Don't bother parsing the stdout output if it hasn't changed since
# the last loop iteration.
if last_output == output:
continue
last_output = output
output = output.decode('UTF-8').strip()
# send the output to STDOUT or MunkiStatus as applicable
if output.startswith('Progress: '):
# Snow Leopard/Lion progress info with '-v' flag
try:
percent = int(output[10:].rstrip('%'))
except ValueError:
percent = -1
display.display_percent_done(percent, 100)
elif output.startswith('Software Update Tool'):
# don't display this
pass
elif output.startswith('Copyright 2'):
# don't display this
pass
elif output.startswith('Installing ') and mode == 'install':
item = output[11:]
if item:
self._display_status_major(output)
elif output.startswith('Downloaded ') and mode == 'install':
# don't display this
pass
elif output.startswith('Installed '):
# 10.6 / 10.7 / 10.8. Successful install of package name.
if mode == 'install':
display.display_status_minor(output)
results['installed'].append(output[10:])
else:
pass
# don't display.
# softwareupdate logging "Installed" at the end of a
# successful download-only session is odd.
elif output.startswith('Done with ') and mode == 'install':
# 10.9 successful install
display.display_status_minor(output)
results['installed'].append(output[10:])
elif output.startswith('Done '):
# 10.5. Successful install of package name.
display.display_status_minor(output)
results['installed'].append(output[5:])
elif output.startswith('Downloading ') and mode == 'install':
# This is 10.5 & 10.7 behavior for a missing subpackage.
display.display_warning(
'A necessary subpackage is not available on disk '
'during an Apple Software Update installation '
'run: %s' % output)
results['download'].append(output[12:])
elif output.startswith('Package failed:'):
# Doesn't tell us which package.
display.display_error(
'Apple update failed to install: %s' % output)
results['failures'].append(output)
elif output.startswith('x '):
# don't display this, it's just confusing
pass
elif 'Missing bundle identifier' in output:
# don't display this, it's noise
pass
elif output == '':
pass
else:
display.display_status_minor(output)
if catalog_url:
# reset CatalogURL if needed
if os_version_tuple in [(10, 9), (10, 10)]:
su_prefs.reset_original_catalogurl()
retcode = job.returncode()
if retcode == 0:
# get SoftwareUpdate's LastResultCode
last_result_code = su_prefs.pref(
'LastResultCode') or 0
if last_result_code > 2:
retcode = last_result_code
if results['failures']:
return 1
return retcode
def install_apple_updates(self, only_unattended=False):
"""Uses softwareupdate to install previously downloaded updates.
Returns:
Boolean. True if a restart is needed after install, False otherwise.
"""
# disable Stop button if we are presenting GUI status
if display.munkistatusoutput:
munkistatus.hideStopButton()
# Get list of unattended_installs
if only_unattended:
msg = 'Installing unattended Apple Software Updates...'
unattended_install_items, unattended_install_product_ids = \
self.get_unattended_installs()
# ensure that we don't restart for unattended installations
restartneeded = False
if not unattended_install_items:
return False # didn't find any unattended installs
else:
msg = 'Installing available Apple Software Updates...'
restartneeded = self.restart_needed()
self._display_status_major(msg)
installlist = self.software_update_info()
installresults = {'installed': [], 'download': []}
su_options = ['-i']
if only_unattended:
# Append list of unattended_install items
su_options.extend(unattended_install_items)
# Filter installist to only include items
# which we're attempting to install
installlist = [item for item in installlist
if item.get('productKey') in
unattended_install_product_ids]
else:
# We're installing all available updates; add all their names
for item in installlist:
su_options.append(
item['name'] + '-' + item['version_to_install'])
# new in 10.11: '--no-scan' flag to tell softwareupdate to just install
# and not rescan for available updates.
os_version_tuple = osutils.getOsVersion(as_tuple=True)
if os_version_tuple >= (10, 11):
su_options.append('--no-scan')
# 10.11 seems not to like file:// URLs, and we don't really need
# to switch to a local file URL anyway since we now have the
# --no-scan option
catalog_url = None
else:
# use our local catalog
if not os.path.exists(self.applesync.local_catalog_path):
display.display_error(
'Missing local Software Update catalog at %s',
self.applesync.local_catalog_path)
return False # didn't do anything, so no restart needed
catalog_url = 'file://localhost' + urllib2.quote(
self.applesync.local_catalog_path)
retcode = self._run_softwareupdate(
su_options, mode='install', catalog_url=catalog_url,
results=installresults)
if not 'InstallResults' in reports.report:
reports.report['InstallResults'] = []
display.display_debug1(
'Raw Apple Update install results: %s', installresults)
for item in installlist:
rep = {}
rep['name'] = item.get('apple_product_name')
rep['version'] = item.get('version_to_install', '')
rep['applesus'] = True
rep['time'] = NSDate.new()
rep['productKey'] = item.get('productKey', '')
message = 'Apple Software Update install of %s-%s: %s'
if rep['name'] in installresults['installed']:
rep['status'] = 0
install_status = 'SUCCESSFUL'
elif ('display_name' in item and
item['display_name'] in installresults['installed']):
rep['status'] = 0
install_status = 'SUCCESSFUL'
elif rep['name'] in installresults['download']:
rep['status'] = -1
install_status = 'FAILED due to missing package.'
display.display_warning(
'Apple update %s, %s failed. A sub-package was missing '
'on disk at time of install.'
% (rep['name'], rep['productKey']))
else:
rep['status'] = -2
install_status = 'FAILED for unknown reason'
display.display_warning(
'Apple update %s, %s may have failed to install. No record '
'of success or failure.', rep['name'], rep['productKey'])
if installresults['installed']:
display.display_warning(
'softwareupdate recorded these installations: %s',
installresults['installed'])
reports.report['InstallResults'].append(rep)
log_msg = message % (rep['name'], rep['version'], install_status)
munkilog.log(log_msg, 'Install.log')
if retcode: # there was an error
display.display_error('softwareupdate error: %s' % retcode)
# clean up our now stale local cache
if not only_unattended:
self.applesync.clean_up_cache()
# remove the now invalid AppleUpdates.plist and AvailableUpdates.plist
self.clear_apple_update_info()
# Also clear our pref value for last check date. We may have
# just installed an update which is a pre-req for some other update.
# Let's check again soon.
prefs.set_pref('LastAppleSoftwareUpdateCheck', None)
# show stop button again
if display.munkistatusoutput:
munkistatus.showStopButton()
return restartneeded
def software_updates_available(
self, force_check=False, suppress_check=False):
"""Checks for available Apple Software Updates, trying not to hit the
SUS more than needed.
Args:
force_check: Boolean. If True, forces a softwareupdate run.
suppress_check: Boolean. If True, skips a softwareupdate run.
Returns:
Integer. Count of available Apple updates.
"""
success = True
if suppress_check:
# don't check at all --
# typically because we are doing a logout install
# just return any AppleUpdates info we already have
if not os.path.exists(self.apple_updates_plist):
return 0
try:
plist = FoundationPlist.readPlist(self.apple_updates_plist)
except FoundationPlist.FoundationPlistException:
plist = {}
return len(plist.get('AppleUpdates', []))
if force_check:
# typically because user initiated the check from
# Managed Software Update.app
success = self.check_for_software_updates(force_check=True)
else:
# have we checked recently? Don't want to check with
# Apple Software Update server too frequently
now = NSDate.new()
next_su_check = now
last_su_check_string = prefs.pref(
'LastAppleSoftwareUpdateCheck')
if last_su_check_string:
try:
last_su_check = NSDate.dateWithString_(
last_su_check_string)
# dateWithString_ returns None if invalid date string.
if not last_su_check:
raise ValueError
interval = 24 * 60 * 60
# only force check every 24 hours.
next_su_check = last_su_check.dateByAddingTimeInterval_(
interval)
except (ValueError, TypeError):
pass
if now.timeIntervalSinceDate_(next_su_check) >= 0:
success = self.check_for_software_updates(force_check=True)
else:
success = self.check_for_software_updates(force_check=False)
display.display_debug1(
'CheckForSoftwareUpdates result: %s' % success)
if success:
count = self.write_appleupdates_file()
else:
self.clear_apple_update_info()
return 0
return count
def copy_update_metadata(self, item, metadata):
"""Applies metadata to Apple update item restricted
to keys contained in 'metadata_to_copy'.
"""
# pylint: disable=no-self-use
metadata_to_copy = ['blocking_applications',
'description',
'display_name',
'force_install_after_date',
'unattended_install',
'RestartAction']
# Mapping of supported restart_actions to
# equal or greater auxiliary actions
restart_actions = {
'RequireRestart': ['RequireRestart', 'RecommendRestart'],
'RecommendRestart': ['RequireRestart', 'RecommendRestart'],
'RequireLogout': ['RequireRestart', 'RecommendRestart',
'RequireLogout'],
'None': ['RequireRestart', 'RecommendRestart',
'RequireLogout']
}
for key in metadata:
# Apply 'white-listed', non-empty metadata keys
if key in metadata_to_copy and metadata[key]:
if key == 'RestartAction':
# Ensure that a heavier weighted 'RestartAction' is not
# overridden by one supplied in metadata
if metadata[key] not in restart_actions.get(
item.get(key, 'None')):
display.display_debug2(
'\tSkipping metadata RestartAction\'%s\' '
'for item %s (ProductKey %s), '
'item\'s original \'%s\' is preferred.',
metadata[key], item.get('name'),
item.get('productKey'), item[key])
continue
elif key == 'unattended_install':
# Don't apply unattended_install if a RestartAction exists
# in either the original item or metadata
if metadata.get('RestartAction', 'None') != 'None':
display.display_warning(
'\tIgnoring unattended_install key for Apple '
'update %s (ProductKey %s) '
'because metadata RestartAction is %s.',
item.get('name'), item.get('productKey'),
metadata.get('RestartAction'))
continue
if item.get('RestartAction', 'None') != 'None':
display.display_warning(
'\tIgnoring unattended_install key for Apple '
'update %s (ProductKey %s) '
'because item RestartAction is %s.'
% (item.get('name'), item.get('productKey'),
item.get('RestartAction')))
continue
display.display_debug2('\tApplying %s...' % key)
item[key] = metadata[key]
return item
def get_unattended_installs(self):
"""Processes AppleUpdates.plist to return a list
of NAME-VERSION formatted items and a list of product_ids
which are elgible for unattended installation.
"""
item_list = []
product_ids = []
try:
pl_dict = FoundationPlist.readPlist(self.apple_updates_plist)
except FoundationPlist.FoundationPlistException:
display.display_error(
'Error reading: %s', self.apple_updates_plist)
return item_list, product_ids
apple_updates = pl_dict.get('AppleUpdates', [])
os_version_tuple = osutils.getOsVersion(as_tuple=True)
for item in apple_updates:
if (item.get('unattended_install') or
(prefs.pref('UnattendedAppleUpdates') and
item.get('RestartAction', 'None') is 'None' and
os_version_tuple >= (10, 10))):
if processes.blocking_applications_running(item):
display.display_detail(
'Skipping unattended install of %s because '
'blocking application(s) running.'
% item['display_name'])
continue
install_item = item['name'] + '-' + item['version_to_install']
item_list.append(install_item)
product_ids.append(item['productKey'])
else:
display.display_detail(
'Skipping install of %s because it\'s not unattended.'
% item['display_name'])
return item_list, product_ids
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'