From bf8cb1b7ebf36ec9cb640af0b99fac28c98c3dbd Mon Sep 17 00:00:00 2001 From: Justin McWilliams Date: Tue, 31 Aug 2010 15:15:47 +0000 Subject: [PATCH] Package SHA-256 Hash verification git-svn-id: http://munki.googlecode.com/svn/branches/pkg-hash-verification@699 a4e17f2e-e282-11dd-95e1-755cbddbdd66 --- code/client/makepkginfo | 6 + code/client/munkilib/munkicommon.py | 321 ++++++++------- code/client/munkilib/updatecheck.py | 587 ++++++++++++++++------------ 3 files changed, 515 insertions(+), 399 deletions(-) diff --git a/code/client/makepkginfo b/code/client/makepkginfo index 85e86187..1e86f2e7 100755 --- a/code/client/makepkginfo +++ b/code/client/makepkginfo @@ -395,8 +395,10 @@ def main(): if item and os.path.exists(item): # get size of installer item itemsize = 0 + itemhash = "N/A" if os.path.isfile(item): itemsize = int(os.path.getsize(item)) + itemhash = munkicommon.getsha256hash(item) if os.path.isdir(item): # need to walk the dir and add it all up for (path, dirs, files) in os.walk(item): @@ -435,6 +437,8 @@ def main(): exit(-1) catinfo['installer_item_size'] = int(itemsize/1024) + catinfo['installer_item_hash'] = itemhash + # try to generate the correct item location temppath = item location = "" @@ -469,7 +473,9 @@ def main(): location = os.path.split(uninstallerpath)[1] catinfo['uninstaller_item_location'] = location itemsize = int(os.path.getsize(uninstallerpath)) + itemhash = munkicommon.getsha256hash(uninstallerpath) catinfo['uninstaller_item_size'] = int(itemsize/1024) + catinfo['uninstaller_item_hash'] = itemhash else: print >> sys.stderr, "No uninstaller at %s" % \ uninstallerpath diff --git a/code/client/munkilib/munkicommon.py b/code/client/munkilib/munkicommon.py index 3e7d15ae..b3e542c0 100644 --- a/code/client/munkilib/munkicommon.py +++ b/code/client/munkilib/munkicommon.py @@ -6,9 +6,9 @@ # 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 -# +# # http://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. @@ -22,14 +22,14 @@ Created by Greg Neagle on 2008-11-18. Common functions used by the munki tools. """ -import sys -import os -import time -import subprocess -import tempfile -import shutil -import urllib2 import hashlib +import os +import shutil +import subprocess +import sys +import tempfile +import time +import urllib2 from distutils import version from xml.dom import minidom @@ -112,8 +112,8 @@ def display_info(msg): elif verbose > 0: print msg.encode('UTF-8') sys.stdout.flush() - - + + def display_detail(msg): """ Displays minor info messages, formatting as needed @@ -128,8 +128,8 @@ def display_detail(msg): sys.stdout.flush() if pref('LoggingLevel') > 0: log(msg) - - + + def display_debug1(msg): """ Displays debug messages, formatting as needed @@ -155,8 +155,8 @@ def display_debug2(msg): print msg.encode('UTF-8') if pref('LoggingLevel') > 2: log("DEBUG2: %s" % msg) - - + + def reset_warnings(): '''Rotate our warnings log.''' warningsfile = os.path.join(os.path.dirname(pref("LogFile")), @@ -216,8 +216,8 @@ def log(msg, logname=''): fileobj.close() except (OSError, IOError): pass - - + + def rotatelog(logname=''): '''Rotate a log''' if not logname: @@ -267,19 +267,19 @@ def printreportitem(label, value, indent=0): printreportitem(subkey, value[subkey], indent+1) else: print indentspace*indent, "%s: %s" % (label, value) - - + + def printreport(reportdict): """Prints the report dictionary in a pretty(?) way""" for key in reportdict.keys(): printreportitem(key, reportdict[key]) - + def savereport(): '''Save our report''' - FoundationPlist.writePlist(report, + FoundationPlist.writePlist(report, os.path.join(pref('ManagedInstallDir'), "ManagedInstallReport.plist")) - + def archive_report(): '''Archive a report''' @@ -302,13 +302,13 @@ def archive_report(): except (OSError, IOError): display_warning("Could not archive report.") # now keep number of archived reports to 100 or fewer - proc = subprocess.Popen(['/bin/ls', '-t1', archivepath], - bufsize=1, stdout=subprocess.PIPE, + proc = subprocess.Popen(['/bin/ls', '-t1', archivepath], + bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (output, err) = proc.communicate() if output: - archiveitems = [item - for item in str(output).splitlines() + archiveitems = [item + for item in str(output).splitlines() if item.startswith("ManagedInstallReport-")] if len(archiveitems) > 100: for item in archiveitems[100:]: @@ -319,9 +319,9 @@ def archive_report(): except (OSError, IOError): display_warning("Could not remove archive item %s" % itempath) - - - + + + # misc functions def validPlist(path): @@ -335,7 +335,7 @@ def validPlist(path): def stopRequested(): - """Allows user to cancel operations when + """Allows user to cancel operations when MunkiStatus is being used""" if munkistatusoutput: if munkistatus.getStopButtonState() == 1: @@ -354,8 +354,8 @@ def getconsoleuser(): def currentGUIusers(): '''Gets a list of GUI users by parsing the output of /usr/bin/who''' gui_users = [] - proc = subprocess.Popen("/usr/bin/who", shell=False, - stdin=subprocess.PIPE, + proc = subprocess.Popen("/usr/bin/who", shell=False, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (output, err) = proc.communicate() lines = str(output).splitlines() @@ -363,15 +363,15 @@ def currentGUIusers(): if "console" in line: parts = line.split() gui_users.append(parts[0]) - + return gui_users def pythonScriptRunning(scriptname): '''Returns Process ID for a running python script''' cmd = ['/bin/ps', '-eo', 'pid=,command='] - proc = subprocess.Popen(cmd, shell=False, bufsize=1, - stdin=subprocess.PIPE, + proc = subprocess.Popen(cmd, shell=False, bufsize=1, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() mypid = os.getpid() @@ -379,28 +379,28 @@ def pythonScriptRunning(scriptname): for line in lines: (pid, process) = line.split(None, 1) # first look for Python processes - if (process.find("MacOS/Python ") != -1 or + if (process.find("MacOS/Python ") != -1 or process.find("python ") != -1): if process.find(scriptname) != -1: if int(pid) != int(mypid): return pid return 0 - - + + def osascript(osastring): '''Wrapper to run AppleScript commands''' cmd = ['/usr/bin/osascript', '-e', osastring] - proc = subprocess.Popen(cmd, shell=False, bufsize=1, - stdin=subprocess.PIPE, + proc = subprocess.Popen(cmd, shell=False, bufsize=1, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() if proc.returncode != 0: print >> sys.stderr, "Error: ", err if out: return str(out).decode('UTF-8').rstrip("\n") - - + + # dmg helpers def mountdmg(dmgpath, use_shadow=False): @@ -411,12 +411,12 @@ def mountdmg(dmgpath, use_shadow=False): """ mountpoints = [] dmgname = os.path.basename(dmgpath) - cmd = ['/usr/bin/hdiutil', 'attach', dmgpath, + cmd = ['/usr/bin/hdiutil', 'attach', dmgpath, '-mountRandom', '/tmp', '-nobrowse', '-plist'] if use_shadow: cmd.append('-shadow') proc = subprocess.Popen(cmd, - bufsize=1, stdout=subprocess.PIPE, + bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (pliststr, err) = proc.communicate() if err: @@ -435,36 +435,59 @@ def unmountdmg(mountpoint): Unmounts the dmg at mountpoint """ proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint], - bufsize=1, stdout=subprocess.PIPE, + bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (ouptut, err) = proc.communicate() if proc.returncode: display_warning("Attempting to force unmount %s" % mountpoint) # try forcing the unmount - retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint, + retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint, '-force']) if retcode: display_warning("Failed to unmount %s" % mountpoint) - + + +def gethash(filename, hash_function): + """ + Calculates the hashvalue of the given file with the given hash_function. + + Args: + filename: The file name to calculate the hash value of. + hash_function: The hash function object to use, which was instanciated + before calling this function, e.g. hashlib.md5(). + + Returns: + The hashvalue of the given file as hex string. + """ + if not os.path.isfile(filename): + return "NOT A FILE" + + f = open(filename, 'rb') + while 1: + chunk = f.read(2**16) + if not chunk: + break + hash_function.update(chunk) + f.close() + return hash_function.hexdigest() + def getmd5hash(filename): """ Returns hex of MD5 checksum of a file """ - if not os.path.isfile(filename): - return "NOT A FILE" + hash_function = hashlib.md5() + return gethash(filename, hash_function) + + +def getsha256hash(filename): + """ + Returns the SHA-256 hash value of a file as a hex string. + """ + hash_function = hashlib.sha256() + return gethash(filename, hash_function) - fileobj = open(filename, 'rb') - md5hash = hashlib.md5() - while 1: - chunk = fileobj.read(2**16) - if not chunk: - break - md5hash.update(chunk) - fileobj.close() - return md5hash.hexdigest() - def isApplication(pathname): '''Returns true if path appears to be an OS X application''' # No symlinks, please @@ -481,11 +504,11 @@ def isApplication(pathname): if 'CFBundlePackageType' in plist: if plist['CFBundlePackageType'] != 'APPL': return False - # get CFBundleExecutable, + # get CFBundleExecutable, # falling back to bundle name if it's missing - bundleexecutable = plist.get('CFBundleExecutable', + bundleexecutable = plist.get('CFBundleExecutable', os.path.basename(pathname)) - bundleexecutablepath = os.path.join(pathname, "Contents", + bundleexecutablepath = os.path.join(pathname, "Contents", "MacOS", bundleexecutable) if os.path.exists(bundleexecutablepath): return True @@ -523,16 +546,16 @@ def prefs(): _prefs['SuppressUserNotification'] = False _prefs['SuppressAutoInstall'] = False _prefs['SuppressStopButtonOnInstall'] = False - + prefsfile = "/Library/Preferences/ManagedInstalls.plist" plist = {} if os.path.exists(prefsfile): try: plist = FoundationPlist.readPlist(prefsfile) except FoundationPlist.NSPropertyListSerializationException: - display_error("ERROR: Could not read preferences file %s." + display_error("ERROR: Could not read preferences file %s." % prefsfile) - raise Exception("Could not read preferences file %s." % + raise Exception("Could not read preferences file %s." % prefsfile) try: for key in plist.keys(): @@ -542,33 +565,33 @@ def prefs(): else: _prefs[key] = plist[key] except AttributeError: - display_error("ERROR: Prefs file %s contains invalid data." + display_error("ERROR: Prefs file %s contains invalid data." % prefsfile) raise Exception("Preferences file %s invalid." % prefsfile) else: # no prefs file, so we'll write out a "default" prefs file del _prefs['LastNotifiedDate'] FoundationPlist.writePlist(_prefs, prefsfile) - + return _prefs def pref(prefname): '''Return a prefernce''' return prefs().get(prefname,'') - - -##################################################### + + +##################################################### # Apple package utilities ##################################################### def getInstallerPkgInfo(filename): - """Uses Apple's installer tool to get basic info + """Uses Apple's installer tool to get basic info about an installer item.""" installerinfo = {} - proc = subprocess.Popen(["/usr/sbin/installer", "-pkginfo", "-verbose", - "-plist", "-pkg", filename], - bufsize=1, stdout=subprocess.PIPE, + proc = subprocess.Popen(["/usr/sbin/installer", "-pkginfo", "-verbose", + "-plist", "-pkg", filename], + bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() @@ -584,7 +607,7 @@ def getInstallerPkgInfo(filename): pass if plist: break - if plist: + if plist: if 'Size' in plist: installerinfo['installed_size'] = int(plist['Size']) installerinfo['description'] = plist.get('Description',"") @@ -592,21 +615,21 @@ def getInstallerPkgInfo(filename): installerinfo['RestartAction'] = "RequireRestart" if "Title" in plist: installerinfo['display_name'] = plist['Title'] - - proc = subprocess.Popen(["/usr/sbin/installer", - "-query", "RestartAction", - "-pkg", filename], - bufsize=1, - stdout=subprocess.PIPE, + + proc = subprocess.Popen(["/usr/sbin/installer", + "-query", "RestartAction", + "-pkg", filename], + bufsize=1, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() if out: restartAction = str(out).rstrip('\n') if restartAction != 'None': installerinfo['RestartAction'] = restartAction - + return installerinfo - + def padVersionString(versString, tupleCount): '''Normalize the format of a version string''' @@ -619,7 +642,7 @@ def padVersionString(versString, tupleCount): while len(components) < tupleCount : components.append("0") return ".".join(components) - + def getVersionString(plist): '''Gets a version string from the plist. @@ -650,7 +673,7 @@ def getVersionString(plist): # replace commas with periods CFBundleVersion = CFBundleVersion.replace(',','.') return CFBundleVersion - + return '' @@ -665,9 +688,9 @@ def getExtendedVersion(bundlepath): versionstring = getVersionString(plist) if versionstring: return padVersionString(versionstring, 5) - + # no version number in Info.plist. Maybe old-style package? - infopath = os.path.join(bundlepath, "Contents", "Resources", + infopath = os.path.join(bundlepath, "Contents", "Resources", "English.lproj") if os.path.exists(infopath): for item in os.listdir(infopath): @@ -683,13 +706,13 @@ def getExtendedVersion(bundlepath): label = parts[0] if label == "Version": return padVersionString(parts[1], 5) - + # didn't find a version number, so return 0... return "0.0.0.0.0" - - + + def parsePkgRefs(filename): - """Parses a .dist or PackageInfo file looking for pkg-ref or pkg-info tags + """Parses a .dist or PackageInfo file looking for pkg-ref or pkg-info tags to get info on included sub-packages""" info = [] dom = minidom.parse(filename) @@ -702,7 +725,7 @@ def parsePkgRefs(filename): # for key in keys: # print key, "=>", \ # ref.attributes[key].value.encode('UTF-8') - + pkginfo = {} pkginfo['packageid'] = \ ref.attributes['id'].value.encode('UTF-8') @@ -724,7 +747,7 @@ def parsePkgRefs(filename): # for key in keys: # print key, "=>", \ # ref.attributes[key].value.encode('UTF-8') - + pkginfo = {} pkginfo['packageid'] = \ ref.attributes['identifier'].value.encode('UTF-8') @@ -751,14 +774,14 @@ def getFlatPackageInfo(pkgpath): cwd = os.getcwd() # change into our tmpdir so we can use xar to unarchive the flat package os.chdir(pkgtmp) - returncode = subprocess.call(["/usr/bin/xar", "-xf", abspkgpath, + returncode = subprocess.call(["/usr/bin/xar", "-xf", abspkgpath, "--exclude", "Payload"]) if returncode == 0: currentdir = pkgtmp packageinfofile = os.path.join(currentdir, "PackageInfo") if os.path.exists(packageinfofile): infoarray = parsePkgRefs(packageinfofile) - + if not infoarray: # didn't get any packageid info or no PackageInfo file # look for subpackages at the top level @@ -768,14 +791,14 @@ def getFlatPackageInfo(pkgpath): packageinfofile = os.path.join(itempath, "PackageInfo") if os.path.exists(packageinfofile): infoarray.extend(parsePkgRefs(packageinfofile)) - + if not infoarray: # found no PackageInfo files and no subpackages, # so let's look at the Distribution file distributionfile = os.path.join(currentdir, "Distribution") if os.path.exists(distributionfile): infoarray = parsePkgRefs(distributionfile) - + # change back to original working dir os.chdir(cwd) shutil.rmtree(pkgtmp) @@ -797,15 +820,15 @@ def getOnePackageInfo(pkgpath): pkginfo['packageid'] = plist["Bundle identifier"] else: pkginfo['packageid'] = os.path.basename(pkgpath) - + if "CFBundleName" in plist: pkginfo['name'] = plist["CFBundleName"] - + if "IFPkgFlagInstalledSize" in plist: pkginfo['installed_size'] = plist["IFPkgFlagInstalledSize"] - + pkginfo['version'] = getExtendedVersion(pkgpath) - except (AttributeError, + except (AttributeError, FoundationPlist.NSPropertyListSerializationException): pkginfo['packageid'] = "BAD PLIST in %s" % \ os.path.basename(pkgpath) @@ -849,7 +872,7 @@ def getText(nodelist): def getBundlePackageInfo(pkgpath): '''Get metadata from a bundle-style package''' infoarray = [] - + if pkgpath.endswith(".pkg"): pkginfo = getOnePackageInfo(pkgpath) if pkginfo: @@ -860,7 +883,7 @@ def getBundlePackageInfo(pkgpath): if os.path.exists(bundlecontents): for item in os.listdir(bundlecontents): if item.endswith(".dist"): - filename = os.path.join(bundlecontents, item) + filename = os.path.join(bundlecontents, item) dom = minidom.parse(filename) pkgrefs = dom.getElementsByTagName("pkg-ref") if pkgrefs: @@ -874,10 +897,10 @@ def getBundlePackageInfo(pkgpath): pkginfo = getBundlePackageInfo(subpkgpath) if pkginfo: infoarray.extend(pkginfo) - + if infoarray: return infoarray - + # no .dist file found, look for packages in subdirs dirsToSearch = [] plistpath = os.path.join(pkgpath, "Contents", "Info.plist") @@ -886,8 +909,8 @@ def getBundlePackageInfo(pkgpath): if 'IFPkgFlagComponentDirectory' in plist: componentdir = plist['IFPkgFlagComponentDirectory'] dirsToSearch.append(componentdir) - - if dirsToSearch == []: + + if dirsToSearch == []: dirsToSearch = ['', 'Contents', 'Contents/Installers', 'Contents/Packages', 'Contents/Resources', 'Contents/Resources/Packages'] @@ -905,18 +928,18 @@ def getBundlePackageInfo(pkgpath): pkginfo = getBundlePackageInfo(itempath) if pkginfo: infoarray.extend(pkginfo) - + if infoarray: return infoarray else: - # couldn't find any subpackages, + # couldn't find any subpackages, # just return info from the .dist file # if it exists for item in os.listdir(bundlecontents): if item.endswith(".dist"): distfile = os.path.join(bundlecontents, item) infoarray.extend(parsePkgRefs(distfile)) - + return infoarray @@ -930,10 +953,10 @@ def getReceiptInfo(pkgname): if os.path.isdir(pkgname): # bundle-style package? info = getBundlePackageInfo(pkgname) - + elif pkgname.endswith('.dist'): info = parsePkgRefs(pkgname) - + return info @@ -944,13 +967,13 @@ def getInstalledPackageVersion(pkgid): Returns the version string of the installed pkg if it exists, or an empty string if it does not """ - + # First check (Leopard and later) package database - - proc = subprocess.Popen(["/usr/sbin/pkgutil", - "--pkg-info-plist", pkgid], - bufsize=1, - stdout=subprocess.PIPE, + + proc = subprocess.Popen(["/usr/sbin/pkgutil", + "--pkg-info-plist", pkgid], + bufsize=1, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() @@ -962,16 +985,16 @@ def getInstalledPackageVersion(pkgid): foundbundleid = plist["pkgid"] if "pkg-version" in plist: foundvers = plist["pkg-version"] - + if pkgid == foundbundleid: - display_debug2("\tThis machine has %s, version %s" % + display_debug2("\tThis machine has %s, version %s" % (pkgid, foundvers)) return padVersionString(foundvers, 5) except (AttributeError, FoundationPlist.NSPropertyListSerializationException): pass - - # If we got to this point, we haven't found the pkgid yet. + + # If we got to this point, we haven't found the pkgid yet. # Check /Library/Receipts receiptsdir = "/Library/Receipts" if os.path.exists(receiptsdir): @@ -988,18 +1011,18 @@ def getInstalledPackageVersion(pkgid): if version.LooseVersion(foundvers) > \ version.LooseVersion(highestversion): highestversion = foundvers - + if highestversion != "0": - display_debug2("\tThis machine has %s, version %s" % + display_debug2("\tThis machine has %s, version %s" % (pkgid, highestversion)) return highestversion - - + + # This package does not appear to be currently installed display_debug2("\tThis machine does not have %s" % pkgid) return "" - - + + def nameAndVersion(aString): """ Splits a string into the name and version numbers: @@ -1031,7 +1054,7 @@ def findInstallerItem(path): for item in os.listdir(path): if item.endswith('.pkg'): return path - + # we didn't find a pkg at this level # look for a Packages dir path = os.path.join(path,"Packages") @@ -1058,19 +1081,19 @@ def getPackageMetaData(pkgitem): name version description - receipts: an array of packageids that may be installed + receipts: an array of packageids that may be installed (some may not be installed on some machines) """ - + pkgitem = findInstallerItem(pkgitem) if pkgitem == None: return {} - - # first get the data /usr/sbin/installer will give us + + # first get the data /usr/sbin/installer will give us installerinfo = getInstallerPkgInfo(pkgitem) # now look for receipt/subpkg info receiptinfo = getReceiptInfo(pkgitem) - + name = os.path.split(pkgitem)[1] shortname = os.path.splitext(name)[0] metaversion = getExtendedVersion(pkgitem) @@ -1086,43 +1109,43 @@ def getPackageMetaData(pkgitem): if "installed_size" in infoitem: # note this is in KBytes installedsize += infoitem['installed_size'] - + if metaversion == "0.0.0.0.0": metaversion = highestpkgversion elif len(receiptinfo) == 1: # there is only one package in this item metaversion = highestpkgversion elif highestpkgversion.startswith(metaversion): - # for example, highestpkgversion is 2.0.3124.0, + # for example, highestpkgversion is 2.0.3124.0, # version in filename is 2.0 metaversion = highestpkgversion - + cataloginfo = {} cataloginfo['name'] = nameAndVersion(shortname)[0] cataloginfo['version'] = metaversion for key in ('display_name', 'RestartAction', 'description'): if key in installerinfo: cataloginfo[key] = installerinfo[key] - + if 'installed_size' in installerinfo: if installerinfo['installed_size'] > 0: cataloginfo['installed_size'] = installerinfo['installed_size'] elif installedsize: cataloginfo['installed_size'] = installedsize - - cataloginfo['receipts'] = receiptinfo - + + cataloginfo['receipts'] = receiptinfo + return cataloginfo - - + + # some utility functions def getAvailableDiskSpace(volumepath="/"): '''Returns available diskspace in KBytes.''' cmd = ["/usr/sbin/diskutil", "info", "-plist", volumepath] - proc = subprocess.Popen(cmd, + proc = subprocess.Popen(cmd, bufsize=1, - stdout=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() if out: @@ -1132,7 +1155,7 @@ def getAvailableDiskSpace(volumepath="/"): if "FreeSpace" in plist: # plist["FreeSpace"] is in bytes return int(plist["FreeSpace"]/1024) - + except (AttributeError, FoundationPlist.NSPropertyListSerializationException): pass @@ -1146,12 +1169,12 @@ def cleanUpTmpDir(): global tmpdir if tmpdir: try: - shutil.rmtree(tmpdir) + shutil.rmtree(tmpdir) except (OSError, IOError): pass tmpdir = None - - + + # module globals #debug = False verbose = 1 diff --git a/code/client/munkilib/updatecheck.py b/code/client/munkilib/updatecheck.py index 2cb406f7..17be0d57 100644 --- a/code/client/munkilib/updatecheck.py +++ b/code/client/munkilib/updatecheck.py @@ -47,23 +47,23 @@ def makeCatalogDB(catalogitems): get our common data faster. Returns a dict we can use like a database''' name_table = {} pkgid_table = {} - + itemindex = -1 for item in catalogitems: itemindex = itemindex + 1 name = item.get('name', "NO NAME") vers = item.get('version', "NO VERSION") - + if name == "NO NAME" or vers == "NO VERSION": munkicommon.display_warning("Bad pkginfo: %s" % item) - + # build indexes for items by name and version if not name in name_table: name_table[name] = {} if not vers in name_table[name]: name_table[name][vers] = [] name_table[name][vers].append(itemindex) - + # do the same for any aliases #if 'aliases' in item: # for alias in item['aliases']: @@ -72,7 +72,7 @@ def makeCatalogDB(catalogitems): # if not vers in name_table[alias]: # name_table[alias][vers] = [] # name_table[alias][vers].append(itemindex) - + # build table of receipts if 'receipts' in item: for receipt in item['receipts']: @@ -86,12 +86,12 @@ def makeCatalogDB(catalogitems): pkgid_table[ receipt['packageid']][ receipt['version']].append(itemindex) - + # build table of update items with a list comprehension -- # filter all items from the catalogitems that have a non-empty # 'update_for' list updaters = [item for item in catalogitems if item.get('update_for')] - + # build table of autoremove items with a list comprehension -- # filter all items from the catalogitems that have a non-empty # 'autoremove' list @@ -102,14 +102,14 @@ def makeCatalogDB(catalogitems): if item.get('autoremove')] # convert to set and back to list to get list of unique names autoremoveitems = list(set(autoremoveitems)) - + pkgdb = {} pkgdb['named'] = name_table pkgdb['receipts'] = pkgid_table pkgdb['updaters'] = updaters pkgdb['autoremoveitems'] = autoremoveitems pkgdb['items'] = catalogitems - + return pkgdb @@ -124,12 +124,12 @@ def addPackageids(catalogitems, pkgid_table): if item.get('receipts'): if not name in pkgid_table: pkgid_table[name] = [] - + for receipt in item['receipts']: if 'packageid' in receipt: if not receipt['packageid'] in pkgid_table[name]: pkgid_table[name].append(receipt['packageid']) - + def getFirstPlist(textString): '''Gets the next plist from a set of concatenated text-style plists. @@ -146,7 +146,7 @@ def getFirstPlist(textString): # adjust end value plistEnd = plistEnd + 8 return (textString[plistStart:plistEnd], textString[plistEnd:]) - + installedpkgs = {} def getInstalledPackages(): @@ -154,10 +154,10 @@ def getInstalledPackages(): Builds a dictionary of installed receipts and their version number """ global installedpkgs - + # we use the --regexp option to pkgutil to get it to return receipt # info for all installed packages. Huge speed up. - proc = subprocess.Popen(["/usr/sbin/pkgutil", "--regexp", + proc = subprocess.Popen(["/usr/sbin/pkgutil", "--regexp", "--pkg-info-plist", ".*"], bufsize=8192, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() @@ -170,7 +170,7 @@ def getInstalledPackages(): plist["pkg-version"] or "0.0.0.0.0" else: break - + # Now check /Library/Receipts receiptsdir = "/Library/Receipts" if os.path.exists(receiptsdir): @@ -195,7 +195,7 @@ def getInstalledPackages(): if version.LooseVersion(thisversion) > \ version.LooseVersion(storedversion): installedpkgs[pkgid] = thisversion - + return installedpkgs # global pkgdata @@ -208,10 +208,10 @@ def analyzeInstalledPkgs(): for catalogname in catalog.keys(): catalogitems = catalog[catalogname]['items'] addPackageids(catalogitems, managed_pkgids) - + if not installedpkgs: getInstalledPackages() - + installed = [] partiallyinstalled = [] installedpkgsmatchedtoname = {} @@ -230,7 +230,7 @@ def analyzeInstalledPkgs(): installed.append(name) elif somepkgsfound: partiallyinstalled.append(name) - + # we pay special attention to the items that seem partially installed. # we need to see if there are any packages that are unique to this item # if there aren't, then this item probably isn't installed, and we're @@ -250,7 +250,7 @@ def analyzeInstalledPkgs(): uniquepkgs = list(set(pkgsforthisname) - set(allotherpkgs)) if uniquepkgs: installed.append(name) - + # build our reference table references = {} for name in installed: @@ -258,7 +258,7 @@ def analyzeInstalledPkgs(): if not pkg in references: references[pkg] = [] references[pkg].append(name) - + pkgdata = {} pkgdata['receipts_for_name'] = installedpkgsmatchedtoname pkgdata['installed_names'] = installed @@ -289,7 +289,7 @@ def getAppData(): spdict = plist[0] if '_items' in spdict: appdict = spdict['_items'] - + return appdict @@ -307,7 +307,7 @@ def getAppBundleID(path): except (AttributeError, FoundationPlist.NSPropertyListSerializationException): pass - + return None @@ -331,10 +331,10 @@ def compareApplicationVersion(app): """ app is a dict with application bundle info - + First checks the given path if it's available, then uses system profiler data to look for the app - + Returns 0 if the app isn't installed or doesn't have valid Info.plist -1 if it's older @@ -346,12 +346,12 @@ def compareApplicationVersion(app): filepath = os.path.join(app['path'], 'Contents', 'Info.plist') if os.path.exists(filepath): return compareBundleVersion(app) - + # not in default location, so let's search: name = app.get('CFBundleName','') bundleid = app.get('CFBundleIdentifier','') versionstring = app.get('CFBundleShortVersionString') - + if name == '' and bundleid == '': if 'path' in app: # already looked at default path, and we don't have @@ -362,7 +362,7 @@ def compareApplicationVersion(app): munkicommon.display_error( "No application name or bundleid was specified!") return -2 - + munkicommon.display_debug1( "Looking for application %s with bundleid: %s, version %s..." % (name, bundleid, versionstring)) @@ -380,13 +380,13 @@ def compareApplicationVersion(app): if '_name' in item: if item['_name'] == name: appinfo.append(item) - + if not appinfo: # app isn't present! munkicommon.display_debug1( "\tDid not find this application on the startup disk.") return 0 - + for item in appinfo: if '_name' in item: munkicommon.display_debug2( @@ -405,7 +405,7 @@ def compareApplicationVersion(app): if compareVersions(item['version'], versionstring) == 2: # version is newer return 2 - + # if we got this far, must only be older munkicommon.display_debug1( "An older version of this application is present.") @@ -426,7 +426,7 @@ def compareBundleVersion(item): else: munkicommon.display_error("Missing bundle path or version!") return -2 - + munkicommon.display_debug1("Checking bundle %s for version %s..." % (item['path'], vers)) filepath = os.path.join(item['path'], 'Contents', 'Info.plist') @@ -437,14 +437,14 @@ def compareBundleVersion(item): munkicommon.display_debug1( "\tNo Info.plist found at %s" % filepath) return 0 - + munkicommon.display_debug1("\tFound Info.plist at %s" % filepath) try: plist = FoundationPlist.readPlist(filepath) except FoundationPlist.NSPropertyListSerializationException: munkicommon.display_debug1("\t%s may not be a plist!" % filepath) return 0 - + installedvers = munkicommon.getVersionString(plist) if installedvers: return compareVersions(installedvers, vers) @@ -469,19 +469,19 @@ def comparePlistVersion(item): else: munkicommon.display_error("Missing plist path or version!") return -2 - + munkicommon.display_debug1("Checking %s for version %s..." % (filepath, vers)) if not os.path.exists(filepath): munkicommon.display_debug1("\tNo plist found at %s" % filepath) return 0 - + try: plist = FoundationPlist.readPlist(filepath) except FoundationPlist.NSPropertyListSerializationException: munkicommon.display_debug1("\t%s may not be a plist!" % filepath) return 0 - + installedvers = munkicommon.getVersionString(plist) if installedvers: return compareVersions(installedvers, vers) @@ -539,7 +539,7 @@ def compareReceiptVersion(item): else: print "Missing packageid or version info!" return -2 - + munkicommon.display_debug1("Looking for package %s, version %s" % (pkgid, vers)) installedvers = installedpkgs.get(pkgid) @@ -549,7 +549,7 @@ def compareReceiptVersion(item): munkicommon.display_debug1( "\tThis package is not currently installed.") return 0 - + def getInstalledVersion(item_plist): """ @@ -573,7 +573,7 @@ def getInstalledVersion(item_plist): return installedversion # if we get here all receipts match return item_plist['version'] - + if 'installs' in item_plist: for install_item in item_plist['installs']: if install_item['type'] == 'application': @@ -604,7 +604,7 @@ def getInstalledVersion(item_plist): if '_name' in ad_item: if ad_item['_name'] == name: appinfo.append(ad_item) - + maxversion = "0.0.0.0.0" for ai_item in appinfo: if 'version' in ai_item: @@ -613,22 +613,22 @@ def getInstalledVersion(item_plist): # version is higher maxversion = ai_item['version'] installedappvers = maxversion - + if compareVersions(installedappvers, install_item['CFBundleShortVersionString']) == 2: # version is higher installedversion = "newer than %s" % plist['version'] return installedversion - + if compareVersions(installedappvers, install_item['CFBundleShortVersionString']) == -1: # version is lower installedversion = "older than %s" % plist['version'] return installedversion - + # if we get here all app versions match return item_plist['version'] - + # if we fall through to here we have no idea what version we have return "UNKNOWN" @@ -643,16 +643,16 @@ def download_installeritem(location): if not downloadbaseurl.endswith('/'): downloadbaseurl = downloadbaseurl + "/" munkicommon.display_debug2("Download base URL is: %s" % downloadbaseurl) - + mycachedir = os.path.join(ManagedInstallDir, "Cache") - + # build a URL, quoting the the location to encode reserved characters pkgurl = downloadbaseurl + urllib2.quote(location) - + # grab last path component of location to derive package name. pkgname = os.path.basename(location) destinationpath = os.path.join(mycachedir, pkgname) - + munkicommon.display_detail("Downloading %s from %s" % (pkgname, location)) # bump up verboseness so we get download percentage done feedback. # this is kind of a hack... @@ -664,15 +664,15 @@ def download_installeritem(location): message=dl_message) # set verboseness back. munkicommon.verbose = oldverbose - + if path: - return True - + return (True, destinationpath) + else: munkicommon.display_error("Could not download %s from server." % pkgname) munkicommon.display_error(err) - return False + return (False, destinationpath) def isItemInInstallInfo(manifestitem_pl, thelist, vers=''): @@ -692,12 +692,12 @@ def isItemInInstallInfo(manifestitem_pl, thelist, vers=''): return True #if the version already processed is the same or greater, #then we're good - if (compareVersions(item.get('version_to_install'), vers) + if (compareVersions(item.get('version_to_install'), vers) in (1, 2)): return True - + return False - + def nameAndVersion(aString): """ @@ -715,7 +715,7 @@ def nameAndVersion(aString): name = delim.join(chunks) if vers[0] in "0123456789": return (name, vers) - + return (aString, '') @@ -723,19 +723,19 @@ def getAllItemsWithName(name, cataloglist): """ Searches the catalogs in cataloglist for all items matching the given name. Returns a list of pkginfo items. - + The returned list is sorted with newest version first. No precedence is given to catalog order. - + """ def compare_item_versions(a, b): return cmp(version.LooseVersion(b['version']), version.LooseVersion(a['version'])) - + itemlist = [] # we'll throw away any included version info (name, includedversion) = nameAndVersion(name) - + munkicommon.display_debug1("Looking for all items matching: %s..." % name) for catalogname in cataloglist: if not catalogname in catalog.keys(): @@ -754,13 +754,13 @@ def getAllItemsWithName(name, cataloglist): "Adding item %s, version %s from catalog %s..." % (name, thisitem['version'], catalogname)) itemlist.append(thisitem) - + if itemlist: # sort so latest version is first itemlist.sort(compare_item_versions) - + return itemlist - + def getItemDetail(name, cataloglist, vers=''): """ @@ -772,7 +772,7 @@ def getItemDetail(name, cataloglist, vers=''): """ def compare_version_keys(a, b): return cmp(version.LooseVersion(b), version.LooseVersion(a)) - + global catalog (name, includedversion) = nameAndVersion(name) if vers == '': @@ -783,14 +783,14 @@ def getItemDetail(name, cataloglist, vers=''): vers = munkicommon.padVersionString(vers, 5) else: vers = 'latest' - + munkicommon.display_debug1("Looking for detail for: %s, version %s..." % (name, vers)) for catalogname in cataloglist: if not catalogname in catalog.keys(): # in case the list refers to a non-existent catalog continue - + # is name in the catalog? if name in catalog[catalogname]['named']: itemsmatchingname = catalog[catalogname]['named'][name] @@ -801,11 +801,11 @@ def getItemDetail(name, cataloglist, vers=''): versionlist.sort(compare_version_keys) for versionkey in versionlist: indexlist.extend(itemsmatchingname[versionkey]) - + elif vers in itemsmatchingname: # get the specific requested version indexlist = itemsmatchingname[vers] - + munkicommon.display_debug1( "Considering %s items with name %s from catalog %s" % (len(indexlist), name, catalogname)) @@ -827,7 +827,7 @@ def getItemDetail(name, cataloglist, vers=''): version.LooseVersion(min_os_vers): # skip this one, go to the next continue - + if 'maximum_os_version' in item: max_os_vers = \ munkicommon.padVersionString( @@ -842,7 +842,7 @@ def getItemDetail(name, cataloglist, vers=''): version.LooseVersion(max_os_vers): # skip this one, go to the next continue - + if 'supported_architectures' in item: supported_arch_found = False munkicommon.display_debug1( @@ -856,23 +856,23 @@ def getItemDetail(name, cataloglist, vers=''): # this machine, so we can use it supported_arch_found = True break - + if not supported_arch_found: # we didn't find a supported architecture that # matches this machine continue - + # item name, version, minimum_os_version, and # supported_architecture are all OK munkicommon.display_debug1( "Found %s, version %s in catalog %s" % (item['name'], item['version'], catalogname)) return item - + # if we got this far, we didn't find it. munkicommon.display_debug1("Nothing found") return None - + def enoughDiskSpace(manifestitem_pl, installlist=None, uninstalling=False): """ @@ -901,9 +901,9 @@ def enoughDiskSpace(manifestitem_pl, installlist=None, uninstalling=False): installedsize = 0 if 'uninstaller_item_size' in manifestitem_pl: installeritemsize = int(manifestitem_pl['uninstaller_item_size']) - diskspaceneeded = (installeritemsize - alreadydownloadedsize + + diskspaceneeded = (installeritemsize - alreadydownloadedsize + installedsize + fudgefactor) - + # munkicommon.getAvailableDiskSpace() returns KB availablediskspace = munkicommon.getAvailableDiskSpace() if installlist: @@ -912,7 +912,7 @@ def enoughDiskSpace(manifestitem_pl, installlist=None, uninstalling=False): if item.get('installer_item'): availablediskspace = availablediskspace - \ item.get('installed_size',0) - + if availablediskspace > diskspaceneeded: return True else: @@ -946,22 +946,22 @@ def isInstalled(item_pl): if itemtype == 'application': if compareApplicationVersion(item) in (-1, 0): return False - + if itemtype == 'bundle': if compareBundleVersion(item) in (-1, 0): # not there or older return False - + if itemtype == 'plist': if comparePlistVersion(item) in (-1, 0): # not there or older return False - + if itemtype == 'file': if filesystemItemExists(item) == 0 : # not there, or wrong checksum return False - + # if there is no 'installs' key, then we'll use receipt info # to determine install status. elif 'receipts' in item_pl: @@ -970,7 +970,7 @@ def isInstalled(item_pl): if compareReceiptVersion(item) in (-1, 0): # not there or older return False - + # if we got this far, we passed all the tests, so the item # must be installed (or we don't have enough info...) return True @@ -985,7 +985,7 @@ def evidenceThisIsInstalled(item_pl): So this isn't the same as isInstalled() """ global pkgdata - + if item_pl.get('uninstall_method') == "removepackages": # we're supposed to use receipt info to remove # this, so we should check for relevent receipts @@ -1008,12 +1008,74 @@ def evidenceThisIsInstalled(item_pl): foundallinstallitems = False if foundallinstallitems: return True - + # if we got this far, we failed all the tests, so the item # must not be installed (or we dont't have the right info...) return False +def verifySoftwarePackageIntegrity(manifestitem, file_path, item_pl, item_key): + ''' + Verifies the integrity of the given software package. + + The feature can be controlled through the PackageVerificationMode key in + the ManagedInstalls.plist. Following modes currently exist: + none: No integrity check is performed. + hash: Integrity check is performed by calcualting a SHA-256 hash of + the given file and comparing it against the reference value in + catalog. Only applies for package plists that contain the item_key; + for packages without the item_key, verifcation always returns True. + hash_strict: Same as hash, but returns False for package plists that + do not contain the item_key. + + Args: + mainfestitem: The name of the manifest item. + file_path: The file to check integrity on. + item_pl: The item plist which contains the reference values. + item_key: The name of the key in plist which contains the hash. + + Returns: + True if the package integrity could be validated. Otherwise, False. + ''' + mode = munkicommon.pref('PackageVerificationMode') + if not mode: + munkicommon.display_warning("The PackageVerificationMode key is " + "missing in the ManagedInstalls.plist. Please add it.") + munkicommon.display_warning("Package integrity checking is disabled.") + return True + elif mode.lower() == 'none': + munkicommon.display_warning("Package integrity checking is disabled.") + return True + elif mode.lower() == 'hash' or mode.lower() == 'hash_strict': + if item_key in item_pl: + item_hash = item_pl[item_key] + if (item_hash is not 'N/A' and + item_hash == munkicommon.getsha256hash(file_path)): + return True + else: + munkicommon.display_error( + "Hash value integrity check for %s failed." % manifestitem) + return False + else: + if mode.lower() == 'hash_strict': + munkicommon.display_error( + "Reference hash value for %s is missing in catalog." + % manifestitem) + return False + else: + munkicommon.display_warning( + "Package integrity checking is disabled for %s." + % manifestitem) + return True + + else: + munkicommon.display_error( + "The PackageVerificationMode in the ManagedInstalls.plist has an " + "illegal value: %s" % munkicommon.pref('PackageVerificationMode')) + + return False + + def getAutoRemovalItems(installinfo, cataloglist): ''' Gets a list of items marked for automatic removal from the catalogs @@ -1036,7 +1098,7 @@ def getAutoRemovalItems(installinfo, cataloglist): if item not in already_processed_names] #print "Auto removal names: ", autoremovalnames return autoremovalnames - + def lookForUpdates(manifestitem, cataloglist): """ @@ -1057,7 +1119,7 @@ def lookForUpdates(manifestitem, cataloglist): if not catalogname in catalog.keys(): # in case the list refers to a non-existant catalog continue - + updaters = catalog[catalogname]['updaters'] # list comprehension coming up... update_items = [catalogitem['name'] @@ -1067,14 +1129,14 @@ def lookForUpdates(manifestitem, cataloglist): catalogitem.get('update_for',[]))] if update_items: update_list.extend(update_items) - + if update_list: # make sure the list has only unique items: update_list = list(set(update_list)) - + return update_list - - + + def processOptionalInstall(manifestitem, cataloglist, installinfo): ''' Process an optional install item to see if it should be added to @@ -1082,7 +1144,7 @@ def processOptionalInstall(manifestitem, cataloglist, installinfo): ''' manifestitemname = os.path.split(manifestitem)[1] item_pl = getItemDetail(manifestitem, cataloglist) - + if not item_pl: munkicommon.display_warning( "Could not process item %s for optional install: " % manifestitem) @@ -1100,7 +1162,7 @@ def processOptionalInstall(manifestitem, cataloglist, installinfo): munkicommon.display_debug1( "%s has already been processed for removal." % manifestitemname) return - # check to see if item (any version) is already in the + # check to see if item (any version) is already in the # optional_install list: for item in installinfo['optional_installs']: if item_pl['name'] == item['name']: @@ -1108,7 +1170,7 @@ def processOptionalInstall(manifestitem, cataloglist, installinfo): "%s has already been processed for optional install." % manifestitemname) return - # if we get to this point we can add this item + # if we get to this point we can add this item # to the list of optional installs iteminfo = {} iteminfo["name"] = item_pl.get('name', '') @@ -1119,7 +1181,7 @@ def processOptionalInstall(manifestitem, cataloglist, installinfo): iteminfo['installed'] = isInstalled(item_pl) iteminfo['uninstallable'] = item_pl.get('uninstallable', False) if not iteminfo['installed']: - iteminfo["installer_item_size"] = item_pl.get('installer_item_size', + iteminfo["installer_item_size"] = item_pl.get('installer_item_size', 0) iteminfo["installed_size"] = item_pl.get('installer_item_size', iteminfo["installer_item_size"]) @@ -1127,9 +1189,9 @@ def processOptionalInstall(manifestitem, cataloglist, installinfo): installinfo.get('managed_installs',[])): iteminfo['note'] = \ "Insufficient disk space to download and install." - + installinfo['optional_installs'].append(iteminfo) - + def processInstall(manifestitem, cataloglist, installinfo): """ @@ -1141,16 +1203,16 @@ def processInstall(manifestitem, cataloglist, installinfo): Returns a boolean; when processing dependencies, a false return will stop the installation of a dependent item """ - + if munkicommon.munkistatusoutput: # reset progress indicator and detail field munkistatus.percent("-1") munkistatus.detail('') - + manifestitemname = os.path.split(manifestitem)[1] #munkicommon.display_info("Getting detail on %s..." % manifestitemname) item_pl = getItemDetail(manifestitem, cataloglist) - + if not item_pl: munkicommon.display_warning( "Could not process item %s for install: " % manifestitem) @@ -1158,17 +1220,17 @@ def processInstall(manifestitem, cataloglist, installinfo): "No pkginfo for %s found in catalogs: %s" % (manifestitem, ', '.join(cataloglist))) return False - + # check to see if item is already in the installlist: - if isItemInInstallInfo(item_pl, + if isItemInInstallInfo(item_pl, installinfo['managed_installs'], item_pl.get('version')): munkicommon.display_debug1( "%s has already been processed for install." % manifestitemname) return True - + # check dependencies dependenciesMet = True - + # there are three kinds of dependencies/relationships. # # 'requires' are prerequistes: @@ -1209,7 +1271,7 @@ def processInstall(manifestitem, cataloglist, installinfo): # With 'requires' and 'update_for' you can completely replace the # functionality of 'modifies', plus do more, so 'modifies' is on its way # out. - + if 'requires' in item_pl: dependencies = item_pl['requires'] for item in dependencies: @@ -1220,7 +1282,7 @@ def processInstall(manifestitem, cataloglist, installinfo): success = processInstall(item, cataloglist, installinfo) if not success: dependenciesMet = False - + if 'modifies' in item_pl: dependencies = item_pl['modifies'] if type(dependencies) == list: @@ -1229,7 +1291,7 @@ def processInstall(manifestitem, cataloglist, installinfo): item = dependencies[0] else: item = dependencies - + munkicommon.display_detail("%s-%s modifies %s. " "Getting info on %s..." % (item_pl.get('name', manifestitemname), @@ -1237,13 +1299,13 @@ def processInstall(manifestitem, cataloglist, installinfo): success = processInstall(item, cataloglist, installinfo) if not success: dependenciesMet = False - + if not dependenciesMet: munkicommon.display_warning("Didn't attempt to install %s " "because could not resolve all " "dependencies." % manifestitemname) return False - + iteminfo = {} iteminfo["name"] = item_pl.get('name', '') iteminfo["manifestitem"] = manifestitemname @@ -1251,7 +1313,7 @@ def processInstall(manifestitem, cataloglist, installinfo): 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 not isInstalled(item_pl): munkicommon.display_detail("Need to install %s" % manifestitemname) # check to see if there is enough free space to download and install @@ -1262,46 +1324,60 @@ def processInstall(manifestitem, cataloglist, installinfo): "Insufficient disk space to download and install" installinfo['managed_installs'].append(iteminfo) return False - + if 'installer_item_location' in item_pl: location = item_pl['installer_item_location'] - if download_installeritem(location): + (download_successful, download_path) = download_installeritem( + location) + if download_successful: filename = os.path.split(location)[1] - # required keys - iteminfo['installer_item'] = filename - iteminfo['installed'] = False - iteminfo["version_to_install"] = \ - item_pl.get('version',"UNKNOWN") - iteminfo['description'] = item_pl.get('description','') - iteminfo['display_name'] = item_pl.get('display_name','') - # optional keys - optional_keys = ['suppress_bundle_relocation', - 'installer_choices_xml', - 'adobe_install_info', - 'RestartAction', - 'installer_type', - 'adobe_package_name', - 'package_path', - 'items_to_copy', # used with copy_from_dmg - 'copy_local'] # used with Adobe CS5 Updaters - for key in optional_keys: - if key in item_pl: - iteminfo[key] = item_pl[key] - - installinfo['managed_installs'].append(iteminfo) - if nameAndVersion(manifestitemname)[1] == '': - # didn't specify a specific version, so - # now look for updates for this item - update_list = lookForUpdates(iteminfo["name"], - cataloglist) - for update_item in update_list: - # call processInstall recursively so we get the latest - # version and and dependencies - is_or_will_be_installed = processInstall(update_item, - cataloglist, - installinfo) - return True + if verifySoftwarePackageIntegrity( + manifestitem, download_path, item_pl, + 'installer_item_hash'): + # required keys + iteminfo['installer_item'] = filename + iteminfo['installed'] = False + iteminfo["version_to_install"] = \ + item_pl.get('version',"UNKNOWN") + iteminfo['description'] = item_pl.get('description','') + iteminfo['display_name'] = (item_pl.get('display_name','')) + # optional keys + optional_keys = ['suppress_bundle_relocation', + 'installer_choices_xml', + 'adobe_install_info', + 'RestartAction', + 'installer_type', + 'adobe_package_name', + 'package_path', + 'items_to_copy', # used w/ copy_from_dmg + 'copy_local'] # used w/ Adobe CS5 Updaters + for key in optional_keys: + if key in item_pl: + iteminfo[key] = item_pl[key] + + installinfo['managed_installs'].append(iteminfo) + if nameAndVersion(manifestitemname)[1] == '': + # didn't specify a specific version, so + # now look for updates for this item + update_list = lookForUpdates(iteminfo["name"], + cataloglist) + for update_item in update_list: + # call processInstall recursively so we get the + # latest version and dependencies + is_or_will_be_installed = processInstall( + update_item, cataloglist, installinfo) + return True + else: + munkicommon.display_warning( + "Can't install %s because the integrity check failed." + % manifestitem) + iteminfo['installed'] = False + iteminfo['note'] = "Integrity check failed" + installinfo['managed_installs'].append(iteminfo) + return False else: + munkicommon.display_warning( + "Download of %s failed." % manifestitem) iteminfo['installed'] = False iteminfo['note'] = "Download failed" installinfo['managed_installs'].append(iteminfo) @@ -1315,7 +1391,7 @@ def processInstall(manifestitem, cataloglist, installinfo): installinfo['managed_installs'].append(iteminfo) return False else: - iteminfo["installed"] = True + iteminfo['installed'] = True #iteminfo["installed_version"] = getInstalledVersion(pl) installinfo['managed_installs'].append(iteminfo) # remove included version number if any @@ -1334,7 +1410,7 @@ def processInstall(manifestitem, cataloglist, installinfo): cataloglist, installinfo) return True - + def processManifestForOptionalInstalls(manifestpath, installinfo, parentcatalogs=None): @@ -1348,7 +1424,7 @@ def processManifestForOptionalInstalls(manifestpath, installinfo, getCatalogs(cataloglist) elif parentcatalogs: cataloglist = parentcatalogs - + if cataloglist: nestedmanifests = getManifestValueForKey(manifestpath, "included_manifests") @@ -1364,7 +1440,7 @@ def processManifestForOptionalInstalls(manifestpath, installinfo, processManifestForOptionalInstalls(nestedmanifestpath, installinfo, cataloglist) - + optionalinstallitems = getManifestValueForKey(manifestpath, "optional_installs") if optionalinstallitems: @@ -1372,7 +1448,7 @@ def processManifestForOptionalInstalls(manifestpath, installinfo, if munkicommon.stopRequested(): return {} processOptionalInstall(item, cataloglist, installinfo) - + def processManifestForInstalls(manifestpath, installinfo, parentcatalogs=None): @@ -1386,7 +1462,7 @@ def processManifestForInstalls(manifestpath, installinfo, getCatalogs(cataloglist) elif parentcatalogs: cataloglist = parentcatalogs - + if cataloglist: nestedmanifests = getManifestValueForKey(manifestpath, "included_manifests") @@ -1401,7 +1477,7 @@ def processManifestForInstalls(manifestpath, installinfo, if nestedmanifestpath: processManifestForInstalls(nestedmanifestpath, installinfo, cataloglist) - + installitems = getManifestValueForKey(manifestpath, "managed_installs") if installitems: @@ -1410,13 +1486,13 @@ def processManifestForInstalls(manifestpath, installinfo, return {} is_or_will_be_installed = processInstall(item, cataloglist, installinfo) - + else: munkicommon.display_warning("Manifest %s has no 'catalogs'" % manifestpath) - + return installinfo - + def getReceiptsToRemove(item): '''Returns a list of receipts to remove for item''' @@ -1424,29 +1500,29 @@ def getReceiptsToRemove(item): if name in pkgdata['receipts_for_name']: return pkgdata['receipts_for_name'][name] return [] - + def processRemoval(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. """ manifestitemname_withversion = os.path.split(manifestitem)[1] - + munkicommon.display_detail("Processing manifest item %s..." % manifestitemname_withversion) (manifestitemname, includedversion) = nameAndVersion( @@ -1461,12 +1537,12 @@ def processRemoval(manifestitem, cataloglist, installinfo): else: # get all items matching the name provided infoitems = getAllItemsWithName(manifestitemname, cataloglist) - + if not infoitems: munkicommon.display_warning("Could not get information for %s" % manifestitemname_withversion) return False - + for item in infoitems: # check to see if item is already in the installlist, # if so, that's bad - it means it's scheduled to be installed @@ -1479,7 +1555,7 @@ def processRemoval(manifestitem, cataloglist, installinfo): "install." % manifestitemname_withversion) return False - + for item in infoitems: # check to see if item is already in the removallist: if isItemInInstallInfo(item, installinfo['removals']): @@ -1487,13 +1563,13 @@ def processRemoval(manifestitem, cataloglist, installinfo): "%s has already been processed for removal." % manifestitemname_withversion) return True - + installEvidence = False for item in infoitems: if evidenceThisIsInstalled(item): installEvidence = True break - + if not installEvidence: munkicommon.display_detail("%s doesn't appear to be installed." % manifestitemname_withversion) @@ -1502,7 +1578,7 @@ def processRemoval(manifestitem, cataloglist, installinfo): iteminfo["installed"] = False installinfo['removals'].append(iteminfo) return True - + # if we get here, installEvidence is true, and item # holds the item we found install evidence for, so we # should use that item to do the removal @@ -1529,14 +1605,14 @@ def processRemoval(manifestitem, cataloglist, installinfo): 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 munkicommon.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 @@ -1551,12 +1627,12 @@ def processRemoval(manifestitem, cataloglist, installinfo): dependentitemsremoved = True ManagedInstallDir = munkicommon.pref('ManagedInstallDir') catalogsdir = os.path.join(ManagedInstallDir, 'catalogs') - + # make a list of the name and aliases of the current uninstall_item uninstall_item_names = [] uninstall_item_names.append(uninstall_item.get('name')) #uninstall_item_names.extend(uninstall_item.get('aliases',[])) - + processednamesandaliases = [] for catalogname in cataloglist: localcatalog = os.path.join(catalogsdir, catalogname) @@ -1589,7 +1665,7 @@ def processRemoval(manifestitem, cataloglist, installinfo): break # record these names so we don't process them again processednamesandaliases.extend(namesandaliases) - + # if this package modifies another one, and we're using removepackages, # we must remove it as well # if we're using another removal method, we just have to hope that @@ -1607,13 +1683,13 @@ def processRemoval(manifestitem, cataloglist, installinfo): installinfo) if not success: dependentitemsremoved = False - + if not dependentitemsremoved: munkicommon.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', '') @@ -1647,7 +1723,7 @@ def processRemoval(manifestitem, cataloglist, installinfo): munkicommon.display_warning("could not find unique packages to " "remove for %s" % iteminfo["name"]) return False - + iteminfo["uninstall_method"] = uninstallmethod if uninstallmethod.startswith("Adobe"): if 'adobe_install_info' in item: @@ -1659,11 +1735,22 @@ def processRemoval(manifestitem, cataloglist, installinfo): location = uninstall_item['installer_item_location'] if not enoughDiskSpace(uninstall_item, uninstalling=True): return False - if download_installeritem(location): - filename = os.path.split(location)[1] - iteminfo['uninstaller_item'] = filename - iteminfo['adobe_package_name'] = \ - uninstall_item.get('adobe_package_name','') + + (download_successful, download_path) = ( + download_installeritem(location)) + if download_successful: + if verifySoftwarePackageIntegrity( + iteminfo['name'], download_path, item_pl, + 'uninstaller_item_hash'): + filename = os.path.split(location)[1] + iteminfo['uninstaller_item'] = filename + iteminfo['adobe_package_name'] = \ + uninstall_item.get('adobe_package_name','') + else: + munkicommon.display_warning( + "Can't uinstall %s because the integrity check failed." + % iteminfo['name']) + return False else: munkicommon.display_warning("Failed to download the " "uninstaller for %s" @@ -1674,7 +1761,7 @@ def processRemoval(manifestitem, cataloglist, installinfo): elif uninstallmethod == "remove_app": if uninstall_item.get('installs', None): iteminfo['remove_app_info'] = uninstall_item['installs'][0] - + # before we add this removal to the list, # check for installed updates and add them to the # removal list as well: @@ -1683,7 +1770,7 @@ def processRemoval(manifestitem, cataloglist, installinfo): # call us recursively... is_or_will_be_removed = processRemoval(update_item, cataloglist, installinfo) - + # finish recording info for this removal iteminfo["installed"] = True iteminfo["installed_version"] = uninstall_item.get('version') @@ -1694,9 +1781,9 @@ def processRemoval(manifestitem, cataloglist, installinfo): "Removal of %s added to ManagedInstaller tasks." % manifestitemname_withversion) return True - -def processManifestForRemovals(manifestpath, installinfo, + +def processManifestForRemovals(manifestpath, installinfo, parentcatalogs=None): """ Processes manifests for removals. Can be recursive if manifests include @@ -1708,7 +1795,7 @@ def processManifestForRemovals(manifestpath, installinfo, getCatalogs(cataloglist) elif parentcatalogs: cataloglist = parentcatalogs - + if cataloglist: nestedmanifests = getManifestValueForKey(manifestpath, "included_manifests") @@ -1723,7 +1810,7 @@ def processManifestForRemovals(manifestpath, installinfo, if nestedmanifestpath: processManifestForRemovals(nestedmanifestpath, installinfo, cataloglist) - + autoremovalitems = getAutoRemovalItems(installinfo, cataloglist) explicitremovalitems = getManifestValueForKey(manifestpath, "managed_uninstalls") or [] @@ -1734,7 +1821,7 @@ def processManifestForRemovals(manifestpath, installinfo, return {} is_or_will_be_removed = processRemoval(item, cataloglist, installinfo) - + else: munkicommon.display_warning("Manifest %s has no 'catalogs'" % manifestpath) @@ -1751,7 +1838,7 @@ def getManifestValueForKey(manifestpath, keyname): return plist[keyname] else: return None - + def getCatalogs(cataloglist): """ @@ -1766,7 +1853,7 @@ def getCatalogs(cataloglist): munkicommon.display_debug2("Catalog base URL is: %s" % catalogbaseurl) catalog_dir = os.path.join(munkicommon.pref('ManagedInstallDir'), "catalogs") - + for catalogname in cataloglist: if not catalogname in catalog: catalogurl = catalogbaseurl + urllib2.quote(catalogname) @@ -1812,7 +1899,7 @@ def getmanifest(partialurl, suppress_errors=False): manifestbaseurl = manifestbaseurl + "/" manifest_dir = os.path.join(munkicommon.pref('ManagedInstallDir'), "manifests") - + if partialurl.startswith("http://") or partialurl.startswith("https://"): # then it's really a request for the client's primary manifest manifesturl = partialurl @@ -1822,10 +1909,10 @@ def getmanifest(partialurl, suppress_errors=False): # request for nested manifest manifestname = os.path.split(partialurl)[1] manifesturl = manifestbaseurl + urllib2.quote(partialurl) - + if manifestname in manifests: return manifests[manifestname] - + munkicommon.display_debug2("Manifest base URL is: %s" % manifestbaseurl) munkicommon.display_detail("Getting manifest %s..." % partialurl) manifestpath = os.path.join(manifest_dir, manifestname) @@ -1840,7 +1927,7 @@ def getmanifest(partialurl, suppress_errors=False): partialurl) munkicommon.display_error(err) return None - + if munkicommon.validPlist(newmanifest): # record it for future access manifests[manifestname] = newmanifest @@ -1865,9 +1952,9 @@ def getPrimaryManifest(alternate_id): if not manifesturl.endswith('?') and not manifesturl.endswith('/'): manifesturl = manifesturl + "/" munkicommon.display_debug2("Manifest base URL is: %s" % manifesturl) - + clientidentifier = alternate_id or munkicommon.pref('ClientIdentifier') - + if not alternate_id and munkicommon.pref('UseClientCertificate') and \ munkicommon.pref('UseClientCertificateCNAsClientIdentifier'): # we're to use the client cert CN as the clientidentifier @@ -1887,7 +1974,7 @@ def getPrimaryManifest(alternate_id): fileobj.close() x509 = load_certificate(FILETYPE_PEM, data) clientidentifier = x509.get_subject().commonName - + try: if not clientidentifier: # no client identifier specified, so use the hostname @@ -1910,7 +1997,7 @@ def getPrimaryManifest(alternate_id): munkicommon.display_detail("Request failed. " + "Trying %s..." % clientidentifier) - + if not manifest: manifest = getmanifest(manifesturl + urllib2.quote(clientidentifier)) @@ -1937,7 +2024,7 @@ def checkServer(url): port = 443 else: return False - + # get rid of any embedded username/password netlocparts = netloc.split("@") netloc = netlocparts[-1] @@ -1999,14 +2086,14 @@ def curl(url, destinationpath, onlyifnewer=False, etag=None, resume=False, interrupted download. You'll get an error if the existing file is complete; if the file has changed since the first download attempt, you'll get a mess.""" - + header = {} header['http_result_code'] = "000" header['http_result_description'] = "" - + curldirectivepath = os.path.join(munkicommon.tmpdir,"curl_temp") tempdownloadpath = destinationpath + ".download" - + # we're writing all the curl options to a file and passing that to # curl so we avoid the problem of URLs showing up in a process listing try: @@ -2016,10 +2103,10 @@ def curl(url, destinationpath, onlyifnewer=False, etag=None, resume=False, print >> fileobj, "no-buffer" # don't buffer output print >> fileobj, "fail" # throw error if download fails print >> fileobj, "dump-header -" # dump headers to stdout - print >> fileobj, "speed-time = 30" # give up if too slow d/l + print >> fileobj, "speed-time = 30" # give up if too slow d/l print >> fileobj, 'output = "%s"' % tempdownloadpath print >> fileobj, 'url = "%s"' % url - + if cacert: if not os.path.isfile(cacert): raise CurlError(-1, "No CA cert at %s" % cacert) @@ -2039,34 +2126,34 @@ def curl(url, destinationpath, onlyifnewer=False, etag=None, resume=False, if os.path.exists(tempdownloadpath) and resume: # let's try to resume this download print >> fileobj, 'continue-at -' - + if os.path.exists(destinationpath): if etag: escaped_etag = etag.replace('"','\\"') - print >> fileobj, ('header = "If-None-Match: %s"' + print >> fileobj, ('header = "If-None-Match: %s"' % escaped_etag) elif onlyifnewer: print >> fileobj, 'time-cond = "%s"' % destinationpath else: os.remove(destinationpath) - + fileobj.close() except: raise CurlError(-5, "Error writing curl directive") - + cmd = ['/usr/bin/curl', '-q', # don't read .curlrc file '--config', # use config file curldirectivepath] - - proc = subprocess.Popen(cmd, shell=False, bufsize=1, + + proc = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - + targetsize = 0 downloadedpercent = -1 donewithheaders = False - + while True: if not donewithheaders: info = proc.stdout.readline().strip("\r\n") @@ -2096,7 +2183,7 @@ def curl(url, destinationpath, onlyifnewer=False, etag=None, resume=False, targetsize = int(contentrange.split("/")[1]) except (ValueError, TypeError): targetsize = 0 - + if message and header['http_result_code'] != "304": if message: # log always, display if verbose is 2 or more @@ -2104,7 +2191,7 @@ def curl(url, destinationpath, onlyifnewer=False, etag=None, resume=False, if munkicommon.munkistatusoutput: # send to detail field on MunkiStatus munkistatus.detail(message) - + elif targetsize and header['http_result_code'].startswith('2'): # display progress if we get a 2xx result code if os.path.exists(tempdownloadpath): @@ -2116,10 +2203,10 @@ def curl(url, destinationpath, onlyifnewer=False, etag=None, resume=False, downloadedpercent = percent munkicommon.display_percent_done(downloadedpercent, 100) time.sleep(0.1) - + if (proc.poll() != None): break - + retcode = proc.poll() if retcode: curlerr = proc.stderr.read().rstrip('\n').split(None, 2)[2] @@ -2153,7 +2240,7 @@ def curl(url, destinationpath, onlyifnewer=False, etag=None, resume=False, def getHTTPfileIfChangedAtomically(url, destinationpath, message=None, resume=False): - '''Gets file from HTTP URL, checking first to see if it has changed on the + '''Gets file from HTTP URL, checking first to see if it has changed on the server.''' ManagedInstallDir = munkicommon.pref('ManagedInstallDir') # get server CA cert if it exists so we can verify the munki server @@ -2171,7 +2258,7 @@ def getHTTPfileIfChangedAtomically(url, destinationpath, ca_cert_path = os.path.join(ManagedInstallDir, "certs", "ca.pem") if not os.path.exists(ca_cert_path): ca_cert_path = None - + client_cert_path = None client_key_path = None # get client cert if it exists @@ -2184,7 +2271,7 @@ def getHTTPfileIfChangedAtomically(url, destinationpath, name) if os.path.exists(client_cert_path): break - + etag = None getonlyifnewer = False if os.path.exists(destinationpath): @@ -2194,7 +2281,7 @@ def getHTTPfileIfChangedAtomically(url, destinationpath, getonlyifnewer = False etag = xattr.getxattr(destinationpath, "com.googlecode.munki.etag") - + try: header = curl(url, destinationpath, @@ -2233,13 +2320,13 @@ def getHTTPfileIfChangedAtomically(url, destinationpath, # store etag in extended attribute for future use xattr.setxattr(destinationpath, "com.googlecode.munki.etag", header['etag']) - + return destinationpath, err - + def getMachineFacts(): global machine - + machine['hostname'] = os.uname()[1] machine['arch'] = os.uname()[4] cmd = ['/usr/bin/sw_vers', '-productVersion'] @@ -2260,23 +2347,23 @@ def check(client_id=''): 0 if there are no available updates, and -1 if there were errors.''' getMachineFacts() munkicommon.report['MachineInfo'] = machine - + ManagedInstallDir = munkicommon.pref('ManagedInstallDir') - + if munkicommon.munkistatusoutput: munkistatus.activate() munkistatus.message("Checking for available updates...") munkistatus.detail("") munkistatus.percent("-1") - + munkicommon.log("### Beginning managed software check ###") - + mainmanifestpath = getPrimaryManifest(client_id) if munkicommon.stopRequested(): return 0 - + installinfo = {} - + if mainmanifestpath: # initialize our installinfo record installinfo['managed_installs'] = [] @@ -2287,24 +2374,24 @@ def check(client_id=''): installinfo) if munkicommon.stopRequested(): return 0 - + if munkicommon.munkistatusoutput: # reset progress indicator and detail field munkistatus.message("Checking for additional changes...") munkistatus.percent("-1") munkistatus.detail('') - + # now generate a list of items to be uninstalled munkicommon.display_detail("**Checking for removals**") processManifestForRemovals(mainmanifestpath, installinfo) if munkicommon.stopRequested(): return 0 - + # build list of optional installs processManifestForOptionalInstalls(mainmanifestpath, installinfo) if munkicommon.stopRequested(): return 0 - + # now process any self-serve choices usermanifest = "/Users/Shared/.SelfServeManifest" selfservemanifest = os.path.join(ManagedInstallDir, "manifests", @@ -2323,7 +2410,7 @@ def check(client_id=''): pass except FoundationPlist.FoundationPlistException: pass - + if os.path.exists(selfservemanifest): # use catalogs from main manifest for self-serve manifest cataloglist = getManifestValueForKey(mainmanifestpath, 'catalogs') @@ -2332,15 +2419,15 @@ def check(client_id=''): installinfo, cataloglist) processManifestForRemovals(selfservemanifest, installinfo, cataloglist) - + # update optional installs with info from self-serve manifest for item in installinfo['optional_installs']: - if isItemInInstallInfo(item, + if isItemInInstallInfo(item, installinfo['managed_installs']): item['will_be_installed'] = True elif isItemInInstallInfo(item, installinfo['removals']): item['will_be_removed'] = True - + # filter managed_installs to get items already installed installed_items = [item for item in installinfo['managed_installs'] @@ -2352,10 +2439,10 @@ def check(client_id=''): if item.get('installed') == False and not item.get('installer_item')] # filter removals to get items already removed (or never installed) - removed_items = [item.get('manifestitem') + removed_items = [item.get('manifestitem') for item in installinfo['removals'] if item.get('installed') == False] - + if os.path.exists(selfservemanifest): # for any item in the managed_uninstalls in the self-serve # manifest that is not installed, we should remove it from @@ -2372,7 +2459,7 @@ def check(client_id=''): FoundationPlist.writePlist(plist, selfservemanifest) except FoundationPlist.FoundationPlistException: pass - + # filter managed_installs and removals lists # so they have only items that need action installinfo['managed_installs'] = \ @@ -2381,13 +2468,13 @@ def check(client_id=''): installinfo['removals'] = \ [item for item in installinfo['removals'] if item.get('installed')] - + munkicommon.report['ManagedInstalls'] = installed_items munkicommon.report['ProblemInstalls'] = problem_items munkicommon.report['RemovedItems'] = removed_items munkicommon.report['ItemsToInstall'] = installinfo['managed_installs'] munkicommon.report['ItemsToRemove'] = installinfo['removals'] - + # clean up cache dir # remove any item in the cache that isn't scheduled # to be used for an install or removal @@ -2421,7 +2508,7 @@ def check(client_id=''): elif item not in cache_list: munkicommon.display_detail("Removing %s from cache" % item) os.unlink(os.path.join(cachedir, item)) - + # write out install list so our installer # can use it to install things in the right order installinfochanged = True @@ -2451,11 +2538,11 @@ def check(client_id=''): installinfo.get('managed_installs', []) munkicommon.report['ItemsToRemove'] = \ installinfo.get('removals', []) - - + + installcount = len(installinfo.get("managed_installs", [])) removalcount = len(installinfo.get("removals", [])) - + munkicommon.log("") if installcount: munkicommon.display_info( @@ -2474,7 +2561,7 @@ def check(client_id=''): if item.get('RestartAction') == 'RequireLogout': munkicommon.display_info(" *Logout required") munkicommon.report['LogoutRequired'] = True - + if removalcount: munkicommon.display_info("The following items will be removed:") for item in installinfo.get('removals', []): @@ -2487,19 +2574,19 @@ def check(client_id=''): if item.get('RestartAction') == 'RequireLogout': munkicommon.display_info(" *Logout required") munkicommon.report['LogoutRequired'] = True - + if installcount == 0 and removalcount == 0: munkicommon.display_info( "No changes to managed software are available.") - + munkicommon.savereport() munkicommon.log("### End managed software check ###") - + if installcount or removalcount: return 1 else: return 0 - + def main(): '''Placeholder'''