#!/usr/bin/python # encoding: utf-8 # # Copyright 2009 Greg Neagle. # # 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. # See the License for the specific language governing permissions and # limitations under the License. """ updatecheck Created by Greg Neagle on 2008-11-13. """ #standard libs import sys import os import subprocess from distutils import version import urllib2 import urlparse import httplib import hashlib import datetime import time import calendar import socket #our lib import munkicommon import munkistatus import FoundationPlist def reporterrors(): # just a placeholder right now; # this needs to be expanded to support error reporting # via email and HTTP CGI. # (and maybe moved to a library module so the installer # can use it, too.) managedinstallprefs = munkicommon.prefs() clientidentifier = managedinstallprefs.get('ClientIdentifier','') #alternate_id = option_id hostname = os.uname()[1] print "installcheck errors %s:" % datetime.datetime.now().ctime() print "Hostname: %s" % hostname print "Client identifier: %s" % clientidentifier #print "Alternate ID: %s" % alternate_id print "-----------------------------------------" print munkicommon.errors # global to hold our catalog DBs catalog = {} def makeCatalogDB(catalogitems): '''Takes an array of catalog items and builds some indexes so we can 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['name'] vers = item['version'] # 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']: if not alias in name_table: name_table[alias] = {} 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']: if 'packageid' in receipt: if not receipt['packageid'] in pkgid_table: pkgid_table[receipt['packageid']] = {} if not receipt['version'] in pkgid_table[receipt['packageid']]: pkgid_table[receipt['packageid']][receipt['version']] = [] pkgid_table[receipt['packageid']][receipt['version']].append(itemindex) pkgdb = {} pkgdb['named'] = name_table pkgdb['receipts'] = pkgid_table #pkgdb['installeritem_table'] = installeritem_table pkgdb['items'] = catalogitems return pkgdb def addPackageids(catalogitems, pkgid_table): ''' Adds packageids from each catalogitem to a dictionary ''' for item in catalogitems: name = item['name'] 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 getInstalledPackages(): """ Builds a dictionary of installed receipts and their version number """ installedpkgs = {} # Check /Library/Receipts receiptsdir = "/Library/Receipts" if os.path.exists(receiptsdir): installitems = os.listdir(receiptsdir) for item in installitems: if item.endswith(".pkg"): infoplist = os.path.join(receiptsdir, item, "Contents/Info.plist") if os.path.exists(infoplist): try: pl = FoundationPlist.readPlist(infoplist) pkgid = pl.get('CFBundleIdentifier') if not pkgid: # special case JAMF Composer packages pkgid = pl.get('Bundle identifier') if pkgid: thisversion = munkicommon.getExtendedVersion(os.path.join(receiptsdir, item)) if not pkgid in installedpkgs: installedpkgs[pkgid] = thisversion else: # pkgid is already in our list. There must be multiple receipts with the same pkgid. # in this case, we want the highest version number, since that's the one that's installed, # since presumably the newer package replaced the older storedversion = installedpkgs[pkgid] if version.LooseVersion(thisversion) > version.LooseVersion(storedversion): installedpkgs[pkgid] = thisversion except: pass # Now check new (Leopard and later) package database p = subprocess.Popen(["/usr/sbin/pkgutil", "--pkgs"], bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() if out: pkgs = out.split("\n") for pkg in pkgs: p = subprocess.Popen(["/usr/sbin/pkgutil", "--pkg-info-plist", pkg], bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() if out: pl = FoundationPlist.readPlistFromString(out) if "pkg-version" in pl: installedpkgs[pkg] = pl["pkg-version"] return installedpkgs # global pkgdata pkgdata = {} def analyzeInstalledPkgs(): global pkgdata managed_pkgids = {} for catalogname in catalog.keys(): catalogitems = catalog[catalogname]['items'] addPackageids(catalogitems, managed_pkgids) installedpkgs = getInstalledPackages() installed = [] partiallyinstalled = [] installedpkgsmatchedtoname = {} for name in managed_pkgids.keys(): somepkgsfound = False allpkgsfound = True for pkg in managed_pkgids[name]: if pkg in installedpkgs.keys(): somepkgsfound = True if not name in installedpkgsmatchedtoname: installedpkgsmatchedtoname[name] = [] installedpkgsmatchedtoname[name].append(pkg) else: allpkgsfound = False if allpkgsfound: 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 # just finding receipts that are shared with other items. for name in partiallyinstalled: # get a list of pkgs for this item that are installed pkgsforthisname = installedpkgsmatchedtoname[name] # now build a list of all the pkgs referred to by all the other # items that are either partially or entirely installed allotherpkgs = [] for othername in installed: allotherpkgs.extend(installedpkgsmatchedtoname[othername]) for othername in partiallyinstalled: if othername != name: allotherpkgs.extend(installedpkgsmatchedtoname[othername]) # use Python sets to find pkgs that are unique to this name uniquepkgs = list(set(pkgsforthisname) - set(allotherpkgs)) if uniquepkgs: installed.append(name) # build our reference table references = {} for name in installed: for pkg in installedpkgsmatchedtoname[name]: if not pkg in references: references[pkg] = [] references[pkg].append(name) pkgdata = {} pkgdata['receipts_for_name'] = installedpkgsmatchedtoname pkgdata['installed_names'] = installed pkgdata['pkg_references'] = references # appdict is a global so we don't call system_profiler more than once per session appdict = {} def getAppData(): """ Queries system_profiler and returns a dict of app info items """ global appdict if appdict == {}: munkicommon.display_debug1("Getting info on currently installed applications...") cmd = ['/usr/sbin/system_profiler', '-XML', 'SPApplicationsDataType'] p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (plist, err) = p.communicate() if p.returncode == 0: pl = FoundationPlist.readPlistFromString(plist) # top level is an array instead of a dict, so get dict spdict = pl[0] if '_items' in spdict: appdict = spdict['_items'] return appdict def getAppBundleID(path): """ Returns CFBundleIdentifier if available for application at path """ infopath = os.path.join(path, "Contents", "Info.plist") if os.path.exists(infopath): try: pl = FoundationPlist.readPlist(infopath) if 'CFBundleIdentifier' in pl: return pl['CFBundleIdentifier'] except: pass return None def compareVersions(thisvers, thatvers): """ Returns -1 if thisvers is older than thatvers Returns 1 if thisvers is the same as thatvers Returns 2 if thisvers is newer than thatvers """ thisvers = munkicommon.padVersionString(thisvers,5) thatvers = munkicommon.padVersionString(thatvers,5) if version.LooseVersion(thisvers) < version.LooseVersion(thatvers): return -1 elif version.LooseVersion(thisvers) == version.LooseVersion(thatvers): return 1 else: return 2 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 1 if the version is the same 2 if the version is newer -2 if there's an error in the input """ if 'path' in app and 'CFBundleShortVersionString' in 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 # any additional info, so we have to assume it's not installed. return 0 else: # no path, no name, no bundleid. Error! 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)) appinfo = [] appdata = getAppData() if appdata: for item in appdata: if 'path' in item: # in case we get items from other disks if not item['path'].startswith('/Volumes/'): if bundleid: if getAppBundleID(item['path']) == bundleid: appinfo.append(item) elif name: 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("\tName: \t %s" % item['_name'].encode("UTF-8")) if 'path' in item: munkicommon.display_debug2("\tPath: \t %s" % item['path'].encode("UTF-8")) munkicommon.display_debug2("\tCFBundleIdentifier: \t %s" % getAppBundleID(item['path'])) if 'version' in item: munkicommon.display_debug2("\tVersion: \t %s" % item['version'].encode("UTF-8")) if compareVersions(item['version'], versionstring) == 1: # version is the same return 1 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.") return -1 def compareBundleVersion(item): """ Returns 0 if the bundle isn't installed or doesn't have valid Info.plist -1 if it's older 1 if the version is the same 2 if the version is newer -2 if there's an error in the input """ if 'path' in item and 'CFBundleShortVersionString' in item: filepath = os.path.join(item['path'], 'Contents', 'Info.plist') vers = item['CFBundleShortVersionString'] else: munkicommon.display_error("Missing bundle 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 Info.plist found at %s" % filepath) return 0 try: pl = FoundationPlist.readPlist(filepath) except: munkicommon.display_debug1("\t%s may not be a plist!" % filepath) return 0 if 'CFBundleShortVersionString' in pl: installedvers = pl['CFBundleShortVersionString'] return compareVersions(installedvers, vers) elif 'CFBundleVersion' in pl: installedvers = pl['CFBundleVersion'] return compareVersions(installedvers, vers) else: munkicommon.display_debug1("\tNo version info in %s." % filepath) return 0 def comparePlistVersion(item): """ Gets the CFBundleShortVersionString from the plist at filepath and compares versions. Returns 0 if the plist isn't installed -1 if it's older 1 if the version is the same 2 if the version is newer -2 if there's an error in the input """ if 'path' in item and 'CFBundleShortVersionString' in item: filepath = item['path'] vers = item['CFBundleShortVersionString'] 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: pl = FoundationPlist.readPlist(filepath) except: munkicommon.display_debug1("\t%s may not be a plist!" % filepath) return 0 if 'CFBundleShortVersionString' in pl: installedvers = pl['CFBundleShortVersionString'] return compareVersions(installedvers, vers) elif 'CFBundleVersion' in pl: installedvers = pl['CFBundleVersion'] return compareVersions(installedvers, vers) else: munkicommon.display_debug1("\tNo version info in %s." % filepath) return 0 def getmd5hash(filename): if not os.path.isfile(filename): return "NOT A FILE" f = open(filename, 'rb') m = hashlib.md5() while 1: chunk = f.read(2**16) if not chunk: break m.update(chunk) f.close() return m.hexdigest() def filesystemItemExists(item): """ Checks to see if a filesystem item exists If item has m5checksum attribute, compares ondisk file's checksum """ if 'path' in item: filepath = item['path'] munkicommon.display_debug1("Checking existence of %s..." % filepath) if os.path.exists(filepath): munkicommon.display_debug2("\tExists.") if 'md5checksum' in item: storedchecksum = item['md5checksum'] ondiskchecksum = getmd5hash(filepath) munkicommon.display_debug2("Comparing checksums...") if storedchecksum == ondiskchecksum: munkicommon.display_debug2("Checksums match.") return 1 else: munkicommon.display_debug2("Checksums differ: expected %s, got %s" % (storedchecksum, ondiskchecksum)) return 0 return 1 else: munkicommon.display_debug2("\tDoes not exist.") return 0 else: munkicommon.display_error("No path specified for filesystem item.") return -2 def compareReceiptVersion(item): """ Determines if the given package is already installed. packageid is a 'com.apple.pkg.ServerAdminTools' style id Returns 0 if the receipt isn't present -1 if it's older 1 if the version is the same 2 if the version is newer -2 if there's an error in the input """ if 'packageid' in item and 'version' in item: pkgid = item['packageid'] vers = item['version'] else: print "Missing packageid or version info!" return -2 munkicommon.display_debug1("Looking for package %s, version %s" % (pkgid, vers)) installedvers = munkicommon.getInstalledPackageVersion(pkgid) if installedvers: return compareVersions(installedvers, vers) else: munkicommon.display_debug1("\tThis package is not currently installed.") return 0 def getInstalledVersion(pl): """ Attempts to determine the currently installed version of the item described by pl """ if 'receipts' in pl: for receipt in pl['receipts']: installedpkgvers = munkicommon.getInstalledPackageVersion(receipt['packageid']) munkicommon.display_debug2("Looking for %s, version %s" % (receipt['packageid'], receipt['version'])) if compareVersions(installedpkgvers, receipt['version']) == 2: # version is higher installedversion = "newer than %s" % pl['version'] return installedversion if compareVersions(installedpkgvers, receipt['version']) == -1: # version is lower installedversion = "older than %s" % pl['version'] return installedversion # if we get here all reciepts match return pl['version'] if 'installs' in pl: for install_item in pl['installs']: if install_item['type'] == 'application': name = install_item.get('CFBundleName') bundleid = install_item.get('CFBundleIdentifier') munkicommon.display_debug2("Looking for application %s, version %s" % (name, install_item.get('CFBundleIdentifier'))) try: # check default location for app filepath = os.path.join(install_item['path'], 'Contents', 'Info.plist') pl = FoundationPlist.readPlist(filepath) installedappvers = pl.get('CFBundleShortVersionString') except: # that didn't work, fall through to the slow way # using System Profiler appinfo = [] appdata = getAppData() if appdata: for ad_item in appdata: if bundleid: if 'path' in ad_item: if getAppBundleID(ad_item['path']) == bundleid: appinfo.append(ad_item) elif name: if '_name' in ad_item: if ad_item['_name'] == name: appinfo.append(ad_item) for ai_item in appinfo: if 'version' in ai_item: if compareVersions(ai_item['version'], maxversion) == 2: # version is higher maxversion = ai_item['version'] installedappvers = maxversion if compareVersions(installedappvers, install_item['CFBundleShortVersionString']) == 2: # version is higher installedversion = "newer than %s" % pl['version'] return installedversion if compareVersions(installedappvers, install_item['CFBundleShortVersionString']) == -1: # version is lower installedversion = "older than %s" % pl['version'] return installedversion # if we get here all app versions match return pl['version'] # if we fall through to here we have no idea what version we have return "UNKNOWN" def download_installeritem(location): """ Downloads a installer item. """ ManagedInstallDir = munkicommon.ManagedInstallDir() sw_repo_baseurl = munkicommon.SoftwareRepoURL() downloadbaseurl = sw_repo_baseurl + "/pkgs/" 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) # bump up verboseness so we get download percentage done feedback. # this is kind of a hack... oldverbose = munkicommon.verbose munkicommon.verbose = oldverbose + 1 dl_message = "Downloading %s from %s" % (pkgname, pkgurl) munkicommon.log(dl_message) dl_message = "Downloading %s..." % pkgname (path, err) = getHTTPfileIfNewerAtomically(pkgurl, destinationpath, message=dl_message) # set verboseness back. munkicommon.verbose = oldverbose if path: return True else: munkicommon.display_error("Could not download %s from server." % pkgname) munkicommon.display_error(err) return False def isItemInInstallInfo(manifestitem_pl, thelist, vers=''): """ Returns True if the manifest item has already been processed (it's in the list) and, optionally, the version is the same or greater. """ names = [] names.append(manifestitem_pl.get('name')) names.extend(manifestitem_pl.get('aliases',[])) for item in thelist: if item.get('name') in names: if not vers: return True if item.get('installed'): return True #if the version already processed is the same or greater, then we're good if compareVersions(item.get('version_to_install'), vers) in (1,2): return True return False def nameAndVersion(s): """ Splits a string into the name and version number. Name and version must be seperated with a hyphen ('-') or double hyphen ('--'). 'TextWrangler-2.3b1' becomes ('TextWrangler', '2.3b1') 'AdobePhotoshopCS3--11.2.1' becomes ('AdobePhotoshopCS3', '11.2.1') 'MicrosoftOffice2008-12.2.1' becomes ('MicrosoftOffice2008', '12.2.1') """ for delim in ('--', '-'): if s.count(delim) > 0: chunks = s.split(delim) vers = chunks.pop() name = delim.join(chunks) if vers[0] in "0123456789": return (name, vers) return (s, '') 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: # is name in the catalog name table? if name in catalog[catalogname]['named']: versionsmatchingname = catalog[catalogname]['named'][name] for vers in versionsmatchingname.keys(): if vers != 'latest': indexlist = catalog[catalogname]['named'][name][vers] for index in indexlist: thisitem = catalog[catalogname]['items'][index] if not thisitem in itemlist: munkicommon.display_debug1("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=''): """ Searches the catalogs in cataloglist for an item matching the given name and version. If no version is supplied, but the version is appended to the name ('TextWrangler--2.3.0.0.0') that version is used. If no version is given at all, the latest version is assumed. Returns a pkginfo item. """ def compare_version_keys(a, b): return cmp(version.LooseVersion(b), version.LooseVersion(a)) global catalog (name, includedversion) = nameAndVersion(name) if vers == '': if includedversion: vers = includedversion if vers: # make sure version is in 1.0.0.0.0 format vers = munkicommon.padVersionString(vers,5) else: vers = 'latest' munkicommon.display_debug1("Looking for detail for: %s, version %s..." % (name, vers)) for catalogname in cataloglist: # is name or alias in the catalog? if name in catalog[catalogname]['named']: itemsmatchingname = catalog[catalogname]['named'][name] indexlist = [] if vers == 'latest': # order all our items, latest first versionlist = itemsmatchingname.keys() 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)) for index in indexlist: item = catalog[catalogname]['items'][index] # we have an item whose name and version matches the request. # now check to see if it meets os and cpu requirements if 'minimum_os_version' in item: min_os_vers = munkicommon.padVersionString(item['minimum_os_version'],3) munkicommon.display_debug1("Considering item %s, version %s with minimum os version required %s" % (item['name'], item['version'], min_os_vers)) munkicommon.display_debug2("Our OS version is %s" % machine['os_vers']) if version.LooseVersion(machine['os_vers']) < version.LooseVersion(min_os_vers): # skip this one, go to the next continue if 'supported_architectures' in item: supported_arch_found = False munkicommon.display_debug1("Considering item %s, version %s with supported architectures: %s" % (item['name'], item['version'], item['supported_architectures'])) for arch in item['supported_architectures']: if arch == machine['arch']: # we found a supported architecture that matches # 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): """ Used to determine if there is enough disk space to be able to download and install the manifestitem """ # fudgefactor is set to 100MB fudgefactor = 100000 installeritemsize = 0 installedsize = 0 if 'installer_item_size' in manifestitem_pl: installeritemsize = manifestitem_pl['installer_item_size'] if 'installed_size' in manifestitem_pl: installedsize = manifestitem_pl['installed_size'] diskspaceneeded = (installeritemsize + installedsize + fudgefactor)/1024 availablediskspace = munkicommon.getAvailableDiskSpace()/1024 if availablediskspace > diskspaceneeded: return True else: munkicommon.display_info("There is insufficient disk space to download and install %s." % manifestitem_pl.get('name')) munkicommon.display_info(" %sMB needed; %sMB available" % (diskspaceneeded, availablediskspace)) return False def isInstalled(pl): """ Checks to see if the item described by pl (or a newer version) is currently installed All tests must pass to be considered installed. Returns True if it looks like this or a newer version is installed; False otherwise. """ if 'installs' in pl: installitems = pl['installs'] for item in installitems: itemtype = item.get('type') 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 pl: receipts = pl['receipts'] for item in receipts: 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 def evidenceThisIsInstalled(pl): """ Checks to see if there is evidence that the item described by pl (any version) is currently installed. If any tests pass, the item might be installed. So this isn't the same as isInstalled() """ global pkgdata # first check our pkgdata if pl['name'] in pkgdata['installed_names']: return True if 'aliases' in pl: for alias in pl['aliases']: if alias in pkgdata['installed_names']: return True if 'installs' in pl: installitems = pl['installs'] for item in installitems: if 'path' in item: # we can only check by path; if the item has been moved # we're not clever enough to find it, and our removal # methods are even less clever if os.path.exists(item['path']): # some version is installed return True # this has been superceded by our pkgdata check #if 'receipts' in pl: # receipts = pl['receipts'] # for item in receipts: # if 'packageid' in item: # if munkicommon.getInstalledPackageVersion(item['packageid']): # # some version of this package is installed # return True # if we got this far, we failed all the tests, so the item # must not be installed (or we have bad metadata...) return 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 be installed first. Items to be installed are added to installinfo['managed_installs'] Calls itself recursively as it processes dependencies. Returns a boolean; when processing dependencies, a false return will stop the installation of a dependent item """ managedinstallprefs = munkicommon.prefs() ManagedInstallDir = managedinstallprefs['ManagedInstallDir'] manifestitemname = os.path.split(manifestitem)[1] #munkicommon.display_info("Getting detail on %s..." % manifestitemname) pl = getItemDetail(manifestitem, cataloglist) if not pl: munkicommon.display_info("Could not process item %s for install because could not get detail." % manifestitem) return False # check to see if item is already in the installlist: if isItemInInstallInfo(pl, installinfo['managed_installs'], pl.get('version')): munkicommon.display_debug1("%s has already been processed for install." % manifestitemname) return True # check dependencies dependenciesMet = True # there are two kinds of dependencies. # # 'requires' are prerequistes: package A requires package B be installed first. # if package A is removed, package B is unaffected. requires is one to many. # # 'modifies' is a package the current package modifies on install; generally this means the # current package is an updater.. For example, 'Office2008' might resolve to Office2008--12.1.7 which modifies # Office2008--12.1.0 which modifies Office2008--12.0.0. (Office2008--12.1.7 and # Office2008--12.1.0 are updater packages, Office2008--12.0.0 is the original installer.) # If you later remove Office2008, you want to remove everything installed by all three packages. # 'modifies' provides a method to theoretically figure it all out. # 'modifies' is one to one relationship - this item can modify only one other item. # # when processing installs, the two dependencies are basically equivilent; # the real difference comes when processing removals. if 'requires' in pl: dependencies = pl['requires'] for item in dependencies: munkicommon.display_detail("%s requires %s. Getting info on %s..." % (manifestitemname, item, item)) success = processInstall(item, cataloglist, installinfo) if not success: dependenciesMet = False if 'modifies' in pl: dependencies = pl['modifies'] if type(dependencies) == list: # in case this was put in as an array # we support only a single modified item. item = dependencies[0] else: item = dependencies munkicommon.display_detail("%s modifies %s. Getting info on %s..." % (manifestitemname, item, item)) success = processInstall(item, cataloglist, installinfo) if not success: dependenciesMet = False if not dependenciesMet: munkicommon.display_info("Didn't attempt to install %s because could not resolve all dependencies." % manifestitemname) return False iteminfo = {} iteminfo["name"] = pl.get('name', '') iteminfo["manifestitem"] = manifestitemname iteminfo["description"] = pl.get('description', '') if not isInstalled(pl): munkicommon.display_detail("Need to install %s" % manifestitemname) # check to see if there is enough free space to download and install if not enoughDiskSpace(pl): iteminfo['installed'] = False iteminfo['note'] = "Insufficient disk space to download and install" installinfo['managed_installs'].append(iteminfo) return False if 'installer_item_location' in pl: location = pl['installer_item_location'] if download_installeritem(location): filename = os.path.split(location)[1] iteminfo['installer_item'] = filename iteminfo['installed'] = False iteminfo["version_to_install"] = pl.get('version',"UNKNOWN") iteminfo['description'] = pl.get('description','') iteminfo['display_name'] = pl.get('display_name','') if 'installer_choices_xml' in pl: iteminfo['installer_choices_xml'] = pl['installer_choices_xml'] if 'RestartAction' in pl: iteminfo['RestartAction'] = pl['RestartAction'] installinfo['managed_installs'].append(iteminfo) return True else: iteminfo['installed'] = False iteminfo['note'] = "Download failed" installinfo['managed_installs'].append(iteminfo) return False else: munkicommon.display_info("Can't install %s because there's no download info for the installer item" % manifestitemname) iteminfo['installed'] = False iteminfo['note'] = "Download info missing" installinfo['managed_installs'].append(iteminfo) return False else: iteminfo["installed"] = True #iteminfo["installed_version"] = getInstalledVersion(pl) installinfo['managed_installs'].append(iteminfo) # remove included version number if any (name, includedversion) = nameAndVersion(manifestitemname) munkicommon.display_detail("%s version %s (or newer) is already installed." % (name, pl['version'])) return True def processManifestForInstalls(manifestpath, installinfo, parentcatalogs=[]): """ Processes manifests to build a list of items to install. Can be recursive if manifests inlcude other manifests. Probably doesn't handle circular manifest references well... """ cataloglist = getManifestValueForKey(manifestpath, 'catalogs') if cataloglist: getCatalogs(cataloglist) else: cataloglist = parentcatalogs nestedmanifests = getManifestValueForKey(manifestpath, "included_manifests") if nestedmanifests: for item in nestedmanifests: nestedmanifestpath = getmanifest(item) if munkicommon.stopRequested(): return {} if nestedmanifestpath: listofinstalls = processManifestForInstalls(nestedmanifestpath, installinfo, cataloglist) installitems = getManifestValueForKey(manifestpath, "managed_installs") if installitems: for item in installitems: if munkicommon.stopRequested(): return {} result = processInstall(item, cataloglist, installinfo) return installinfo def getReceiptsToRemove(item): name = item['name'] if name in pkgdata['receipts_for_name']: return pkgdata['receipts_for_name'][name] # now check aliases if 'aliases' in item: for alias in item['aliases']: if alias in pkgdata['receipts_for_name']: return pkgdata['receipts_for_name'][alias] # found nothing 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. """ global pkgdata manifestitemname_withversion = os.path.split(manifestitem)[1] munkicommon.display_detail("Processing manifest item %s..." % manifestitemname_withversion) (manifestitemname, includedversion) = nameAndVersion(manifestitemname_withversion) infoitems = [] if includedversion: # a specific version was specified pl = getItemDetail(manifestitemname, cataloglist, includedversion) if pl: infoitems.append(pl) else: # get all items matching the name provided infoitems = getAllItemsWithName(manifestitemname,cataloglist) if not infoitems: munkicommon.display_info("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 # _and_ removed. We'll warn, and do nothing with this item. if isItemInInstallInfo(item, installinfo['managed_installs']): munkicommon.display_info("Will not attempt to remove %s because some version of it is in the list of managed installs, or it is required by another managed install." % manifestitemname_withversion) return False for item in infoitems: # check to see if item is already in the removallist: if isItemInInstallInfo(item, installinfo['removals']): munkicommon.display_debug1("%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) iteminfo = {} iteminfo["manifestitem"] = manifestitemname_withversion iteminfo["installed"] = False installinfo['removals'].append(iteminfo) return True uninstall_item = None packagesToRemove = [] for item in infoitems: # check for uninstall info # walk through the list of items (sorted newest first) # and grab the first uninstall method we find. if 'uninstallable' in item and 'uninstall_method' in item: uninstallmethod = item['uninstall_method'] if uninstallmethod == 'removepackages': packagesToRemove = getReceiptsToRemove(item) if packagesToRemove: uninstall_item = item break else: # no matching packages found. Check next item continue else: # uninstall_method is a local script. # Check to see if it exists and is executable if os.path.exists(uninstallmethod) and os.access(uninstallmethod, os.X_OK): uninstall_item = item break if not uninstall_item: # we didn't find an item that seems to match anything on disk. munkicommon.display_info("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 # # First, look through catalogs for items that are required by this item; # if any are installed, we need to remove them as well # # still not sure how to handle references to specific versions -- # if another package says it requires SomePackage--1.0.0.0.0 # and we're supposed to remove SomePackage--1.0.1.0.0... what do we do? # dependentitemsremoved = True ManagedInstallDir = munkicommon.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) catalog = FoundationPlist.readPlist(localcatalog) for item_pl in catalog: namesandaliases = [] namesandaliases.append(item_pl.get('name')) namesandaliases.extend(item_pl.get('aliases',[])) if not set(namesandaliases).intersection(processednamesandaliases): if 'requires' in item_pl: if set(item_pl['requires']).intersection(uninstall_item_names): munkicommon.display_debug1("%s requires %s, checking to see if it's installed..." % (item_pl.get('name'), manifestitemname)) if evidenceThisIsInstalled(item_pl): munkicommon.display_info("%s requires %s and must be removed as well." % (item_pl.get('name'), manifestitemname)) success = processRemoval(item_pl.get('name'), cataloglist, installinfo) if not success: dependentitemsremoved = False 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 # the method is smart enough to get everything... if 'modifies' in uninstall_item and uninstallmethod == 'removepackages': modifies_value = uninstall_item['modifies'] if type(modifies_value) == list: modifieditem = modifies_value[0] else: modifieditem = modifies_value (modifieditemname, modifieditemversion) = nameAndVersion(modifieditem) if not modifieditemname in uninstall_item_names: success = processRemoval(modifieditem, cataloglist, installinfo) if not success: dependentitemsremoved = False if not dependentitemsremoved: munkicommon.display_info("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', '') iteminfo["display_name"] = uninstall_item.get('display_name', '') iteminfo["manifestitem"] = manifestitemname_withversion iteminfo["description"] = "Will be removed." if packagesToRemove: # remove references for each package packagesToReallyRemove = [] for pkg in packagesToRemove: munkicommon.display_debug1("Considering %s for removal..." % pkg) # find pkg in pkgdata['pkg_references'] and remove the reference so # we only remove packages if we're the last reference to it if pkg in pkgdata['pkg_references']: munkicommon.display_debug1("%s references are: %s" % (pkg, pkgdata['pkg_references'][pkg])) pkgdata['pkg_references'][pkg].remove(iteminfo["name"]) if len(pkgdata['pkg_references'][pkg]) == 0: munkicommon.display_debug1("Adding %s to removal list." % pkg) packagesToReallyRemove.append(pkg) else: # This shouldn't happen munkicommon.display_error("WARNING: pkg id %s missing from pkgdata" % pkg) iteminfo['packages'] = packagesToReallyRemove iteminfo["uninstall_method"] = uninstallmethod iteminfo["installed"] = True iteminfo["installed_version"] = uninstall_item.get('version') if 'RestartAction' in uninstall_item: iteminfo['RestartAction'] = uninstall_item['RestartAction'] installinfo['removals'].append(iteminfo) munkicommon.display_detail("Removal of %s added to ManagedInstaller tasks." % manifestitemname_withversion) return True def processManifestForRemovals(manifestpath, installinfo): """ Processes manifests for removals. Can be recursive if manifests include other manifests. Probably doesn't handle circular manifest references well... """ global pkgdata if pkgdata == {}: # build our database of installed packages analyzeInstalledPkgs() cataloglist = getManifestValueForKey(manifestpath, 'catalogs') nestedmanifests = getManifestValueForKey(manifestpath, "included_manifests") if nestedmanifests: for item in nestedmanifests: if munkicommon.stopRequested(): return {} nestedmanifestpath = getmanifest(item) if nestedmanifestpath: listofremovals = processManifestForRemovals(nestedmanifestpath, installinfo) removalitems = getManifestValueForKey(manifestpath, "managed_uninstalls") if removalitems: for item in removalitems: if munkicommon.stopRequested(): return {} result = processRemoval(item, cataloglist, installinfo) return installinfo def getManifestValueForKey(manifestpath, keyname): try: pl = FoundationPlist.readPlist(manifestpath) except: munkicommon.display_error("Could not read plist %s" % manifestpath) return None if keyname in pl: return pl[keyname] else: return None def getCatalogs(cataloglist): """ Retreives the catalogs from the server and populates our catalogs dictionary """ global catalog managedinstallprefs = munkicommon.prefs() sw_repo_baseurl = managedinstallprefs['SoftwareRepoURL'] catalog_dir = os.path.join(managedinstallprefs['ManagedInstallDir'], "catalogs") for catalogname in cataloglist: if not catalogname in catalog: catalogurl = sw_repo_baseurl + "/catalogs/" + urllib2.quote(catalogname) catalogpath = os.path.join(catalog_dir, catalogname) message = "Getting catalog %s from %s..." % (catalogname, catalogurl) munkicommon.log(message) message = "Retreiving catalog '%s'..." % catalogname (newcatalog, err) = getHTTPfileIfNewerAtomically(catalogurl, catalogpath, message=message) if newcatalog: catalog[catalogname] = makeCatalogDB(FoundationPlist.readPlist(newcatalog)) else: munkicommon.display_error("Could not retreive catalog %s from server." % catalog) munkicommon.display_error(err) def getmanifest(partialurl, suppress_errors=False): """ Gets a manifest from the server """ managedinstallprefs = munkicommon.prefs() sw_repo_baseurl = managedinstallprefs['SoftwareRepoURL'] manifest_dir = os.path.join(managedinstallprefs['ManagedInstallDir'], "manifests") if partialurl.startswith("http"): # then it's really a request for the client's primary manifest manifesturl = partialurl manifestname = "client_manifest.plist" else: # request for nested manifest manifestname = os.path.split(partialurl)[1] manifesturl = sw_repo_baseurl + "/manifests/" + urllib2.quote(partialurl) manifestpath = os.path.join(manifest_dir, manifestname) message = "Getting manifest %s from %s..." % (manifestname, manifesturl) munkicommon.log(message) message = "Retreiving list of software for this machine..." (newmanifest, err) = getHTTPfileIfNewerAtomically(manifesturl, manifestpath, message=message) if not newmanifest and not suppress_errors: munkicommon.display_error("Could not retreive manifest %s from the server." % partialurl) munkicommon.display_error(err) return newmanifest def getPrimaryManifest(alternate_id): """ Gets the client manifest from the server """ global errors manifest = "" managedinstallprefs = munkicommon.prefs() manifesturl = managedinstallprefs['ManifestURL'] clientidentifier = managedinstallprefs.get('ClientIdentifier','') if not manifesturl.endswith('?') and not manifesturl.endswith('/'): manifesturl = manifesturl + "/" if alternate_id: # use id passed in at command-line manifesturl = manifesturl + urllib2.quote(alternate_id) elif clientidentifier: # use client_identfier from /Library/Preferences/ManagedInstalls.plist manifesturl = manifesturl + urllib2.quote(clientidentifier) else: # no client identifier specified, so use the hostname hostname = os.uname()[1] munkicommon.display_detail("No client id specified. Requesting %s..." % (manifesturl + hostname)) manifest = getmanifest(manifesturl + hostname,suppress_errors=True) if not manifest: # try the short hostname munkicommon.display_detail("Request failed. Trying %s..." % (manifesturl + hostname.split('.')[0])) manifest = getmanifest(manifesturl + hostname.split('.')[0], suppress_errors=True) if not manifest: # last resort - try for the site_default manifest munkicommon.display_detail("Request failed. Trying %s..." % (manifesturl + "site_default")) manifesturl = manifesturl + "site_default" if not manifest: manifest = getmanifest(manifesturl) if manifest: # clear out any errors we got while trying to find # the primary manifest errors = "" return manifest def getInstallCount(installinfo): count = 0 for item in installinfo.get('managed_installs',[]): if 'installer_item' in item: count +=1 return count def getRemovalCount(installinfo): count = 0 for item in installinfo.get('removals',[]): if 'installed' in item: if item['installed']: count +=1 return count def checkServer(): '''in progress''' managedinstallprefs = munkicommon.prefs() manifesturl = managedinstallprefs['ManifestURL'] # deconstruct URL so we can check availability port = 80 (scheme, netloc, path, query, fragment) = urlparse.urlsplit(manifesturl) # get rid of any embedded username/password netlocparts = netloc.split("@") netloc = netlocparts[-1] # split into host and port if present netlocparts = netloc.split(":") host = netlocparts[0] if len(netlocparts) == 2: port = netlocparts[1] s = socket.socket() #try: s.connect((host, port)) s.close() return True #except: #return False # HTTP download functions # # Handles http downloads # # Supports Last-modified and If-modified-since headers so # we download from the server only if we don't have it in the # local cache, or the locally cached item is older than the # one on the server. # # Possible failure mode: if client's main catalog gets pointed # to a different, older, catalog, we'll fail to retreive it. # Need to check content length as well, and if it changes, retreive # it anyway. # # Should probably cleanup/unify # httpDownload/getfilefromhttpurl/getHTTPfileIfNewerAtomically # # urllib2 has no handler for client certificates, so make one... # Subclass HTTPSClientAuthHandler adapted from the following sources: # http://www.osmonov.com/2009/04/client-certificates-with-urllib2.html # http://www.threepillarsoftware.com/soap_client_auth # http://bugs.python.org/issue3466 # bcw class HTTPSClientAuthHandler(urllib2.HTTPSHandler): def __init__(self, key, cert): urllib2.HTTPSHandler.__init__(self) self.key = key self.cert = cert def https_open(self, req): # Rather than pass in a reference to a connection class, we pass in # a reference to a function which, for all intents and purposes, # will behave as a constructor return self.do_open(self.getConnection, req) def getConnection(self, host, timeout=300): return httplib.HTTPSConnection(host, key_file=self.key, cert_file=self.cert) # An empty subclass for identifying missing certs, bcw # Maybe there is a better place for this? class UseClientCertificateError(IOError): pass def httpDownload(url, filename, headers={}, postData=None, reporthook=None, message=None): # The required name for combination certifcate and private key # File must be PEM formatted and include the client's private key # bcw pemfile = 'munki.pem' # Grab the prefs for UseClientCertificate and construct a loc for the cert, bcw ManagedInstallDir = munkicommon.ManagedInstallDir() UseClientCertificate = munkicommon.UseClientCertificate() cert = os.path.join(ManagedInstallDir, 'certs', pemfile) reqObj = urllib2.Request(url, postData, headers) if UseClientCertificate == True: # Check for the existence of the PEM file, bcw if os.path.isfile(cert): # Construct a secure urllib2 opener, bcw secureopener = urllib2.build_opener(HTTPSClientAuthHandler(cert, cert)) fp = secureopener.open(reqObj) else: # No x509 cert so fail -0x509 (decimal -1289). So amusing. bcw raise UseClientCertificateError(-1289, "PEM file missing, %s" % cert) else: fp = urllib2.urlopen(reqObj) headers = fp.info() if message: # log always, display if verbose is 2 or more munkicommon.display_detail(message) if munkicommon.munkistatusoutput: # send to detail field on MunkiStatus munkistatus.detail(message) #read & write fileObj to filename tfp = open(filename, 'wb') result = filename, headers bs = 1024*8 size = -1 read = 0 blocknum = 0 if reporthook: if "content-length" in headers: size = int(headers["Content-Length"]) reporthook(blocknum, bs, size) while 1: block = fp.read(bs) if block == "": break read += len(block) tfp.write(block) blocknum += 1 if reporthook: reporthook(blocknum, bs, size) fp.close() tfp.close() # raise exception if actual size does not match content-length header if size >= 0 and read < size: raise ContentTooShortError("retrieval incomplete: got only %i out " "of %i bytes" % (read, size), result) return result def getfilefromhttpurl(url,filepath, ifmodifiedsince=None, message=None): """ gets a file from a url. If 'ifmodifiedsince' is specified, this header is set and the file is not retreived if it hasn't changed on the server. Returns 0 if successful, or HTTP error code """ def reporthook(block_count, block_size, file_size): if (file_size > 0): max_blocks = file_size/block_size munkicommon.display_percent_done(block_count, max_blocks) try: request_headers = {} if ifmodifiedsince: modtimestr = time.strftime("%a, %d %b %Y %H:%M:%S GMT",time.gmtime(ifmodifiedsince)) request_headers["If-Modified-Since"] = modtimestr (f,headers) = httpDownload(url, filename=filepath, headers=request_headers, reporthook=reporthook, message=message) if 'last-modified' in headers: # set the modtime of the downloaded file to the modtime of the # file on the server modtimestr = headers['last-modified'] modtimetuple = time.strptime(modtimestr, "%a, %d %b %Y %H:%M:%S %Z") modtimeint = calendar.timegm(modtimetuple) os.utime(filepath, (time.time(), modtimeint)) except urllib2.HTTPError, err: return err.code # Uncommented the exception handler below and added str(err) # This will catch missing or invalid certs/keys in getHTTPfileIfNewerAtomically # bcw except urllib2.URLError, err: return str(err) # This will catch missing certs in getHTTPfileIfNewerAtomically, bcw except UseClientCertificateError, err: return err except IOError, err: return err except Exception, err: return (-1, err) return 0 def getHTTPfileIfNewerAtomically(url,destinationpath, message=None): """ Gets file from HTTP URL, only if newer on web server. Replaces pre-existing file only on success. (thus 'Atomically') """ err = None mytemppath = os.path.join(munkicommon.tmpdir,"TempDownload") if os.path.exists(destinationpath): modtime = os.stat(destinationpath).st_mtime else: modtime = None result = getfilefromhttpurl(url, mytemppath, ifmodifiedsince=modtime, message=message) if result == 0: try: os.rename(mytemppath, destinationpath) return destinationpath, err except: err = "Could not write to %s" % destinationpath destinationpath = None elif result == 304: # not modified, return existing file munkicommon.display_debug1("%s already exists and is up-to-date." % destinationpath) return destinationpath, err # Added to catch private key errors when the opener is constructed, bcw elif result == '': err = "SSL_CTX_use_PrivateKey_file error: PrivateKey Invalid or Missing" destinationpath = None # Added to catch certificate errors when the opener is constructed, bcw elif result == '': err = "SSL_CTX_use_certificate_chain_file error: Certificate Invalid or Missing" destinationpath = None else: err = "Error code: %s retreiving %s" % (result, url) destinationpath = None if os.path.exists(mytemppath): os.remove(mytemppath) return destinationpath, err def getMachineFacts(): global machine machine['arch'] = os.uname()[4] cmd = ['/usr/bin/sw_vers', '-productVersion'] p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (output, err) = p.communicate() # format version string like "10.5.8", so that "10.6" becomes "10.6.0" machine['os_vers'] = munkicommon.padVersionString(output.rstrip("\n"),3) # some globals machine = {} def check(id=''): '''Checks for available new or updated managed software, downloading installer items if needed. Returns 1 if there are available updates, 0 if there are no available updates, and -1 if there were errors.''' getMachineFacts() ManagedInstallDir = munkicommon.ManagedInstallDir() if munkicommon.munkistatusoutput: munkistatus.activate() munkistatus.message("Checking for available updates...") munkistatus.percent("-1") munkicommon.log("### Beginning managed software check ###") mainmanifestpath = getPrimaryManifest(id) if munkicommon.stopRequested(): return 0 installinfo = {} if mainmanifestpath: # initialize our installinfo record installinfo['managed_installs'] = [] installinfo['removals'] = [] munkicommon.display_detail("**Checking for installs**") installinfo = processManifestForInstalls(mainmanifestpath, installinfo) if munkicommon.stopRequested(): return 0 # clean up cache dir # remove any item in the install cache that isn't scheduled # to be installed -- # this allows us to 'pull back' an item before it is installed # by removing it from the manifest installer_item_list = [] for item in installinfo['managed_installs']: if "installer_item" in item: installer_item_list.append(item["installer_item"]) cachedir = os.path.join(ManagedInstallDir, "Cache") for item in os.listdir(cachedir): if item not in installer_item_list: munkicommon.display_detail("Removing %s from cache" % item) os.unlink(os.path.join(cachedir, item)) if munkicommon.munkistatusoutput: # reset progress indicator and detail field munkistatus.percent("-1") munkistatus.detail('') # now generate a list of items to be uninstalled munkicommon.display_detail("**Checking for removals**") if munkicommon.stopRequested(): return 0 installinfo = processManifestForRemovals(mainmanifestpath, installinfo) if munkicommon.munkistatusoutput: munkistatus.disableStopButton() # need to write out install list so the autoinstaller # can use it to install things in the right order installinfochanged = True installinfopath = os.path.join(ManagedInstallDir, "InstallInfo.plist") if os.path.exists(installinfopath): oldinstallinfo = FoundationPlist.readPlist(installinfopath) if oldinstallinfo == installinfo: installinfochanged = False munkicommon.display_detail("No change in InstallInfo.") if installinfochanged: FoundationPlist.writePlist(installinfo, os.path.join(ManagedInstallDir, "InstallInfo.plist")) else: # couldn't get a primary manifest. Check to see if we have a valid InstallList from # an earlier run. munkicommon.display_error("Could not retreive managed install primary manifest.") installinfopath = os.path.join(ManagedInstallDir, "InstallInfo.plist") if os.path.exists(installinfopath): try: installinfo = FoundationPlist.readPlist(installinfopath) except: installinfo = {} installcount = getInstallCount(installinfo) removalcount = getRemovalCount(installinfo) if installcount: munkicommon.display_info("The following items will be installed or upgraded:") for item in installinfo['managed_installs']: if item.get('installer_item'): munkicommon.display_info(" + %s-%s" % (item.get('name',''), item.get('version_to_install',''))) if item.get('description'): munkicommon.display_info(" %s" % item['description']) if item.get('RestartAction') == 'RequireRestart': munkicommon.display_info(" *Restart required") if removalcount: munkicommon.display_info("The following items will be removed:") for item in installinfo['removals']: if item.get('installed'): munkicommon.display_info(" - %s" % item.get('name')) if item.get('RestartAction') == 'RequireRestart': munkicommon.display_info(" *Restart required") if installcount == 0 and removalcount == 0: munkicommon.display_info("No changes to managed software are available.") munkicommon.log("### End managed software check ###") if munkicommon.errors: reporterrors() if installcount or removalcount: return 1 else: return 0 def main(): pass if __name__ == '__main__': main()