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