# 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.')