More changes to support showing optional item updates that require user action to install -- like updating macOS version or making disk space available.

This commit is contained in:
Greg Neagle
2017-06-29 15:08:15 -07:00
parent eef37a2734
commit ad9c08856d
6 changed files with 148 additions and 71 deletions
@@ -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 '<span class="warning">' + localizedNote + '</span>'
# 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 += ('<span class="warning">%s</span><br/><br/>'
% 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 += ('<span class="warning">%s</span><br/><br/>'
% filtered_html(warning_text))
if self.get('dependent_items'):
@@ -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;
}
@@ -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'
+64 -25
View File
@@ -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]
+7 -28
View File
@@ -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:
+4 -2
View File
@@ -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