Files
munki/code/client/munkilib/updatecheck/analyze.py
2020-01-01 08:53:37 -08:00

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