diff --git a/code/Managed Software Update/MSUAppDelegate.py b/code/Managed Software Update/MSUAppDelegate.py index 986ab40f..8718336a 100644 --- a/code/Managed Software Update/MSUAppDelegate.py +++ b/code/Managed Software Update/MSUAppDelegate.py @@ -484,6 +484,11 @@ class MSUAppDelegate(NSObject): status = "Not installed" if item.get("will_be_installed"): status = NSLocalizedString(u"Will be installed", None) + elif 'licensed_seats_available' in item: + if not item['licensed_seats_available']: + status = NSLocalizedString( + u"No available license seats", None) + row['enabled'] = objc.NO elif item.get("note"): # some reason we can't install status = item.get("note") diff --git a/code/client/munkilib/fetch.py b/code/client/munkilib/fetch.py index bf0d4255..28baf31d 100644 --- a/code/client/munkilib/fetch.py +++ b/code/client/munkilib/fetch.py @@ -366,11 +366,11 @@ def curl(url, destinationpath, else: temp_download_exists = os.path.isfile(tempdownloadpath) http_result = header.get('http_result_code') - if http_result.startswith('2') and \ - temp_download_exists: + if http_result.startswith('2') and temp_download_exists: downloadedsize = os.path.getsize(tempdownloadpath) if downloadedsize >= targetsize: - if not downloadedpercent == 100: + if targetsize and not downloadedpercent == 100: + # need to display a percent done of 100% munkicommon.display_percent_done(100, 100) os.rename(tempdownloadpath, destinationpath) if (resume and not header.get('etag') @@ -439,8 +439,8 @@ def getResourceIfChangedAtomically(url, if xattr_hash == expected_hash: #File is already current, no change. return False - elif munkicommon.pref('PackageVerificationMode').lower() in \ - ['hash_strict', 'hash']: + elif munkicommon.pref( + 'PackageVerificationMode').lower() in ['hash_strict', 'hash']: try: os.unlink(destinationpath) except OSError: diff --git a/code/client/munkilib/updatecheck.py b/code/client/munkilib/updatecheck.py index 725c51e2..fcf282d7 100755 --- a/code/client/munkilib/updatecheck.py +++ b/code/client/munkilib/updatecheck.py @@ -28,6 +28,7 @@ import subprocess import socket import urllib2 import urlparse +from urllib import quote_plus from OpenSSL.crypto import load_certificate, FILETYPE_PEM # our libs @@ -1589,6 +1590,74 @@ def processOptionalInstall(manifestitem, cataloglist, installinfo): installinfo['optional_installs'].append(iteminfo) +def updateAvailableLicenseSeats(installinfo): + '''Records # of available seats for each optional install''' + license_info_url = munkicommon.pref('LicenseInfoURL') + if not license_info_url: + # nothing to do! + return + if not installinfo.get('optional_installs'): + # nothing to do! + return + + license_info = {} + items_to_check = [item['name'] + for item in installinfo['optional_installs'] + if not item['installed']] + + # complicated logic here to 'batch' process our GET requests but + # keep them under 256 characters each + start_index = 0 + while start_index < len(items_to_check): + end_index = len(items_to_check) + while True: + query_items = ['name=' + quote_plus(item) + for item in items_to_check[start_index:end_index]] + querystring = '?' + '&'.join(query_items) + url = license_info_url + querystring + if len(url) < 256: + break + # drop an item and see if we're under 256 characters + end_index = end_index - 1 + + munkicommon.display_debug1('Fetching licensed seat data from %s' % url) + license_data = getDataFromURL(url) + munkicommon.display_debug1('Got: %s' % license_data) + try: + license_dict = FoundationPlist.readPlistFromString( + license_data) + except FoundationPlist.FoundationPlistException: + munkicommon.display_warning( + 'Bad license data from %s: %s' + % (url, license_data)) + # should we act as all are zero? + continue + else: + # merge data from license_dict into license_info + license_info.update(license_dict) + start_index = end_index + + # use license_info to update our remaining seats + for item in installinfo['optional_installs']: + munkicommon.display_debug2( + 'Looking for license info for %s' % item['name']) + if item['name'] in license_info.keys(): + # record available seats for this item + munkicommon.display_debug1( + 'Recording available seats for %s: %s' + % (item['name'], license_info[item['name']])) + try: + seats_available = int(license_info[item['name']]) > 0 + munkicommon.display_debug2( + 'Seats available: %s' % seats_available) + item['licensed_seats_available'] = seats_available + except ValueError: + munkicommon.display_warning( + 'Bad license data for %s: %s' + % (item['name'], license_info[item['name']])) + item['licensed_seats_available'] = False + + def processInstall(manifestitem, cataloglist, installinfo): """Processes a manifest item. Determines if it needs to be installed, and if so, if any items it is dependent on need to @@ -2706,6 +2775,10 @@ def check(client_id='', localmanifestpath=None): installinfo) if munkicommon.stopRequested(): return 0 + + # verify available license seats for optional installs + if installinfo.get('optional_installs'): + updateAvailableLicenseSeats(installinfo) # now process any self-serve choices usermanifest = '/Users/Shared/.SelfServeManifest' @@ -2740,8 +2813,13 @@ def check(client_id='', localmanifestpath=None): '**Processing self-serve choices**') selfserveinstalls = getManifestValueForKey(selfservemanifest, 'managed_installs') + + # build list of items in the optional_installs list + # that have not exceeded available seats available_optional_installs = [item['name'] - for item in installinfo.get('optional_installs', [])] + for item in installinfo.get('optional_installs', []) + if (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 @@ -3079,6 +3157,28 @@ def checkForceInstallPackages(): return result +def getDataFromURL(url): + '''Returns data from url as string. We use the existing + getResourceIfChangedAtomically function so any custom + authentication/authorization headers are reused''' + urldata = os.path.join(munkicommon.tmpdir, 'urldata') + if os.path.exists(urldata): + try: + os.unlink(urldata) + except (IOError, OSError), err: + munkicommon.display_warning('Error in getDataFromURL' % err) + unused_result = getResourceIfChangedAtomically(url, urldata) + try: + fdesc = open(urldata) + data = fdesc.read() + fdesc.close() + os.unlink(urldata) + return data + except (IOError, OSError), err: + munkicommon.display_warning('Error in getDataFromURL' % err) + return '' + + def getResourceIfChangedAtomically(url, destinationpath, message=None,