diff --git a/code/apps/Managed Software Center/Managed Software Center/MunkiItems.py b/code/apps/Managed Software Center/Managed Software Center/MunkiItems.py
index 29e22b8b..9dbe9e2b 100644
--- a/code/apps/Managed Software Center/Managed Software Center/MunkiItems.py
+++ b/code/apps/Managed Software Center/Managed Software Center/MunkiItems.py
@@ -465,6 +465,52 @@ def unmanage(item):
user_removal_selections.discard(item['name'])
+def getLocalizedShortNoteForItem(item, is_update=False):
+ '''Attempt to localize a note. Currently handle only two types.'''
+ note = item.get('note')
+ if is_update:
+ return NSLocalizedString(u"Update available",
+ u"Update available display text")
+ if note.startswith('Insufficient disk space to download and install'):
+ return NSLocalizedString(u"Not enough disk space",
+ u"Not Enough Disk Space display text")
+ if note.startswith('Requires macOS version '):
+ return NSLocalizedString(u"macOS update required",
+ u"macOS update required text")
+ # we don't know how to localize this note, return None
+ return None
+
+
+def getLocalizedLongNoteForItem(item, is_update=False):
+ '''Attempt to localize a note. Currently handle only two types.'''
+ note = item.get('note')
+ if note.startswith('Insufficient disk space to download and install'):
+ if is_update:
+ return NSLocalizedString(
+ u"An older version is currently installed. There is not enough "
+ "disk space to download and install this update.",
+ u"Long Not Enough Disk Space For Update display text")
+ else:
+ return NSLocalizedString(
+ u"There is not enough disk space to download and install this "
+ "item.",
+ u"Long Not Enough Disk Space display text")
+ if note.startswith('Requires macOS version '):
+ if is_update:
+ base_string = NSLocalizedString(
+ u"An older version is currently installed. You must upgrade to "
+ "macOS version %s or higher to be able to install this update.",
+ u"Long update requires a higher OS version text")
+ else:
+ base_string = NSLocalizedString(
+ u"You must upgrade to macOS version %s to be able to "
+ "install this item.",
+ u"Long item requires a higher OS version text")
+ os_version = item.get('minimum_os_version', 'UNKNOWN')
+ return base_string % os_version
+ # we don't know how to localize this note, return None
+ return None
+
class GenericItem(dict):
'''Base class for our types of Munki items'''
@@ -591,24 +637,16 @@ class GenericItem(dict):
# use the Generic package icon
return 'static/Generic.png'
- def unavailable_reason_text(self):
+ def unavailable_reason_text(self, is_update=False):
'''There are several reasons an item might be unavailable for install.
Return the relevent reason'''
if ('licensed_seats_available' in self
and not self['licensed_seats_available']):
return NSLocalizedString(u"No licenses available",
u"No Licenses Available display text")
- if (self.get('note') ==
- 'Insufficient disk space to download and install.'):
- return NSLocalizedString(u"Not enough disk space",
- u"Not Enough Disk Space display text")
- if self.get('note', '').startswith('Requires macOS version '):
- base_string = NSLocalizedString(
- u"Requires macOS version %s",
- u"Item requires a higher OS version text")
- # this is a bit of a cheat; we should probably store the
- # minimum_os_version with the install info
- return base_string % self['note'].split()[-1]
+ localizedNote = getLocalizedShortNoteForItem(self, is_update=is_update)
+ if localizedNote:
+ return '' + localizedNote + ''
# return generic reason
return NSLocalizedString(u"Not currently available",
u"Not Currently Available display text")
@@ -617,6 +655,9 @@ class GenericItem(dict):
'''Return localized status display text'''
if self['status'] == 'unavailable':
return self.unavailable_reason_text()
+ if (self['status'] in ['installed', 'installed-not-removable'] and
+ self.get('note')):
+ return self.unavailable_reason_text(is_update=True)
text_map = {
'install-error':
NSLocalizedString(u"Installation Error",
@@ -1008,9 +1049,12 @@ class OptionalItem(GenericItem):
start_text += ('%s
'
% filtered_html(warning_text))
if self.get('note'):
- # some other note. Probably won't be localized, but we can try
- warning_text = NSBundle.mainBundle().localizedStringForKey_value_table_(
- self['note'], self['note'], None)
+ is_update = self['status'] in ['installed', 'installed-not-removable']
+ warning_text = getLocalizedLongNoteForItem(self, is_update=is_update)
+ if not warning_text:
+ # some other note. Probably won't be localized, but we can try
+ warning_text = NSBundle.mainBundle().localizedStringForKey_value_table_(
+ self['note'], self['note'], None)
start_text += ('%s
'
% filtered_html(warning_text))
if self.get('dependent_items'):
diff --git a/code/apps/Managed Software Center/Managed Software Center/WebResources/base.css b/code/apps/Managed Software Center/Managed Software Center/WebResources/base.css
index 564e8fbb..56cffe27 100644
--- a/code/apps/Managed Software Center/Managed Software Center/WebResources/base.css
+++ b/code/apps/Managed Software Center/Managed Software Center/WebResources/base.css
@@ -1243,7 +1243,8 @@ span.removal-error,
span.will-be-installed,
span.will-be-removed,
span.update-will-be-installed,
-span.update-available {
+span.update-available,
+span.warning {
visibility: visible !important;
color: #CC0000 !important;
}
diff --git a/code/apps/Managed Software Center/Managed Software Center/mschtml.py b/code/apps/Managed Software Center/Managed Software Center/mschtml.py
index 99dd0bf5..1739ecb6 100644
--- a/code/apps/Managed Software Center/Managed Software Center/mschtml.py
+++ b/code/apps/Managed Software Center/Managed Software Center/mschtml.py
@@ -534,10 +534,22 @@ def build_updates_page():
item_list = MunkiItems.getEffectiveUpdateList()
+ # find any optional installs with update available
other_updates = [
item for item in MunkiItems.getOptionalInstallItems()
if item['status'] == 'update-available']
+ # find any listed optional install updates that require a higher OS
+ # or have insufficent disk space or other blockers (because they have a
+ # note)
+ higher_os_updates = [
+ item for item in MunkiItems.getOptionalInstallItems()
+ if item['status'] == 'installed' and item.get('note')]
+ for item in higher_os_updates:
+ item['hide_cancel_button'] = u'hidden'
+
+ other_updates.extend(higher_os_updates)
+
page = {}
page['update_rows'] = u''
page['hide_progress_spinner'] = u'hidden'
diff --git a/code/client/munkilib/updatecheck/analyze.py b/code/client/munkilib/updatecheck/analyze.py
index 9aa8fef0..556e24b9 100644
--- a/code/client/munkilib/updatecheck/analyze.py
+++ b/code/client/munkilib/updatecheck/analyze.py
@@ -157,19 +157,65 @@ def process_optional_install(manifestitem, cataloglist, installinfo):
manifestitemname)
return
- item_pl = catalogs.get_item_detail(manifestitem, cataloglist,
- is_optional_item=True)
+ item_pl = catalogs.get_item_detail(manifestitem, cataloglist)
if not item_pl:
- display.display_warning(
- 'Could not process item %s for optional install. No pkginfo found '
- 'in catalogs: %s ', manifestitem, ', '.join(cataloglist))
- return
+ # 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)
+ 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
+ else:
+ # could not find anything!
+ 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)
- if is_currently_installed and unused_software.should_be_removed(item_pl):
- process_removal(manifestitem, cataloglist, installinfo)
- installer.remove_from_selfserve_installs(manifestitem)
- return
+ needs_update = False
+ if is_currently_installed:
+ if unused_software.should_be_removed(item_pl):
+ process_removal(manifestitem, cataloglist, installinfo)
+ installer.remove_from_selfserve_installs(manifestitem)
+ return
+ if not item_pl.get('OnDemand') and 'installcheck_script' not 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.
+ needs_update = False
+ else:
+ needs_update = installationstate.installed_state(item_pl) == 0
+ if not needs_update:
+ # 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)
+ 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
@@ -183,19 +229,7 @@ def process_optional_install(manifestitem, cataloglist, installinfo):
if key in item_pl:
iteminfo[key] = item_pl[key]
iteminfo['installed'] = is_currently_installed
- if iteminfo['installed']:
- if not item_pl.get('OnDemand') and '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.
- iteminfo['needs_update'] = False
- else:
- iteminfo['needs_update'] = (
- installationstate.installed_state(item_pl) == 0)
+ iteminfo['needs_update'] = needs_update
iteminfo['licensed_seat_info_available'] = item_pl.get(
'licensed_seat_info_available', False)
iteminfo['uninstallable'] = (
@@ -209,15 +243,20 @@ def process_optional_install(manifestitem, cataloglist, installinfo):
# catalogs.get_item_detail() passed us a note about this item;
# pass it along
iteminfo['note'] = item_pl['note']
- elif (not iteminfo['installed']) or (iteminfo.get('needs_update')):
+ 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']
+ 'OnDemand',
+ 'minimum_os_version',
+ 'update_available']
for key in optional_keys:
if key in item_pl:
iteminfo[key] = item_pl[key]
diff --git a/code/client/munkilib/updatecheck/catalogs.py b/code/client/munkilib/updatecheck/catalogs.py
index ee1b0493..942ea1aa 100644
--- a/code/client/munkilib/updatecheck/catalogs.py
+++ b/code/client/munkilib/updatecheck/catalogs.py
@@ -414,9 +414,10 @@ def analyze_installed_pkgs():
return pkgdata
-def get_item_detail(name, cataloglist, vers='', is_optional_item=False):
+def get_item_detail(name, cataloglist, vers='', skip_min_os_check=False):
"""Searches the catalogs in list for an item matching the given name that
- can be installed on the current hardware/OS
+ can be installed on the current hardware/OS (optionally skipping the
+ minimum OS check so we can return an item that requires a higher OS)
If no version is supplied, but the version is appended to the name
('TextWrangler--2.3.0.0.0') that version is used.
@@ -580,11 +581,12 @@ def get_item_detail(name, cataloglist, vers='', is_optional_item=False):
(len(indexlist), name, catalogname))
for index in indexlist:
# iterate through list of items with matching name, highest
- # version first, looking for one that passes all the conditional
- # tests (if any)
+ # version first, looking for first one that passes all the
+ # conditional tests (if any)
item = _CATALOG[catalogname]['items'][index]
if (munki_version_ok(item) and
- os_version_ok(item) and
+ os_version_ok(item,
+ skip_min_os_check=skip_min_os_check) and
cpu_arch_ok(item) and
installable_condition_ok(item)):
display.display_debug1(
@@ -592,29 +594,6 @@ def get_item_detail(name, cataloglist, vers='', is_optional_item=False):
item['name'], item['version'], catalogname)
return item
- # if we're here, we didn't find an item that matches our machine
- # restraints. if we are checking for optional installs, check again,
- # this time returning an item that requires a higher os version
- # that we can use to incentivize people to update their os
- if is_optional_item:
- for index in indexlist:
- # iterate through list of items with matching name, highest
- # version first, looking for one that passes all the
- # conditional tests (if any)
- item = _CATALOG[catalogname]['items'][index]
- if (munki_version_ok(item) and
- os_version_ok(item, skip_min_os_check=True) and
- cpu_arch_ok(item) and
- installable_condition_ok(item)):
- display.display_debug1(
- 'Found %s, version %s in catalog %s that requires '
- 'a higher os version',
- item['name'], item['version'], catalogname)
- # insert a note
- item['note'] = ('Requires macOS version %s.'
- % item['minimum_os_version'])
- return item
-
# if we got this far, we didn't find it.
display.display_debug1('Not found')
for reason in rejected_items:
diff --git a/code/client/munkilib/updatecheck/core.py b/code/client/munkilib/updatecheck/core.py
index 330cfea9..5debfcc4 100644
--- a/code/client/munkilib/updatecheck/core.py
+++ b/code/client/munkilib/updatecheck/core.py
@@ -237,11 +237,13 @@ def check(client_id='', localmanifestpath=None):
# build list of items in the optional_installs list
# that have not exceeded available seats
+ # and don't have notes (indicating why they can't be installed)
available_optional_installs = [
item['name']
for item in installinfo.get('optional_installs', [])
- if (not 'licensed_seats_available' in item
- or item['licensed_seats_available'])]
+ if (not 'note' in item and
+ (not 'licensed_seats_available' in item or
+ item['licensed_seats_available']))]
if selfserveinstalls:
# filter the list, removing any items not in the current list
# of available self-serve installs