mirror of
https://github.com/munki/munki.git
synced 2026-01-18 05:00:19 -06:00
installcheck replaces catalogcheck.py. installcheck supports the new catalog format and the new dependencies. Cleaned up output and logging. ManagedInstaller and removepackages tweaked for better logging and MunkiStatus output. Removed the logout hook examples (for now) makecatalogitem is now makepkginfo New makecatalogs tool. git-svn-id: http://munki.googlecode.com/svn/trunk@50 a4e17f2e-e282-11dd-95e1-755cbddbdd66
1240 lines
46 KiB
Python
Executable File
1240 lines
46 KiB
Python
Executable File
#!/usr/bin/env 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.
|
|
"""
|
|
installcheck
|
|
|
|
Created by Greg Neagle on 2008-11-13.
|
|
2009-04-27: renamed to 'installcheck'
|
|
terminology change!
|
|
replacing "catalog" with "manifest"
|
|
a "catalog" is now a list of what's available on the server;
|
|
a "manifest" is a list of what should be installed or
|
|
uninstalled on this client
|
|
"""
|
|
|
|
#standard libs
|
|
import sys
|
|
import os
|
|
import plistlib
|
|
import tempfile
|
|
import subprocess
|
|
from distutils import version
|
|
import urlparse
|
|
import optparse
|
|
import hashlib
|
|
import time
|
|
|
|
#our lib
|
|
import managedinstalls
|
|
|
|
|
|
def log(message):
|
|
global logdir
|
|
logfile = os.path.join(logdir,'ManagedInstallerCheck.log')
|
|
f = open(logfile, mode='a', buffering=1)
|
|
if f:
|
|
print >>f, time.ctime(), message
|
|
f.close()
|
|
|
|
|
|
def logerror(message):
|
|
print >>sys.stderr, message
|
|
message = "ERROR: %s" % message
|
|
log(message)
|
|
|
|
|
|
def printandlog(message, verbositylevel=0):
|
|
if (options.verbose >= verbositylevel) and not options.quiet:
|
|
print message
|
|
if logginglevel >= verbositylevel:
|
|
log(message)
|
|
|
|
|
|
# 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 == {}:
|
|
printandlog("Getting info on currently installed applications...", 2)
|
|
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 = plistlib.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 = plistlib.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 = managedinstalls.padVersionString(thisvers,5)
|
|
thatvers = managedinstalls.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!
|
|
print >>sys.stderr,"No application name or bundleid was specified!"
|
|
return -2
|
|
|
|
printandlog("Looking for application %s with bundleid: %s, version %s..." % (name, bundleid, versionstring), 2)
|
|
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!
|
|
printandlog("\tDid not find this application on the startup disk.", 2)
|
|
return 0
|
|
|
|
for item in appinfo:
|
|
if '_name' in item:
|
|
printandlog("\tName: \t %s" % item['_name'].encode("UTF-8"), 3)
|
|
if 'path' in item:
|
|
printandlog("\tPath: \t %s" % item['path'].encode("UTF-8"), 3)
|
|
printandlog("\tCFBundleIdentifier: \t %s" % getAppBundleID(item['path']), 3)
|
|
if 'version' in item:
|
|
printandlog("\tVersion: \t %s" % item['version'].encode("UTF-8"), 3)
|
|
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
|
|
printandlog("An older version of this application is present.", 2)
|
|
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:
|
|
logerror("Missing bundle path or version!")
|
|
return -2
|
|
|
|
printandlog("Checking %s for version %s..." % (filepath, vers), 2)
|
|
if not os.path.exists(filepath):
|
|
printandlog("\tNo Info.plist found at %s" % filepath, 1)
|
|
return 0
|
|
|
|
try:
|
|
pl = plistlib.readPlist(filepath)
|
|
except:
|
|
printandlog("\t%s may not be a plist!" % filepath, 1)
|
|
return 0
|
|
|
|
if 'CFBundleShortVersionString' in pl:
|
|
installedvers = pl['CFBundleShortVersionString']
|
|
return compareVersions(installedvers, vers)
|
|
else:
|
|
printandlog("\tNo version info in %s." % filepath, 1)
|
|
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:
|
|
logerror("Missing plist path or version!")
|
|
return -2
|
|
|
|
printandlog("Checking %s for version %s..." % (filepath, vers), 2)
|
|
if not os.path.exists(filepath):
|
|
printandlog("\tNo plist found at %s" % filepath, 1)
|
|
return 0
|
|
|
|
try:
|
|
pl = plistlib.readPlist(filepath)
|
|
except:
|
|
printandlog("\t%s may not be a plist!" % filepath, 1)
|
|
return 0
|
|
|
|
if 'CFBundleShortVersionString' in pl:
|
|
installedvers = pl['CFBundleShortVersionString']
|
|
return compareVersions(installedvers, vers)
|
|
else:
|
|
printandlog("\tNo version info in %s." % filepath, 1)
|
|
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']
|
|
printandlog("Checking existence of %s..." % filepath, 2)
|
|
if os.path.exists(filepath):
|
|
printandlog("\tExists.", 3)
|
|
if 'md5checksum' in item:
|
|
storedchecksum = item['md5checksum']
|
|
ondiskchecksum = getmd5hash(filepath)
|
|
printandlog("Comparing checksums...", 3)
|
|
if storedchecksum == ondiskchecksum:
|
|
printandlog("Checksums match.", 3)
|
|
return 1
|
|
else:
|
|
printandlog("Checksums differ: expected %s, got %s" % (storedchecksum, ondiskchecksum), 3)
|
|
return 0
|
|
return 1
|
|
else:
|
|
printandlog("\tDoes not exist.", 2)
|
|
return 0
|
|
else:
|
|
logerror("No path specified for filesystemItemExists")
|
|
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
|
|
|
|
printandlog("Looking for package %s, version %s" % (pkgid, vers), 2)
|
|
installedvers = managedinstalls.getInstalledPackageVersion(pkgid)
|
|
if installedvers:
|
|
return compareVersions(installedvers, vers)
|
|
else:
|
|
printandlog("\tThis package is not currently installed.", 2)
|
|
return 0
|
|
|
|
|
|
def getInstalledVersion(pl):
|
|
"""
|
|
Attempts to determine the currently installed version of the item
|
|
described by pl
|
|
"""
|
|
if 'receipts' in pl:
|
|
if len(pl['receipts']) == 1:
|
|
pkgid = pl['receipts'][0]['packageid']
|
|
installedvers = managedinstalls.getInstalledPackageVersion(pkgid)
|
|
if installedvers:
|
|
return installedvers
|
|
|
|
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')
|
|
|
|
try:
|
|
# check default location for app
|
|
filepath = os.path.join(install_item['path'], 'Contents', 'Info.plist')
|
|
pl = plistlib.readPlist(filepath)
|
|
return pl.get('CFBundleShortVersionString')
|
|
except:
|
|
# that didn't work, fall through to the slow way
|
|
# using System Profiler
|
|
pass
|
|
|
|
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)
|
|
|
|
maxversion = "0.0.0.0.0"
|
|
for ai_item in appinfo:
|
|
if 'version' in ai_item:
|
|
if compareVersions(ai_item['version'], maxversion) == 2:
|
|
# version is higher
|
|
maxversion = ai_item['version']
|
|
|
|
return maxversion
|
|
|
|
return "UNKNOWN"
|
|
|
|
|
|
def download_installeritem(pkgurl):
|
|
"""
|
|
Downloads a installer item from pkgurl.
|
|
"""
|
|
global mytmpdir
|
|
|
|
managed_install_dir = managedinstalls.managed_install_dir()
|
|
mycachedir = os.path.join(managed_install_dir, "Cache")
|
|
pkgname = os.path.basename(urlparse.urlsplit(pkgurl)[2])
|
|
destinationpath = os.path.join(mycachedir, pkgname)
|
|
if os.path.exists(destinationpath):
|
|
itemmodtime = os.stat(destinationpath).st_mtime
|
|
else:
|
|
itemmodtime = None
|
|
|
|
tempfilepath = os.path.join(mytmpdir, pkgname)
|
|
|
|
dl_message = "Downloading %s from %s" % (pkgname, pkgurl)
|
|
log(dl_message)
|
|
if options.quiet:
|
|
dl_message = None
|
|
elif not options.verbose:
|
|
dl_message = "Downloading %s" % pkgname
|
|
|
|
result = managedinstalls.getfilefromhttpurl(pkgurl, tempfilepath, showprogress=not(options.quiet), ifmodifiedsince=itemmodtime, message=dl_message)
|
|
if result == 0:
|
|
os.rename(tempfilepath, destinationpath)
|
|
return True
|
|
elif result == 304:
|
|
# not modified
|
|
printandlog("%s is already in the install cache." % pkgname, 1)
|
|
return True
|
|
else:
|
|
logerror("Error code: %s" % result)
|
|
if os.path.exists(tempfilepath):
|
|
os.remove(tempfilepath)
|
|
logerror("Couldn't get %s: %s" % (pkgname, result))
|
|
return False
|
|
|
|
|
|
def isItemInInstallInfo(manifestitem_pl, thelist, version=''):
|
|
"""
|
|
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 version:
|
|
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'), version) 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)
|
|
version = chunks.pop()
|
|
name = delim.join(chunks)
|
|
if version[0] in "0123456789":
|
|
return (name, version)
|
|
|
|
return (s, '')
|
|
|
|
|
|
|
|
def compare_versions(a, b):
|
|
return cmp(version.LooseVersion(b['version']), version.LooseVersion(a['version']))
|
|
|
|
|
|
def getAllMatchingItems(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.
|
|
|
|
"""
|
|
itemlist = []
|
|
# we'll throw away any included version info
|
|
(name, includedversion) = nameAndVersion(name)
|
|
managedinstalldir = managedinstalls.managed_install_dir()
|
|
catalogsdir = os.path.join(managedinstalldir, 'catalogs')
|
|
printandlog("Looking for all items matching: %s..." % name, 1)
|
|
for catalogname in cataloglist:
|
|
printandlog("\tChecking catalog %s" % catalogname, 2)
|
|
localcatalog = os.path.join(catalogsdir,catalogname)
|
|
catalog = plistlib.readPlist(localcatalog)
|
|
for item in catalog:
|
|
if (name == item.get('name')) or (name in item.get('aliases',[])):
|
|
if not item in itemlist:
|
|
printandlog("\tAdding %s, version %s..." % (item.get('name'), item.get('version')), 2)
|
|
itemlist.append(item)
|
|
|
|
if itemlist:
|
|
# sort so latest version is first
|
|
itemlist.sort(compare_versions)
|
|
|
|
return itemlist
|
|
|
|
|
|
def getManifestItemDetail(name, cataloglist, version=''):
|
|
"""
|
|
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') that version is used.
|
|
If no version is given at all, the latest version is assumed.
|
|
Returns a pkginfo item.
|
|
"""
|
|
(name, includedversion) = nameAndVersion(name)
|
|
if version == '':
|
|
if includedversion:
|
|
version = includedversion
|
|
else:
|
|
version = 'latest'
|
|
managedinstalldir = managedinstalls.managed_install_dir()
|
|
catalogsdir = os.path.join(managedinstalldir, 'catalogs')
|
|
printandlog("Looking for detail for: %s, version %s..." % (name, version), 1)
|
|
for catalogname in cataloglist:
|
|
printandlog("\tChecking catalog %s" % catalogname, 2)
|
|
localcatalog = os.path.join(catalogsdir,catalogname)
|
|
catalog = plistlib.readPlist(localcatalog)
|
|
candidate = {}
|
|
for item in catalog:
|
|
if (name == item.get('name')) or (name in item.get('aliases',[])):
|
|
printandlog("\tConsidering: %s %s %s" % (item.get('name'), item.get('version'), item.get('installer_item_location')), 3)
|
|
if version == 'latest':
|
|
if not candidate:
|
|
# this is the first version we've seen
|
|
candidate = item
|
|
elif compareVersions(item.get('version'), candidate.get('version')) == 2:
|
|
# item is newer, replace the candidate
|
|
candidate = item
|
|
else:
|
|
if compareVersions(version, item.get('version')) == 1:
|
|
#versions match
|
|
printandlog("Found: %s %s %s" % (item.get('name'), item.get('version'), item.get('installer_item_location')), 1)
|
|
return item
|
|
|
|
if candidate:
|
|
printandlog("Found: %s %s %s" % (candidate.get('name'), candidate.get('version'), candidate.get('installer_item_location')), 1)
|
|
return candidate
|
|
|
|
# if we got this far, we didn't find it.
|
|
printandlog("Nothing found",2)
|
|
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 = managedinstalls.getAvailableDiskSpace()/1024
|
|
if availablediskspace > diskspaceneeded:
|
|
return True
|
|
else:
|
|
printandlog("There is insufficient disk space to download and install %s.", manifestitem_pl.get('name'))
|
|
printandlog(" %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()
|
|
"""
|
|
if 'installs' in pl:
|
|
installitems = pl['installs']
|
|
for item in installitems:
|
|
itemtype = item.get('type')
|
|
if itemtype == 'application':
|
|
if compareApplicationVersion(item) in (-1, 1, 2):
|
|
# some version found
|
|
return True
|
|
|
|
if itemtype == 'bundle':
|
|
if compareBundleVersion(item) in (-1, 1, 2):
|
|
# some version is installed
|
|
return True
|
|
|
|
if itemtype == 'plist':
|
|
if comparePlistVersion(item) in (-1, 1, 2):
|
|
# some version is installed
|
|
return True
|
|
|
|
if itemtype == 'file':
|
|
if filesystemItemExists(item) == 1:
|
|
return True
|
|
|
|
if 'receipts' in pl:
|
|
receipts = pl['receipts']
|
|
for item in receipts:
|
|
if compareReceiptVersion(item) in (-1, 1, 2):
|
|
# some version is installed
|
|
return True
|
|
|
|
# if we got this far, we failed all the tests, so the item
|
|
# must not be installed (or we don't have enough info...)
|
|
return False
|
|
|
|
|
|
def processInstalls(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 = managedinstalls.prefs()
|
|
sw_repo_baseurl = managedinstallprefs['sw_repo_url']
|
|
managed_install_dir = managedinstallprefs['managed_install_dir']
|
|
|
|
downloadbaseurl = sw_repo_baseurl + "/pkgs/"
|
|
|
|
manifestitemname = os.path.split(manifestitem)[1]
|
|
#printandlog("Getting detail on %s..." % manifestitemname, 1)
|
|
pl = getManifestItemDetail(manifestitem, cataloglist)
|
|
|
|
if not pl:
|
|
printandlog("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')):
|
|
printandlog("%s has already been processed for install." % manifestitemname, 1)
|
|
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.
|
|
#
|
|
# 'modifies' are packages the current package modifies on install; generally these
|
|
# are updaters. 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 a superset of 'requires'.
|
|
#
|
|
# when processing installs, the two dependencies are basically equivilent;
|
|
# the real difference comes when processing removals.
|
|
|
|
dependency_types = ['requires', 'modifies']
|
|
for dependency in dependency_types:
|
|
if dependency in pl:
|
|
dependencies = pl[dependency]
|
|
for item in dependencies:
|
|
printandlog("%s %s %s. Getting info on %s..." % (manifestitemname, dependency, item, item), 1)
|
|
success = processInstalls(item, cataloglist, installinfo)
|
|
if not success:
|
|
dependenciesMet = False
|
|
|
|
if not dependenciesMet:
|
|
printandlog("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):
|
|
printandlog("Need to install %s" % manifestitemname, 1)
|
|
# check to see if there is enough free space to download and install
|
|
if not enoughDiskSpace(pl):
|
|
iteminfo["installed"] = False
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
return False
|
|
|
|
if 'installer_item_location' in pl:
|
|
location = pl['installer_item_location']
|
|
url = downloadbaseurl + location
|
|
if download_installeritem(url):
|
|
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 'RestartAction' in pl:
|
|
iteminfo['RestartAction'] = pl['RestartAction']
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
return True
|
|
else:
|
|
iteminfo["installed"] = False
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
return False
|
|
else:
|
|
printandlog("Can't install %s because there's no download info for the installer item" % manifestitemname)
|
|
iteminfo["installed"] = False
|
|
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)
|
|
printandlog("%s version %s is already installed." % (name, iteminfo["installed_version"]), 1)
|
|
return True
|
|
|
|
|
|
def processManifestForInstalls(manifestpath, installinfo):
|
|
"""
|
|
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)
|
|
|
|
nestedmanifests = getManifestValueForKey(manifestpath, "included_manifests")
|
|
if nestedmanifests:
|
|
for item in nestedmanifests:
|
|
nestedmanifestpath = getmanifest(item)
|
|
if nestedmanifestpath:
|
|
listofinstalls = processManifestForInstalls(nestedmanifestpath, installinfo)
|
|
|
|
installitems = getManifestValueForKey(manifestpath, "managed_installs")
|
|
if installitems:
|
|
for item in installitems:
|
|
result = processInstalls(item, cataloglist, installinfo)
|
|
|
|
return installinfo
|
|
|
|
|
|
def getMatchingReceipts(pl):
|
|
"""
|
|
Checks to see if the receipts for pl
|
|
are present.
|
|
If a different version of the receipt
|
|
is present, return an empty list.
|
|
If no receipts are present, return an empty list
|
|
At least one receipt must be present, and
|
|
all present receipts must match versions.
|
|
On success, return a list of matching receipts
|
|
"""
|
|
matchingReceipts = []
|
|
|
|
if 'receipts' in pl:
|
|
receipts = pl['receipts']
|
|
for item in receipts:
|
|
comparisonResult = compareReceiptVersion(item)
|
|
if comparisonResult in (-1, 2):
|
|
# different version installed, return empty list
|
|
return []
|
|
elif comparisonResult == 1:
|
|
matchingReceipts.append(item['packageid'])
|
|
|
|
return matchingReceipts
|
|
|
|
|
|
def processRemovals(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]
|
|
|
|
printandlog("Processing manifest item %s..." % manifestitemname_withversion,1)
|
|
(manifestitemname, includedversion) = nameAndVersion(manifestitemname_withversion)
|
|
infoitems = []
|
|
if includedversion:
|
|
# a specific version was specified
|
|
pl = getManifestItemDetail(manifestitemname, cataloglist, includedversion)
|
|
if pl:
|
|
infoitems.append(pl)
|
|
else:
|
|
# get all items matching the name provided
|
|
infoitems = getAllMatchingItems(manifestitemname,cataloglist)
|
|
|
|
if not infoitems:
|
|
printandlog("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']):
|
|
printandlog("Will not attempt to remove %s because it (or another version) is in the list of managed installs." % manifestitemname_withversion)
|
|
return False
|
|
|
|
for item in infoitems:
|
|
# check to see if item is already in the removallist:
|
|
if isItemInInstallInfo(item, installinfo['removals']):
|
|
printandlog("%s has already been processed for removal." % manifestitemname_withversion, 1)
|
|
return True
|
|
|
|
installEvidence = False
|
|
for item in infoitems:
|
|
if evidenceThisIsInstalled(item):
|
|
installEvidence = True
|
|
break
|
|
|
|
if not installEvidence:
|
|
printandlog("%s doesn't appear to be installed." % manifestitemname_withversion, 1)
|
|
iteminfo = {}
|
|
iteminfo["manifestitem"] = manifestitemname_withversion
|
|
iteminfo["installed"] = False
|
|
installinfo['removals'].append(iteminfo)
|
|
return True
|
|
|
|
uninstall_item = None
|
|
for item in infoitems:
|
|
# check for uninstall info
|
|
if item.get('uninstallable') and item.get('uninstall_method'):
|
|
uninstallmethod = item['uninstall_method']
|
|
if uninstallmethod == 'removepackages':
|
|
packagesToRemove = getMatchingReceipts(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
|
|
|
|
if not uninstall_item:
|
|
# we didn't find an item that seems to match anything on disk.
|
|
printandlog("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
|
|
managed_install_dir = managedinstalls.managed_install_dir()
|
|
catalogsdir = os.path.join(managed_install_dir, 'catalogs')
|
|
|
|
processednamesandaliases = []
|
|
for catalogname in cataloglist:
|
|
localcatalog = os.path.join(catalogsdir,catalogname)
|
|
catalog = plistlib.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(namesandaliases):
|
|
if evidenceThisIsInstalled(item_pl):
|
|
printandlog("%s requires %s and must be removed as well." % (item_pl.get('name'), manifestitemname), 1)
|
|
success = processRemovals(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 others, we must remove them as well
|
|
if 'modifies' in uninstall_item:
|
|
modifiedItems = uninstall_item['modifies']
|
|
for item in modifiedItems:
|
|
printandlog("%s is modified by %s and must also be removed. Getting info on %s..." % (item, manifestitemname_withversion, item),1)
|
|
success = processRemovals(item, cataloglist, installinfo)
|
|
if not success:
|
|
dependentitemsremoved = False
|
|
|
|
if not dependentitemsremoved:
|
|
printandlog("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"] = uninstall_item.get('description','')
|
|
if packagesToRemove:
|
|
iteminfo['packages'] = packagesToRemove
|
|
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)
|
|
printandlog("Removal of %s added to ManagedInstaller tasks." % manifestitemname_withversion, 1)
|
|
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...
|
|
"""
|
|
cataloglist = getManifestValueForKey(manifestpath, 'catalogs')
|
|
|
|
nestedmanifests = getManifestValueForKey(manifestpath, "included_manifests")
|
|
if nestedmanifests:
|
|
for item in nestedmanifests:
|
|
nestedmanifestpath = getmanifest(item)
|
|
if nestedmanifestpath:
|
|
listofremovals = processManifestForRemovals(nestedmanifestpath, installinfo)
|
|
|
|
removalitems = getManifestValueForKey(manifestpath, "managed_uninstalls")
|
|
if removalitems:
|
|
for item in removalitems:
|
|
result = processRemovals(item, cataloglist, installinfo)
|
|
|
|
return installinfo
|
|
|
|
|
|
def getManifestValueForKey(manifestpath, keyname):
|
|
|
|
try:
|
|
pl = plistlib.readPlist(manifestpath)
|
|
except:
|
|
logerror("Could not read plist %s" % manifestpath)
|
|
return None
|
|
if keyname in pl:
|
|
return pl[keyname]
|
|
else:
|
|
return None
|
|
|
|
|
|
def createDirsIfNeeded(dirlist):
|
|
for directory in dirlist:
|
|
if not os.path.exists(directory):
|
|
try:
|
|
os.mkdir(directory)
|
|
except:
|
|
print >>sys.stderr, "Could not create %s" % directory
|
|
return False
|
|
|
|
return True
|
|
|
|
def getCatalogs(cataloglist):
|
|
"""
|
|
Retreives the catalogs from the server
|
|
"""
|
|
managedinstallprefs = managedinstalls.prefs()
|
|
sw_repo_baseurl = managedinstallprefs['sw_repo_url']
|
|
catalog_dir = os.path.join(managedinstallprefs['managed_install_dir'], "catalogs")
|
|
|
|
for catalog in cataloglist:
|
|
catalogurl = sw_repo_baseurl + "/catalogs/" + catalog
|
|
catalogpath = os.path.join(catalog_dir, catalog)
|
|
message = "Getting catalog %s from %s..." % (catalog, catalogurl)
|
|
log(message)
|
|
if options.quiet:
|
|
message = None
|
|
elif not options.verbose:
|
|
message = "Retreiving catalog '%s'..." % catalog
|
|
newcatalog = managedinstalls.getHTTPfileIfNewerAtomically(catalogurl,catalogpath,showprogress=not(options.quiet), message=message)
|
|
|
|
|
|
def getmanifest(partialurl):
|
|
"""
|
|
Gets a manifest from the server
|
|
"""
|
|
managedinstallprefs = managedinstalls.prefs()
|
|
sw_repo_baseurl = managedinstallprefs['sw_repo_url']
|
|
manifest_dir = os.path.join(managedinstallprefs['managed_install_dir'], "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/" + partialurl
|
|
|
|
manifestpath = os.path.join(manifest_dir, manifestname)
|
|
message = "Getting manifest %s from %s..." % (manifestname, manifesturl)
|
|
log(message)
|
|
if options.quiet:
|
|
message = None
|
|
elif not options.verbose:
|
|
message = "Retreiving list of software for this machine..."
|
|
|
|
return managedinstalls.getHTTPfileIfNewerAtomically(manifesturl,manifestpath,showprogress=not(options.quiet), message=message)
|
|
|
|
|
|
def getPrimaryManifest(alternate_id):
|
|
"""
|
|
Gets the client manifest from the server
|
|
"""
|
|
managedinstallprefs = managedinstalls.prefs()
|
|
manifesturl = managedinstallprefs['manifest_url']
|
|
clientidentifier = managedinstallprefs['client_identifier']
|
|
|
|
if not manifesturl.endswith('?') and not manifesturl.endswith('/'):
|
|
manifesturl = manifesturl + "/"
|
|
if alternate_id:
|
|
# use id passed in at command-line
|
|
manifesturl = manifesturl + alternate_id
|
|
elif clientidentifier:
|
|
# use client_identfier from /Library/Preferences/ManagedInstalls.plist
|
|
manifesturl = manifesturl + clientidentifier
|
|
else:
|
|
# no client identifier specified, so use the hostname
|
|
manifesturl = manifesturl + os.uname()[1]
|
|
|
|
return getmanifest(manifesturl)
|
|
|
|
|
|
def getInstallCount(installinfo):
|
|
count = 0
|
|
for item in installinfo['managed_installs']:
|
|
if 'installed' in item:
|
|
if not item['installed']:
|
|
count +=1
|
|
return count
|
|
|
|
|
|
def getRemovalCount(installinfo):
|
|
count = 0
|
|
for item in installinfo['removals']:
|
|
if 'installed' in item:
|
|
if item['installed']:
|
|
count +=1
|
|
return count
|
|
|
|
|
|
# some globals
|
|
logdir = ''
|
|
logginglevel = 1
|
|
mytmpdir = tempfile.mkdtemp()
|
|
p = optparse.OptionParser()
|
|
p.add_option('--id', '-i', default='',
|
|
help='Alternate identifier for catalog retreival')
|
|
p.add_option('--quiet', '-q', action='store_true',
|
|
help='Quiet mode. Logs messages, but nothing to stdout.')
|
|
p.add_option('--verbose', '-v', action='count', default=0,
|
|
help='More verbose output. May be specified multiple times.')
|
|
options, arguments = p.parse_args()
|
|
|
|
|
|
def main():
|
|
global mytmpdir, options, logdir
|
|
|
|
if not options.quiet: print "Managed Software Check\n"
|
|
|
|
managedinstallprefs = managedinstalls.prefs()
|
|
managed_install_dir = managedinstallprefs['managed_install_dir']
|
|
logginglevel = managedinstallprefs.get('logging_level', 1)
|
|
manifestsdir = os.path.join(managed_install_dir, "manifests")
|
|
cachedir = os.path.join(managed_install_dir, "Cache")
|
|
logdir = os.path.join(managed_install_dir, "Logs")
|
|
|
|
if not createDirsIfNeeded([managed_install_dir, manifestsdir, cachedir, logdir]):
|
|
# can't use logerror function since logdir might not exist
|
|
print >>sys.stderr, "No write access to managed install directory: %s" % managed_install_dir
|
|
exit(-1)
|
|
log("### Beginning managed software check ###")
|
|
|
|
mainmanifestpath = getPrimaryManifest(options.id)
|
|
if not mainmanifestpath:
|
|
logerror("Could not retreive managed install primary manifest.")
|
|
exit(-1)
|
|
|
|
# initialize our installinfo record
|
|
installinfo = {}
|
|
installinfo['managed_installs'] = []
|
|
installinfo['removals'] = []
|
|
|
|
printandlog("**Checking for installs**", 1)
|
|
installinfo = processManifestForInstalls(mainmanifestpath, installinfo)
|
|
|
|
# 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"])
|
|
|
|
for item in os.listdir(cachedir):
|
|
if item not in installer_item_list:
|
|
if options.verbose > 1:
|
|
print "Removing %s from cache" % item
|
|
os.unlink(os.path.join(cachedir, item))
|
|
|
|
# now generate a list of items to be uninstalled
|
|
printandlog("**Checking for removals**", 1)
|
|
installinfo = processManifestForRemovals(mainmanifestpath, installinfo)
|
|
|
|
# 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(managed_install_dir, "InstallInfo.plist")
|
|
if os.path.exists(installinfopath):
|
|
oldinstallinfo = plistlib.readPlist(installinfopath)
|
|
if oldinstallinfo == installinfo:
|
|
installinfochanged = False
|
|
printandlog("No change in InstallInfo.", 1)
|
|
if installinfochanged:
|
|
plistlib.writePlist(installinfo, os.path.join(managed_install_dir, "InstallInfo.plist"))
|
|
|
|
try:
|
|
# clean up our tmp dir
|
|
os.rmdir(mytmpdir)
|
|
except:
|
|
# not fatal if it fails
|
|
pass
|
|
|
|
installcount = getInstallCount(installinfo)
|
|
removalcount = getRemovalCount(installinfo)
|
|
|
|
if installinfochanged and not options.quiet:
|
|
if installcount or removalcount:
|
|
pass
|
|
#print "Managed software updates are available."
|
|
|
|
if installcount:
|
|
printandlog("The following items will be installed or upgraded:")
|
|
for item in installinfo['managed_installs']:
|
|
if not item.get('installed'):
|
|
printandlog(" + %s-%s" % (item.get('name'), item.get('version_to_install')))
|
|
if item.get('description'):
|
|
printandlog(" %s" % item['description'])
|
|
if item.get('RestartAction') == 'RequireRestart':
|
|
printandlog(" *Restart required")
|
|
if removalcount:
|
|
printandlog("The following items will be removed:")
|
|
for item in installinfo['removals']:
|
|
if item.get('installed'):
|
|
printandlog(" - %s" % item.get('name'))
|
|
if item.get('RestartAction') == 'RequireRestart':
|
|
printandlog(" *Restart required")
|
|
|
|
if installcount == 0 and removalcount == 0:
|
|
printandlog("No changes to managed software scheduled.")
|
|
|
|
log("### End managed software check ###")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|