mirror of
https://github.com/munki/munki.git
synced 2026-01-26 08:59:17 -06:00
1024 lines
45 KiB
Python
1024 lines
45 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.
|
|
"""
|
|
updatecheck.analyze
|
|
|
|
Created by Greg Neagle on 2017-01-10.
|
|
|
|
"""
|
|
from __future__ import absolute_import, print_function
|
|
|
|
import datetime
|
|
import os
|
|
|
|
from . import catalogs
|
|
from . import compare
|
|
from . import download
|
|
from . import installationstate
|
|
from . import manifestutils
|
|
from . import unused_software
|
|
|
|
from .. import display
|
|
from .. import fetch
|
|
from .. import info
|
|
from .. import munkilog
|
|
from .. import prefs
|
|
from .. import processes
|
|
from ..wrappers import is_a_string
|
|
|
|
|
|
def item_in_installinfo(item_pl, thelist, vers=''):
|
|
"""Determines if an item is in a list of processed items.
|
|
|
|
Returns True if the item has already been processed (it's in the list)
|
|
and, optionally, the version is the same or greater.
|
|
"""
|
|
for listitem in thelist:
|
|
try:
|
|
if listitem['name'] == item_pl['name']:
|
|
if not vers:
|
|
return True
|
|
#if the version already installed or processed to be
|
|
#installed is the same or greater, then we're good.
|
|
if listitem.get('installed') and (compare.compare_versions(
|
|
listitem.get('installed_version'), vers) in (1, 2)):
|
|
return True
|
|
if (compare.compare_versions(
|
|
listitem.get('version_to_install'), vers) in (1, 2)):
|
|
return True
|
|
except KeyError:
|
|
# item is missing 'name', so doesn't match
|
|
pass
|
|
|
|
return False
|
|
|
|
|
|
def is_apple_item(item_pl):
|
|
"""Returns True if the item to be installed or removed appears to be from
|
|
Apple. If we are installing or removing any Apple items in a check/install
|
|
cycle, we skip checking/installing Apple updates from an Apple Software
|
|
Update server so we don't stomp on each other"""
|
|
# is this a startosinstall item?
|
|
if item_pl.get('installer_type') == 'startosinstall':
|
|
return True
|
|
# check receipts
|
|
for receipt in item_pl.get('receipts', []):
|
|
if receipt.get('packageid', '').startswith('com.apple.'):
|
|
return True
|
|
# check installs items
|
|
for install_item in item_pl.get('installs', []):
|
|
if install_item.get('CFBundleIdentifier', '').startswith('com.apple.'):
|
|
return True
|
|
# if we get here, no receipts or installs items have Apple
|
|
# identifiers
|
|
return False
|
|
|
|
|
|
def already_processed(itemname, installinfo, sections):
|
|
'''Returns True if itemname has already been added to installinfo in one
|
|
of the given sections'''
|
|
description = {'processed_installs': 'install',
|
|
'processed_uninstalls': 'uninstall',
|
|
'managed_updates': 'update',
|
|
'optional_installs': 'optional install'}
|
|
for section in sections:
|
|
if itemname in installinfo[section]:
|
|
display.display_debug1(
|
|
'%s has already been processed for %s.',
|
|
itemname, description[section])
|
|
return True
|
|
return False
|
|
|
|
|
|
def process_managed_update(manifestitem, cataloglist, installinfo):
|
|
"""Process a managed_updates item to see if it is installed, and if so,
|
|
if it needs an update.
|
|
"""
|
|
manifestitemname = os.path.split(manifestitem)[1]
|
|
display.display_debug1(
|
|
'* Processing manifest item %s for update', manifestitemname)
|
|
|
|
if already_processed(
|
|
manifestitemname, installinfo,
|
|
['managed_updates', 'processed_installs', 'processed_uninstalls']):
|
|
return
|
|
|
|
item_pl = catalogs.get_item_detail(manifestitem, cataloglist)
|
|
if not item_pl:
|
|
display.display_warning(
|
|
'Could not process item %s for update. No pkginfo found in '
|
|
'catalogs: %s ', manifestitem, ', '.join(cataloglist))
|
|
return
|
|
|
|
# we only offer to update if some version of the item is already
|
|
# installed, so let's check
|
|
if installationstate.some_version_installed(item_pl):
|
|
# add to the list of processed managed_updates
|
|
installinfo['managed_updates'].append(manifestitemname)
|
|
dummy_result = process_install(
|
|
manifestitem, cataloglist, installinfo, is_managed_update=True)
|
|
else:
|
|
display.display_debug1(
|
|
'%s does not appear to be installed, so no managed updates...',
|
|
manifestitemname)
|
|
|
|
|
|
def process_optional_install(manifestitem, cataloglist, installinfo):
|
|
"""Process an optional install item to see if it should be added to
|
|
the list of optional installs.
|
|
"""
|
|
manifestitemname = os.path.split(manifestitem)[1]
|
|
display.display_debug1(
|
|
"* Processing manifest item %s for optional install" % manifestitemname)
|
|
|
|
if already_processed(
|
|
manifestitemname, installinfo,
|
|
['optional_installs',
|
|
'processed_installs', 'processed_uninstalls']):
|
|
return
|
|
|
|
# check to see if item (any version) is already in the
|
|
# optional_install list:
|
|
for item in installinfo['optional_installs']:
|
|
if manifestitemname == item['name']:
|
|
display.display_debug1(
|
|
'%s has already been processed for optional install.',
|
|
manifestitemname)
|
|
return
|
|
|
|
item_pl = catalogs.get_item_detail(manifestitem, cataloglist,
|
|
suppress_warnings=True)
|
|
if not item_pl and prefs.pref('ShowOptionalInstallsForHigherOSVersions'):
|
|
# could not find an item valid for the current OS and hardware
|
|
# try again to see if there is an item for a higher OS
|
|
item_pl = catalogs.get_item_detail(
|
|
manifestitem, cataloglist, skip_min_os_check=True,
|
|
suppress_warnings=True)
|
|
if item_pl:
|
|
# found an item that requires a higher OS version
|
|
display.display_debug1(
|
|
'Found %s, version %s that requires a higher os version',
|
|
item_pl['name'], item_pl['version'])
|
|
# insert a note about the OS version requirement
|
|
item_pl['note'] = ('Requires macOS version %s.'
|
|
% item_pl['minimum_os_version'])
|
|
item_pl['update_available'] = True
|
|
if not item_pl:
|
|
# could not find anything that matches and is applicable
|
|
display.display_warning(
|
|
'Could not process item %s for optional install. No pkginfo '
|
|
'found in catalogs: %s ', manifestitem, ', '.join(cataloglist))
|
|
return
|
|
|
|
is_currently_installed = installationstate.some_version_installed(item_pl)
|
|
needs_update = False
|
|
if is_currently_installed:
|
|
if unused_software.should_be_removed(item_pl):
|
|
process_removal(manifestitem, cataloglist, installinfo)
|
|
manifestutils.remove_from_selfserve_installs(manifestitem)
|
|
return
|
|
if not 'installcheck_script' in item_pl:
|
|
# installcheck_scripts can be expensive and only tell us if
|
|
# an item is installed or not. So if iteminfo['installed'] is
|
|
# True, and we're using an installcheck_script,
|
|
# installationstate.installed_state is going to return 1
|
|
# (which does not equal 0), so we can avoid running it again.
|
|
# We should really revisit all of this in the future to avoid
|
|
# repeated checks of the same data.
|
|
# (installcheck_script isn't called if OnDemand is True, but if
|
|
# OnDemand is true, is_currently_installed would be False, and
|
|
# therefore we would not be here!)
|
|
#
|
|
# TL;DR: only check installed_state if no installcheck_script
|
|
needs_update = installationstate.installed_state(item_pl) == 0
|
|
|
|
if (not needs_update and
|
|
prefs.pref('ShowOptionalInstallsForHigherOSVersions')):
|
|
# the version we have installed is the newest for the current OS.
|
|
# check again to see if there is a newer version for a higher OS
|
|
display.display_debug1(
|
|
'Checking for versions of %s that require a higher OS version',
|
|
manifestitem)
|
|
another_item_pl = catalogs.get_item_detail(
|
|
manifestitem, cataloglist, skip_min_os_check=True,
|
|
suppress_warnings=True)
|
|
if another_item_pl != item_pl:
|
|
# we found a different item. Replace the one we found
|
|
# previously with this one.
|
|
item_pl = another_item_pl
|
|
display.display_debug1(
|
|
'Found %s, version %s that requires a higher os version',
|
|
item_pl['name'], item_pl['version'])
|
|
# insert a note about the OS version requirement
|
|
item_pl['note'] = ('Requires macOS version %s.'
|
|
% item_pl['minimum_os_version'])
|
|
item_pl['update_available'] = True
|
|
|
|
# if we get to this point we can add this item
|
|
# to the list of optional installs
|
|
iteminfo = {}
|
|
iteminfo['name'] = item_pl.get('name', manifestitemname)
|
|
iteminfo['description'] = item_pl.get('description', '')
|
|
iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
|
|
iteminfo['display_name'] = item_pl.get('display_name', '')
|
|
for key in ['category', 'developer', 'featured', 'icon_name', 'icon_hash',
|
|
'requires', 'RestartAction']:
|
|
if key in item_pl:
|
|
iteminfo[key] = item_pl[key]
|
|
iteminfo['installed'] = is_currently_installed
|
|
iteminfo['needs_update'] = needs_update
|
|
iteminfo['licensed_seat_info_available'] = item_pl.get(
|
|
'licensed_seat_info_available', False)
|
|
iteminfo['uninstallable'] = (
|
|
item_pl.get('uninstallable', False)
|
|
and (item_pl.get('uninstall_method', '') != ''))
|
|
# If the item is a precache item, record the precache flag
|
|
# and also the installer item location (as long as item doesn't have a note
|
|
# explaining why it's not available and as long as available seats is not 0)
|
|
if (item_pl.get('installer_item_location') and
|
|
item_pl.get('precache') and
|
|
not 'note' in item_pl and
|
|
(not 'licensed_seats_available' in item_pl or
|
|
item_pl['licensed_seats_available'])):
|
|
iteminfo['precache'] = True
|
|
iteminfo['installer_item_location'] = item_pl['installer_item_location']
|
|
for key in ['installer_item_hash', 'PackageCompleteURL', 'PackageURL']:
|
|
if key in item_pl:
|
|
iteminfo[key] = item_pl[key]
|
|
iteminfo['installer_item_size'] = \
|
|
item_pl.get('installer_item_size', 0)
|
|
iteminfo['installed_size'] = item_pl.get(
|
|
'installer_item_size', iteminfo['installer_item_size'])
|
|
if item_pl.get('note'):
|
|
# catalogs.get_item_detail() passed us a note about this item;
|
|
# pass it along
|
|
iteminfo['note'] = item_pl['note']
|
|
elif needs_update or not is_currently_installed:
|
|
if not download.enough_disk_space(
|
|
item_pl, installinfo.get('managed_installs', []), warn=False):
|
|
iteminfo['note'] = (
|
|
'Insufficient disk space to download and install.')
|
|
if needs_update:
|
|
iteminfo['needs_update'] = False
|
|
iteminfo['update_available'] = True
|
|
optional_keys = ['preinstall_alert',
|
|
'preuninstall_alert',
|
|
'preupgrade_alert',
|
|
'OnDemand',
|
|
'minimum_os_version',
|
|
'update_available',
|
|
'localized_strings']
|
|
for key in optional_keys:
|
|
if key in item_pl:
|
|
iteminfo[key] = item_pl[key]
|
|
|
|
display.display_debug1(
|
|
'Adding %s to the optional install list', iteminfo['name'])
|
|
installinfo['optional_installs'].append(iteminfo)
|
|
|
|
|
|
def process_install(manifestitem, cataloglist, installinfo,
|
|
is_managed_update=False,
|
|
is_optional_install=False):
|
|
"""Processes a manifest item for install. Determines if it needs to be
|
|
installed, and if so, if any items it is dependent on need to
|
|
be installed first. Installation detail is added to
|
|
installinfo['managed_installs']
|
|
Calls itself recursively as it processes dependencies.
|
|
Returns a boolean; when processing dependencies, a false return
|
|
will stop the installation of a dependent item
|
|
"""
|
|
|
|
manifestitemname = os.path.split(manifestitem)[1]
|
|
display.display_debug1(
|
|
'* Processing manifest item %s for install', manifestitemname)
|
|
(manifestitemname_withoutversion, includedversion) = (
|
|
catalogs.split_name_and_version(manifestitemname))
|
|
|
|
# have we processed this already?
|
|
if manifestitemname in installinfo['processed_installs']:
|
|
display.display_debug1(
|
|
'%s has already been processed for install.', manifestitemname)
|
|
return True
|
|
elif (manifestitemname_withoutversion in
|
|
installinfo['processed_uninstalls']):
|
|
display.display_warning(
|
|
'Will not process %s for install because it has already '
|
|
'been processed for uninstall!', manifestitemname)
|
|
return False
|
|
|
|
item_pl = catalogs.get_item_detail(manifestitem, cataloglist)
|
|
if not item_pl:
|
|
display.display_warning(
|
|
'Could not process item %s for install. No pkginfo found in '
|
|
'catalogs: %s ', manifestitem, ', '.join(cataloglist))
|
|
return False
|
|
elif is_managed_update:
|
|
# we're processing this as a managed update, so don't
|
|
# add it to the processed_installs list
|
|
pass
|
|
else:
|
|
# we found it, so add it to our list of processed installs
|
|
# so we don't process it again in the future
|
|
display.display_debug2(
|
|
'Adding %s to list of processed installs' % manifestitemname)
|
|
installinfo['processed_installs'].append(manifestitemname)
|
|
|
|
if item_in_installinfo(item_pl, installinfo['managed_installs'],
|
|
vers=item_pl.get('version')):
|
|
# has this item already been added to the list of things to install?
|
|
display.display_debug1(
|
|
'%s is or will be installed.', manifestitemname)
|
|
return True
|
|
|
|
# check dependencies
|
|
dependencies_met = True
|
|
|
|
# there are two kinds of dependencies/relationships.
|
|
#
|
|
# 'requires' are prerequisites:
|
|
# package A requires package B be installed first.
|
|
# if package A is removed, package B is unaffected.
|
|
# requires can be a one to many relationship.
|
|
#
|
|
# The second type of relationship is 'update_for'.
|
|
# This signifies that that current package should be considered an update
|
|
# for the packages listed in the 'update_for' array. When processing a
|
|
# package, we look through the catalogs for other packages that declare
|
|
# they are updates for the current package and install them if needed.
|
|
# This can be a one-to-many relationship - one package can be an update
|
|
# for several other packages; for example, 'PhotoshopCS4update-11.0.1'
|
|
# could be an update for PhotoshopCS4 and for AdobeCS4DesignSuite.
|
|
#
|
|
# When removing an item, any updates for that item are removed as well.
|
|
|
|
if 'requires' in item_pl:
|
|
dependencies = item_pl['requires']
|
|
# fix things if 'requires' was specified as a string
|
|
# instead of an array of strings
|
|
if is_a_string(dependencies):
|
|
dependencies = [dependencies]
|
|
for item in dependencies:
|
|
display.display_detail(
|
|
'%s-%s requires %s. Getting info on %s...'
|
|
% (item_pl.get('name', manifestitemname),
|
|
item_pl.get('version', ''), item, item))
|
|
success = process_install(item, cataloglist, installinfo,
|
|
is_managed_update=is_managed_update)
|
|
if not success:
|
|
dependencies_met = False
|
|
|
|
iteminfo = {}
|
|
iteminfo['name'] = item_pl.get('name', '')
|
|
iteminfo['display_name'] = item_pl.get('display_name', iteminfo['name'])
|
|
iteminfo['description'] = item_pl.get('description', '')
|
|
|
|
if item_pl.get('localized_strings'):
|
|
iteminfo['localized_strings'] = item_pl['localized_strings']
|
|
|
|
if not dependencies_met:
|
|
display.display_warning(
|
|
'Didn\'t attempt to install %s because could not resolve all '
|
|
'dependencies.', manifestitemname)
|
|
# add information to managed_installs so we have some feedback
|
|
# to display in MSC.app
|
|
iteminfo['installed'] = False
|
|
iteminfo['note'] = ('Can\'t install %s because could not resolve all '
|
|
'dependencies.' % iteminfo['display_name'])
|
|
iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
return False
|
|
|
|
installed_state = installationstate.installed_state(item_pl)
|
|
if installed_state == 0:
|
|
display.display_detail('Need to install %s', manifestitemname)
|
|
iteminfo['installer_item_size'] = item_pl.get(
|
|
'installer_item_size', 0)
|
|
iteminfo['installed_size'] = item_pl.get(
|
|
'installed_size', iteminfo['installer_item_size'])
|
|
try:
|
|
# Get a timestamp, then download the installer item.
|
|
start = datetime.datetime.now()
|
|
if item_pl.get('installer_type', 0) == 'nopkg':
|
|
# Packageless install
|
|
download_speed = 0
|
|
filename = 'packageless_install'
|
|
else:
|
|
if download.download_installeritem(item_pl, installinfo):
|
|
# Record the download speed to the InstallResults output.
|
|
end = datetime.datetime.now()
|
|
download_seconds = (end - start).seconds
|
|
try:
|
|
if iteminfo['installer_item_size'] < 1024:
|
|
# ignore downloads under 1 MB or speeds will
|
|
# be skewed.
|
|
download_speed = 0
|
|
else:
|
|
# installer_item_size is KBytes, so divide
|
|
# by seconds.
|
|
download_speed = int(
|
|
iteminfo['installer_item_size'] /
|
|
download_seconds)
|
|
except (TypeError, ValueError, ZeroDivisionError):
|
|
download_speed = 0
|
|
else:
|
|
# Item was already in cache; set download_speed to 0.
|
|
download_speed = 0
|
|
|
|
filename = download.get_url_basename(
|
|
item_pl['installer_item_location'])
|
|
|
|
iteminfo['download_kbytes_per_sec'] = download_speed
|
|
if download_speed:
|
|
display.display_detail(
|
|
'%s downloaded at %d KB/s', filename, download_speed)
|
|
|
|
# required keys
|
|
iteminfo['installer_item'] = filename
|
|
iteminfo['installed'] = False
|
|
iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
|
|
|
|
# we will ignore the unattended_install key if the item needs a
|
|
# restart or logout...
|
|
if (item_pl.get('unattended_install') or
|
|
item_pl.get('forced_install')):
|
|
if item_pl.get('RestartAction', 'None') != 'None':
|
|
display.display_warning(
|
|
'Ignoring unattended_install key for %s because '
|
|
'RestartAction is %s.',
|
|
item_pl['name'], item_pl.get('RestartAction'))
|
|
else:
|
|
iteminfo['unattended_install'] = True
|
|
|
|
# optional keys to copy if they exist
|
|
optional_keys = ['additional_startosinstall_options',
|
|
'allow_untrusted',
|
|
'suppress_bundle_relocation',
|
|
'installer_choices_xml',
|
|
'installer_environment',
|
|
'adobe_install_info',
|
|
'RestartAction',
|
|
'installer_type',
|
|
'adobe_package_name',
|
|
'package_path',
|
|
'blocking_applications',
|
|
'installs',
|
|
'requires',
|
|
'update_for',
|
|
'payloads',
|
|
'preinstall_script',
|
|
'postinstall_script',
|
|
'items_to_copy', # used w/ copy_from_dmg
|
|
'copy_local', # used w/ AdobeCS5 Updaters
|
|
'force_install_after_date',
|
|
'apple_item',
|
|
'category',
|
|
'developer',
|
|
'icon_name',
|
|
'PayloadIdentifier',
|
|
'icon_hash',
|
|
'OnDemand',
|
|
'precache']
|
|
|
|
if (is_optional_install and
|
|
not installationstate.some_version_installed(item_pl)):
|
|
# For optional installs where no version is installed yet
|
|
# we do not enforce force_install_after_date
|
|
optional_keys.remove('force_install_after_date')
|
|
|
|
for key in optional_keys:
|
|
if key in item_pl:
|
|
iteminfo[key] = item_pl[key]
|
|
|
|
if 'apple_item' not in iteminfo:
|
|
# admin did not explicitly mark this item; let's determine if
|
|
# it's from Apple
|
|
if is_apple_item(item_pl):
|
|
munkilog.log(
|
|
'Marking %s as apple_item - this will block '
|
|
'Apple SUS updates' % iteminfo['name'])
|
|
iteminfo['apple_item'] = True
|
|
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
|
|
update_list = []
|
|
# (manifestitemname_withoutversion, includedversion) =
|
|
# nameAndVersion(manifestitemname)
|
|
if includedversion:
|
|
# a specific version was specified in the manifest
|
|
# so look only for updates for this specific version
|
|
update_list = catalogs.look_for_updates_for_version(
|
|
manifestitemname_withoutversion,
|
|
includedversion, cataloglist)
|
|
else:
|
|
# didn't specify a specific version, so
|
|
# now look for all updates for this item
|
|
update_list = catalogs.look_for_updates(
|
|
manifestitemname_withoutversion, cataloglist)
|
|
# now append any updates specifically
|
|
# for the version to be installed
|
|
update_list.extend(
|
|
catalogs.look_for_updates_for_version(
|
|
manifestitemname_withoutversion,
|
|
iteminfo['version_to_install'],
|
|
cataloglist))
|
|
|
|
for update_item in update_list:
|
|
# call processInstall recursively so we get the
|
|
# latest version and dependencies
|
|
dummy_result = process_install(
|
|
update_item, cataloglist, installinfo,
|
|
is_managed_update=is_managed_update)
|
|
return True
|
|
except fetch.PackageVerificationError:
|
|
display.display_warning(
|
|
'Can\'t install %s because the integrity check failed.',
|
|
manifestitem)
|
|
iteminfo['installed'] = False
|
|
iteminfo['note'] = 'Integrity check failed'
|
|
iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
|
|
for key in ['developer', 'icon_name']:
|
|
if key in item_pl:
|
|
iteminfo[key] = item_pl[key]
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
#if manifestitemname in installinfo['processed_installs']:
|
|
# installinfo['processed_installs'].remove(manifestitemname)
|
|
return False
|
|
except (fetch.GurlError, fetch.GurlDownloadError) as errmsg:
|
|
display.display_warning(
|
|
'Download of %s failed: %s', manifestitem, errmsg)
|
|
iteminfo['installed'] = False
|
|
iteminfo['note'] = u'Download failed (%s)' % errmsg
|
|
iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
|
|
for key in ['developer', 'icon_name']:
|
|
if key in item_pl:
|
|
iteminfo[key] = item_pl[key]
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
#if manifestitemname in installinfo['processed_installs']:
|
|
# installinfo['processed_installs'].remove(manifestitemname)
|
|
return False
|
|
except fetch.Error as errmsg:
|
|
display.display_warning(
|
|
'Can\'t install %s because: %s', manifestitemname, errmsg)
|
|
iteminfo['installed'] = False
|
|
iteminfo['note'] = '%s' % errmsg
|
|
iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
|
|
for key in ['developer', 'icon_name']:
|
|
if key in item_pl:
|
|
iteminfo[key] = item_pl[key]
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
#if manifestitemname in installinfo['processed_installs']:
|
|
# installinfo['processed_installs'].remove(manifestitemname)
|
|
return False
|
|
else:
|
|
iteminfo['installed'] = True
|
|
# record installed size for reporting
|
|
iteminfo['installed_size'] = item_pl.get(
|
|
'installed_size', item_pl.get('installer_item_size', 0))
|
|
if installed_state == 1:
|
|
# just use the version from the pkginfo
|
|
iteminfo['installed_version'] = item_pl['version']
|
|
else:
|
|
# might be newer; attempt to figure out the version
|
|
installed_version = compare.get_installed_version(item_pl)
|
|
if installed_version == "UNKNOWN":
|
|
installed_version = '(newer than %s)' % item_pl['version']
|
|
iteminfo['installed_version'] = installed_version
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
# remove included version number if any
|
|
(name, includedversion) = catalogs.split_name_and_version(
|
|
manifestitemname)
|
|
display.display_detail('%s version %s (or newer) is already installed.',
|
|
name, item_pl['version'])
|
|
update_list = []
|
|
if not includedversion:
|
|
# no specific version is specified;
|
|
# the item is already installed;
|
|
# now look for updates for this item
|
|
update_list = catalogs.look_for_updates(name, cataloglist)
|
|
# and also any for this specific version
|
|
installed_version = iteminfo['installed_version']
|
|
if not installed_version.startswith('(newer than '):
|
|
update_list.extend(
|
|
catalogs.look_for_updates_for_version(
|
|
name, installed_version, cataloglist))
|
|
elif compare.compare_versions(
|
|
includedversion, iteminfo['installed_version']) == 1:
|
|
# manifest specifies a specific version
|
|
# if that's what's installed, look for any updates
|
|
# specific to this version
|
|
update_list = catalogs.look_for_updates_for_version(
|
|
manifestitemname_withoutversion, includedversion, cataloglist)
|
|
# if we have any updates, process them
|
|
for update_item in update_list:
|
|
# call processInstall recursively so we get updates
|
|
# and any dependencies
|
|
dummy_result = process_install(
|
|
update_item, cataloglist, installinfo,
|
|
is_managed_update=is_managed_update)
|
|
|
|
return True
|
|
|
|
|
|
def process_manifest_for_key(manifest, manifest_key, installinfo,
|
|
parentcatalogs=None):
|
|
"""Processes keys in manifests to build the lists of items to install and
|
|
remove.
|
|
|
|
Can be recursive if manifests include other manifests.
|
|
Probably doesn't handle circular manifest references well.
|
|
|
|
manifest can be a path to a manifest file or a dictionary object.
|
|
"""
|
|
if is_a_string(manifest):
|
|
display.display_debug1(
|
|
"** Processing manifest %s for %s",
|
|
os.path.basename(manifest), manifest_key)
|
|
manifestdata = manifestutils.get_manifest_data(manifest)
|
|
else:
|
|
manifestdata = manifest
|
|
manifest = 'embedded manifest'
|
|
|
|
cataloglist = manifestdata.get('catalogs')
|
|
if cataloglist:
|
|
catalogs.get_catalogs(cataloglist)
|
|
elif parentcatalogs:
|
|
cataloglist = parentcatalogs
|
|
|
|
if not cataloglist:
|
|
display.display_warning('Manifest %s has no catalogs', manifest)
|
|
return
|
|
|
|
for item in manifestdata.get('included_manifests', []):
|
|
if item: # only process if item is not empty
|
|
nestedmanifestpath = manifestutils.get_manifest(item)
|
|
if not nestedmanifestpath:
|
|
raise manifestutils.ManifestException
|
|
if processes.stop_requested():
|
|
return
|
|
process_manifest_for_key(nestedmanifestpath, manifest_key,
|
|
installinfo, cataloglist)
|
|
|
|
conditionalitems = manifestdata.get('conditional_items', [])
|
|
if conditionalitems:
|
|
display.display_debug1(
|
|
'** Processing conditional_items in %s', manifest)
|
|
# conditionalitems should be an array of dicts
|
|
# each dict has a predicate; the rest consists of the
|
|
# same keys as a manifest
|
|
for item in conditionalitems:
|
|
try:
|
|
predicate = item['condition']
|
|
except (AttributeError, KeyError):
|
|
display.display_warning(
|
|
'Missing predicate for conditional_item %s', item)
|
|
continue
|
|
except BaseException:
|
|
display.display_warning(
|
|
'Conditional item is malformed: %s', item)
|
|
continue
|
|
if info.predicate_evaluates_as_true(
|
|
predicate, additional_info={'catalogs': cataloglist}):
|
|
conditionalmanifest = item
|
|
process_manifest_for_key(
|
|
conditionalmanifest, manifest_key, installinfo, cataloglist)
|
|
|
|
for item in manifestdata.get(manifest_key, []):
|
|
if processes.stop_requested():
|
|
return
|
|
if manifest_key == 'managed_installs':
|
|
dummy_result = process_install(item, cataloglist, installinfo)
|
|
elif manifest_key == 'managed_updates':
|
|
process_managed_update(item, cataloglist, installinfo)
|
|
elif manifest_key == 'optional_installs':
|
|
process_optional_install(item, cataloglist, installinfo)
|
|
elif manifest_key == 'managed_uninstalls':
|
|
dummy_result = process_removal(item, cataloglist, installinfo)
|
|
elif manifest_key == 'featured_items':
|
|
installinfo['featured_items'].append(item)
|
|
|
|
|
|
def process_removal(manifestitem, cataloglist, installinfo):
|
|
"""Processes a manifest item; attempts to determine if it
|
|
needs to be removed, and if it can be removed.
|
|
|
|
Unlike installs, removals aren't really version-specific -
|
|
If we can figure out how to remove the currently installed
|
|
version, we do, unless the admin specifies a specific version
|
|
number in the manifest. In that case, we only attempt a
|
|
removal if the version installed matches the specific version
|
|
in the manifest.
|
|
|
|
Any items dependent on the given item need to be removed first.
|
|
Items to be removed are added to installinfo['removals'].
|
|
|
|
Calls itself recursively as it processes dependencies.
|
|
Returns a boolean; when processing dependencies, a false return
|
|
will stop the removal of a dependent item.
|
|
"""
|
|
def get_receipts_to_remove(item):
|
|
"""Returns a list of receipts to remove for item"""
|
|
name = item['name']
|
|
pkgdata = catalogs.analyze_installed_pkgs()
|
|
if name in pkgdata['receipts_for_name']:
|
|
return pkgdata['receipts_for_name'][name]
|
|
return []
|
|
|
|
manifestitemname_withversion = os.path.split(manifestitem)[1]
|
|
display.display_debug1(
|
|
'* Processing manifest item %s for removal' %
|
|
manifestitemname_withversion)
|
|
|
|
(manifestitemname, includedversion) = catalogs.split_name_and_version(
|
|
manifestitemname_withversion)
|
|
|
|
# have we processed this already?
|
|
if manifestitemname in [catalogs.split_name_and_version(item)[0]
|
|
for item in installinfo['processed_installs']]:
|
|
display.display_warning(
|
|
'Will not attempt to remove %s because some version of it is in '
|
|
'the list of managed installs, or it is required by another'
|
|
' managed install.', manifestitemname)
|
|
return False
|
|
elif manifestitemname in installinfo['processed_uninstalls']:
|
|
display.display_debug1(
|
|
'%s has already been processed for removal.', manifestitemname)
|
|
return True
|
|
else:
|
|
installinfo['processed_uninstalls'].append(manifestitemname)
|
|
|
|
infoitems = []
|
|
if includedversion:
|
|
# a specific version was specified
|
|
item_pl = catalogs.get_item_detail(
|
|
manifestitemname, cataloglist, includedversion)
|
|
if item_pl:
|
|
infoitems.append(item_pl)
|
|
else:
|
|
# get all items matching the name provided
|
|
infoitems = catalogs.get_all_items_with_name(
|
|
manifestitemname, cataloglist)
|
|
|
|
if not infoitems:
|
|
display.display_warning(
|
|
'Could not process item %s for removal. No pkginfo found in '
|
|
'catalogs: %s ', manifestitemname, ', '.join(cataloglist))
|
|
return False
|
|
|
|
install_evidence = False
|
|
for item in infoitems:
|
|
display.display_debug2('Considering item %s-%s for removal info',
|
|
item['name'], item['version'])
|
|
if installationstate.evidence_this_is_installed(item):
|
|
install_evidence = True
|
|
break
|
|
else:
|
|
display.display_debug2(
|
|
'%s-%s not installed.', item['name'], item['version'])
|
|
|
|
if not install_evidence:
|
|
display.display_detail(
|
|
'%s doesn\'t appear to be installed.', manifestitemname_withversion)
|
|
iteminfo = {}
|
|
iteminfo['name'] = manifestitemname
|
|
iteminfo['installed'] = False
|
|
installinfo['removals'].append(iteminfo)
|
|
return True
|
|
|
|
# if we get here, install_evidence is true, and item
|
|
# holds the item we found install evidence for, so we
|
|
# should use that item to do the removal
|
|
uninstall_item = None
|
|
packages_to_remove = []
|
|
# check for uninstall info
|
|
# and grab the first uninstall method we find.
|
|
if item.get('uninstallable') and 'uninstall_method' in item:
|
|
uninstallmethod = item['uninstall_method']
|
|
if uninstallmethod == 'removepackages':
|
|
packages_to_remove = get_receipts_to_remove(item)
|
|
if packages_to_remove:
|
|
uninstall_item = item
|
|
elif uninstallmethod.startswith('Adobe'):
|
|
# Adobe CS3/CS4/CS5/CS6/CC product
|
|
uninstall_item = item
|
|
elif uninstallmethod in ['remove_copied_items',
|
|
'remove_app',
|
|
'uninstall_script',
|
|
'remove_profile']:
|
|
uninstall_item = item
|
|
else:
|
|
# uninstall_method is a local script.
|
|
# Check to see if it exists and is executable
|
|
if os.path.exists(uninstallmethod) and \
|
|
os.access(uninstallmethod, os.X_OK):
|
|
uninstall_item = item
|
|
|
|
if not uninstall_item:
|
|
# the uninstall info for the item couldn't be matched
|
|
# to what's on disk
|
|
display.display_warning('Could not find uninstall info for %s.',
|
|
manifestitemname_withversion)
|
|
return False
|
|
|
|
# if we got this far, we have enough info to attempt an uninstall.
|
|
# the pkginfo is in uninstall_item
|
|
# Now check for dependent items
|
|
#
|
|
# First, look through catalogs for items that are required by this item;
|
|
# if any are installed, we need to remove them as well
|
|
#
|
|
# still not sure how to handle references to specific versions --
|
|
# if another package says it requires SomePackage--1.0.0.0.0
|
|
# and we're supposed to remove SomePackage--1.0.1.0.0... what do we do?
|
|
#
|
|
dependentitemsremoved = True
|
|
|
|
uninstall_item_name = uninstall_item.get('name')
|
|
uninstall_name_w_version = (
|
|
'%s-%s' % (uninstall_item.get('name'), uninstall_item.get('version')))
|
|
alt_uninstall_name_w_version = (
|
|
'%s--%s' % (uninstall_item.get('name'), uninstall_item.get('version')))
|
|
processednames = []
|
|
for catalogname in cataloglist:
|
|
if not catalogname in catalogs.catalogs():
|
|
# in case the list refers to a non-existent catalog
|
|
continue
|
|
for item_pl in catalogs.catalogs()[catalogname]['items']:
|
|
name = item_pl.get('name')
|
|
if name not in processednames:
|
|
if 'requires' in item_pl:
|
|
if (uninstall_item_name in item_pl['requires'] or
|
|
uninstall_name_w_version
|
|
in item_pl['requires'] or
|
|
alt_uninstall_name_w_version
|
|
in item_pl['requires']):
|
|
display.display_debug1(
|
|
'%s requires %s, checking to see if it\'s '
|
|
'installed...', item_pl.get('name'),
|
|
manifestitemname)
|
|
if installationstate.evidence_this_is_installed(
|
|
item_pl):
|
|
display.display_detail(
|
|
'%s requires %s. %s must be removed as well.',
|
|
item_pl.get('name'), manifestitemname,
|
|
item_pl.get('name'))
|
|
success = process_removal(
|
|
item_pl.get('name'), cataloglist, installinfo)
|
|
if not success:
|
|
dependentitemsremoved = False
|
|
break
|
|
# record this name so we don't process it again
|
|
processednames.append(name)
|
|
|
|
if not dependentitemsremoved:
|
|
display.display_warning('Will not attempt to remove %s because could '
|
|
'not remove all items dependent on it.',
|
|
manifestitemname_withversion)
|
|
return False
|
|
|
|
# Finally! We can record the removal information!
|
|
iteminfo = {}
|
|
iteminfo['name'] = uninstall_item.get('name', '')
|
|
iteminfo['display_name'] = uninstall_item.get('display_name', '')
|
|
iteminfo['description'] = 'Will be removed.'
|
|
|
|
# we will ignore the unattended_uninstall key if the item needs a restart
|
|
# or logout...
|
|
if (uninstall_item.get('unattended_uninstall') or
|
|
uninstall_item.get('forced_uninstall')):
|
|
if uninstall_item.get('RestartAction', 'None') != 'None':
|
|
display.display_warning(
|
|
'Ignoring unattended_uninstall key for %s '
|
|
'because RestartAction is %s.',
|
|
uninstall_item['name'],
|
|
uninstall_item.get('RestartAction'))
|
|
else:
|
|
iteminfo['unattended_uninstall'] = True
|
|
|
|
# some keys we'll copy if they exist
|
|
optional_keys = ['blocking_applications',
|
|
'installs',
|
|
'requires',
|
|
'update_for',
|
|
'payloads',
|
|
'preuninstall_script',
|
|
'postuninstall_script',
|
|
'apple_item',
|
|
'category',
|
|
'developer',
|
|
'icon_name',
|
|
'PayloadIdentifier']
|
|
for key in optional_keys:
|
|
if key in uninstall_item:
|
|
iteminfo[key] = uninstall_item[key]
|
|
|
|
if 'apple_item' not in iteminfo:
|
|
# admin did not explicitly mark this item; let's determine if
|
|
# it's from Apple
|
|
if is_apple_item(item_pl):
|
|
iteminfo['apple_item'] = True
|
|
|
|
if packages_to_remove:
|
|
# remove references for each package
|
|
packages_to_really_remove = []
|
|
for pkg in packages_to_remove:
|
|
display.display_debug1('Considering %s for removal...', pkg)
|
|
# find pkg in pkgdata['pkg_references'] and remove the reference
|
|
# so we only remove packages if we're the last reference to it
|
|
pkgdata = catalogs.analyze_installed_pkgs()
|
|
if pkg in pkgdata['pkg_references']:
|
|
display.display_debug1('%s references are: %s', pkg,
|
|
pkgdata['pkg_references'][pkg])
|
|
if iteminfo['name'] in pkgdata['pkg_references'][pkg]:
|
|
pkgdata['pkg_references'][pkg].remove(iteminfo['name'])
|
|
if not pkgdata['pkg_references'][pkg]:
|
|
# no other items reference this pkg
|
|
display.display_debug1(
|
|
'Adding %s to removal list.', pkg)
|
|
packages_to_really_remove.append(pkg)
|
|
else:
|
|
# This shouldn't happen
|
|
display.display_warning('pkg id %s missing from pkgdata', pkg)
|
|
if packages_to_really_remove:
|
|
iteminfo['packages'] = packages_to_really_remove
|
|
else:
|
|
# no packages that belong to this item only.
|
|
display.display_warning('could not find unique packages to remove '
|
|
'for %s', iteminfo['name'])
|
|
return False
|
|
|
|
iteminfo['uninstall_method'] = uninstallmethod
|
|
if uninstallmethod.startswith('Adobe'):
|
|
if (uninstallmethod == "AdobeCS5AAMEEPackage" and
|
|
'adobe_install_info' in item):
|
|
iteminfo['adobe_install_info'] = item['adobe_install_info']
|
|
else:
|
|
if 'uninstaller_item_location' in item:
|
|
location = uninstall_item['uninstaller_item_location']
|
|
else:
|
|
location = uninstall_item['installer_item_location']
|
|
try:
|
|
download.download_installeritem(
|
|
item, installinfo, uninstalling=True)
|
|
filename = os.path.split(location)[1]
|
|
iteminfo['uninstaller_item'] = filename
|
|
iteminfo['adobe_package_name'] = uninstall_item.get(
|
|
'adobe_package_name', '')
|
|
except fetch.PackageVerificationError:
|
|
display.display_warning(
|
|
'Can\'t uninstall %s because the integrity check '
|
|
'failed.', iteminfo['name'])
|
|
return False
|
|
except fetch.Error as errmsg:
|
|
display.display_warning(
|
|
'Failed to download the uninstaller for %s because %s',
|
|
iteminfo['name'], errmsg)
|
|
return False
|
|
elif uninstallmethod == 'remove_copied_items':
|
|
iteminfo['items_to_remove'] = item.get('items_to_copy', [])
|
|
elif uninstallmethod == 'remove_app':
|
|
if uninstall_item.get('installs', None):
|
|
iteminfo['remove_app_info'] = uninstall_item['installs'][0]
|
|
elif uninstallmethod == 'uninstall_script':
|
|
iteminfo['uninstall_script'] = item.get('uninstall_script', '')
|
|
|
|
# before we add this removal to the list,
|
|
# check for installed updates and add them to the
|
|
# removal list as well:
|
|
update_list = catalogs.look_for_updates(uninstall_item_name, cataloglist)
|
|
update_list.extend(catalogs.look_for_updates(
|
|
uninstall_name_w_version, cataloglist))
|
|
update_list.extend(catalogs.look_for_updates(
|
|
alt_uninstall_name_w_version, cataloglist))
|
|
for update_item in update_list:
|
|
# call us recursively...
|
|
dummy_result = process_removal(update_item, cataloglist, installinfo)
|
|
|
|
# finish recording info for this removal
|
|
iteminfo['installed'] = True
|
|
iteminfo['installed_version'] = uninstall_item.get('version')
|
|
if 'RestartAction' in uninstall_item:
|
|
iteminfo['RestartAction'] = uninstall_item['RestartAction']
|
|
installinfo['removals'].append(iteminfo)
|
|
display.display_detail(
|
|
'Removal of %s added to ManagedInstaller tasks.',
|
|
manifestitemname_withversion)
|
|
return True
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print('This is a library of support tools for the Munki Suite.')
|