Files
munki/code/client/munkilib/installinfo.py

300 lines
11 KiB
Python

# encoding: utf-8
#
# Copyright 2009-2020 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.
"""
installinfo.py
Created by Greg Neagle on 2017-01-01.
Functions for getting data from the InstallInfo.plist, etc
"""
from __future__ import absolute_import, print_function
# standard libs
import os
# Apple's libs
# 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
# our libs
from . import display
from . import info
from . import prefs
from . import reports
from . import FoundationPlist
try:
_ = xrange # pylint: disable=xrange-builtin
except NameError:
# no xrange in Python 3
xrange = range # pylint: disable=redefined-builtin,invalid-name
# This many hours before a force install deadline, start notifying the user.
FORCE_INSTALL_WARNING_HOURS = 4
def get_installinfo():
'''Returns the dictionary describing the managed installs and removals'''
managedinstallbase = prefs.pref('ManagedInstallDir')
plist = {}
installinfo = os.path.join(managedinstallbase, 'InstallInfo.plist')
if os.path.exists(installinfo):
try:
plist = FoundationPlist.readPlist(installinfo)
except FoundationPlist.NSPropertyListSerializationException:
pass
return plist
def get_appleupdates():
'''Returns any available Apple updates'''
managedinstallbase = prefs.pref('ManagedInstallDir')
plist = {}
appleupdatesfile = os.path.join(managedinstallbase, 'AppleUpdates.plist')
if (os.path.exists(appleupdatesfile) and
(prefs.pref('InstallAppleSoftwareUpdates') or
prefs.pref('AppleSoftwareUpdatesOnly'))):
try:
plist = FoundationPlist.readPlist(appleupdatesfile)
except FoundationPlist.NSPropertyListSerializationException:
pass
return plist
def oldest_pending_update_in_days():
'''Return the datestamp of the oldest pending update'''
pendingupdatespath = os.path.join(
prefs.pref('ManagedInstallDir'), 'UpdateNotificationTracking.plist')
try:
pending_updates = FoundationPlist.readPlist(pendingupdatespath)
except FoundationPlist.NSPropertyListSerializationException:
return 0
oldest_date = now = NSDate.date()
for category in pending_updates:
for name in pending_updates[category]:
this_date = pending_updates[category][name]
if this_date < oldest_date:
oldest_date = this_date
return now.timeIntervalSinceDate_(oldest_date) / (24 * 60 * 60)
def get_pending_update_info():
'''Returns a dict with some data managedsoftwareupdate records at the end
of a run'''
data = {}
installinfo = get_installinfo()
data['install_count'] = len(installinfo.get('managed_installs', []))
data['removal_count'] = len(installinfo.get('removals', []))
appleupdates = get_appleupdates()
data['apple_update_count'] = len(appleupdates.get('AppleUpdates', []))
data['PendingUpdateCount'] = (data['install_count'] + data['removal_count']
+ data['apple_update_count'])
data['OldestUpdateDays'] = oldest_pending_update_in_days()
# calculate earliest date a forced install is due
installs = installinfo.get('managed_installs', [])
installs.extend(appleupdates.get('AppleUpdates', []))
earliest_date = None
for install in installs:
this_force_install_date = install.get('force_install_after_date')
if this_force_install_date:
try:
this_force_install_date = info.subtract_tzoffset_from_date(
this_force_install_date)
if not earliest_date or this_force_install_date < earliest_date:
earliest_date = this_force_install_date
except ValueError:
# bad date!
pass
data['ForcedUpdateDueDate'] = earliest_date
return data
def save_pending_update_times():
'''Record the time each update first is made available. We can use this to
escalate our notifications if there are items that have been skipped a lot
'''
now = NSDate.date()
managed_install_dir = prefs.pref('ManagedInstallDir')
pendingupdatespath = os.path.join(
managed_install_dir, 'UpdateNotificationTracking.plist')
installinfo = get_installinfo()
install_names = [item['name']
for item in installinfo.get('managed_installs', [])]
removal_names = [item['name']
for item in installinfo.get('removals', [])]
appleupdatesinfo = get_appleupdates()
appleupdate_names = [item['name']
for item in appleupdatesinfo.get('AppleUpdates', [])]
update_names = {
'managed_installs': install_names,
'removals': removal_names,
'AppleUpdates': appleupdate_names}
try:
prior_pending_updates = FoundationPlist.readPlist(pendingupdatespath)
except FoundationPlist.NSPropertyListSerializationException:
prior_pending_updates = {}
current_pending_updates = {}
for category in update_names:
current_pending_updates[category] = {}
for name in update_names[category]:
if (category in prior_pending_updates and
name in prior_pending_updates[category]):
# copy the prior datetime from matching item
current_pending_updates[category][name] = prior_pending_updates[
category][name]
else:
# record new item with current datetime
current_pending_updates[category][name] = now
try:
FoundationPlist.writePlist(current_pending_updates, pendingupdatespath)
except FoundationPlist.NSPropertyListWriteException:
# we tried! oh well
pass
def display_update_info():
'''Prints info about available updates'''
def display_and_record_restart_info(item):
'''Displays logout/restart info for item if present and also updates
our report'''
if (item.get('RestartAction') == 'RequireRestart' or
item.get('RestartAction') == 'RecommendRestart'):
display.display_info(' *Restart required')
reports.report['RestartRequired'] = True
if item.get('RestartAction') == 'RequireLogout':
display.display_info(' *Logout required')
reports.report['LogoutRequired'] = True
installinfo = get_installinfo()
installcount = len(installinfo.get('managed_installs', []))
removalcount = len(installinfo.get('removals', []))
if installcount:
display.display_info('')
display.display_info(
'The following items will be installed or upgraded:')
for item in installinfo.get('managed_installs', []):
if item.get('installer_item'):
display.display_info(
' + %s-%s', item.get('name', ''),
item.get('version_to_install', ''))
if item.get('description'):
display.display_info(' %s', item['description'])
display_and_record_restart_info(item)
if removalcount:
display.display_info('The following items will be removed:')
for item in installinfo.get('removals', []):
if item.get('installed'):
display.display_info(' - %s', item.get('name'))
display_and_record_restart_info(item)
if installcount == 0 and removalcount == 0:
display.display_info(
'No changes to managed software are available.')
def force_install_package_check():
"""Check installable packages and applicable Apple updates
for force install parameters.
This method modifies InstallInfo and/or AppleUpdates in one scenario:
It enables the unattended_install flag on all packages which need to be
force installed and do not have a RestartAction.
The return value may be one of:
'now': a force install is about to occur
'soon': a force install will occur within FORCE_INSTALL_WARNING_HOURS
'logout': a force install is about to occur and requires logout
'restart': a force install is about to occur and requires restart
None: no force installs are about to occur
"""
result = None
managed_install_dir = prefs.pref('ManagedInstallDir')
installinfo_types = {'InstallInfo.plist': 'managed_installs'}
if (prefs.pref('InstallAppleSoftwareUpdates') or
prefs.pref('AppleSoftwareUpdatesOnly')):
# only consider Apple updates if the prefs say it's OK
installinfo_types['AppleUpdates.plist'] = 'AppleUpdates'
now = NSDate.date()
now_xhours = NSDate.dateWithTimeIntervalSinceNow_(
FORCE_INSTALL_WARNING_HOURS * 3600)
for installinfo_plist in installinfo_types:
pl_dict = installinfo_types[installinfo_plist]
installinfopath = os.path.join(managed_install_dir, installinfo_plist)
try:
installinfo = FoundationPlist.readPlist(installinfopath)
except FoundationPlist.NSPropertyListSerializationException:
continue
writeback = False
for i in xrange(len(installinfo.get(pl_dict, []))):
install = installinfo[pl_dict][i]
force_install_after_date = install.get('force_install_after_date')
if not force_install_after_date:
continue
force_install_after_date = (
info.subtract_tzoffset_from_date(force_install_after_date))
display.display_debug1(
'Forced install for %s at %s',
install['name'], force_install_after_date)
if now >= force_install_after_date:
result = 'now'
if install.get('RestartAction'):
if install['RestartAction'] == 'RequireLogout':
result = 'logout'
elif (install['RestartAction'] == 'RequireRestart' or
install['RestartAction'] == 'RecommendRestart'):
result = 'restart'
elif not install.get('unattended_install', False):
display.display_debug1(
'Setting unattended install for %s', install['name'])
install['unattended_install'] = True
installinfo[pl_dict][i] = install
writeback = True
if not result and now_xhours >= force_install_after_date:
result = 'soon'
if writeback:
FoundationPlist.writePlist(installinfo, installinfopath)
return result
if __name__ == '__main__':
print('This is a library of support tools for the Munki Suite.')