Files
munki/code/client/installcheck
Greg Neagle 012c41674d munkilib.py: fixed default ManifestURL and SoftwareRepo URL
removepackages.py: better output capture when calling `lsbom`
installcheck: better output capture when calling `softwareupdate`
		Now creates catalogs dir in ManagedInstallDir if it's missing
managedinstaller: better output capture when calling `installer`




git-svn-id: http://munki.googlecode.com/svn/trunk@99 a4e17f2e-e282-11dd-95e1-755cbddbdd66
2009-05-28 21:30:38 +00:00

1451 lines
55 KiB
Python
Executable File

#!/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.
"""
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 datetime
import dateutil.parser
import time
import random
import socket
#our lib
import munkilib
from munkistatus import osascript
def log(message):
global logdir
logfile = os.path.join(logdir,'ManagedInstallerCheck.log')
f = open(logfile, mode='a', buffering=1)
if f:
print >>f, datetime.datetime.now().ctime(), message
f.close()
def logerror(message):
global errors
print >>sys.stderr, message
message = "ERROR: %s" % message
log(message)
# collect the errors for later reporting
errors = errors + message + '\n'
def printandlog(message, verbositylevel=0):
if (options.verbose >= verbositylevel) and not options.quiet:
print message
if logginglevel >= verbositylevel:
log(message)
def reporterrors():
# just a placeholder right now;
# this needs to be expanded to support error reporting
# via email and HTTP CGI.
global options
managedinstallprefs = munkilib.prefs()
clientidentifier = managedinstallprefs.get('ClientIdentifier','')
alternate_id = options.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 errors
# 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 = munkilib.padVersionString(thisvers,5)
thatvers = munkilib.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 = munkilib.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 = munkilib.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
ManagedInstallDir = munkilib.ManagedInstallDir()
mycachedir = os.path.join(ManagedInstallDir, "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 = munkilib.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 = munkilib.ManagedInstallDir()
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 = munkilib.ManagedInstallDir()
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 = munkilib.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 = munkilib.prefs()
sw_repo_baseurl = managedinstallprefs['SoftwareRepoURL']
ManagedInstallDir = managedinstallprefs['ManagedInstallDir']
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
ManagedInstallDir = munkilib.ManagedInstallDir()
catalogsdir = os.path.join(ManagedInstallDir, '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 = munkilib.prefs()
sw_repo_baseurl = managedinstallprefs['SoftwareRepoURL']
catalog_dir = os.path.join(managedinstallprefs['ManagedInstallDir'], "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 = munkilib.getHTTPfileIfNewerAtomically(catalogurl,catalogpath,showprogress=not(options.quiet), message=message)
if not newcatalog:
logerror("Could not retreive catalog %s from server." % catalog)
def getmanifest(partialurl):
"""
Gets a manifest from the server
"""
managedinstallprefs = munkilib.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/" + 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..."
newmanifest = munkilib.getHTTPfileIfNewerAtomically(manifesturl,manifestpath,showprogress=not(options.quiet), message=message)
if not newmanifest:
logerror("Could not retreive manifest %s from the server." % partialurl)
return newmanifest
def getPrimaryManifest(alternate_id):
"""
Gets the client manifest from the server
"""
managedinstallprefs = munkilib.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 + 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
def isSUinstallItem(itempath):
if itempath.endswith('.pkg') or itempath.endswith('.mpkg'):
return True
if os.path.isdir(itempath):
for subitem in os.listdir(itempath):
if subitem.endswith('.dist'):
return True
return False
def doSoftwareUpdate(installinfo):
installinfo['apple_updates'] = []
if munkilib.pref('InstallAppleSoftwareUpdates'):
# switch to our preferred Software Update Server if supplied
if munkilib.pref('SoftwareUpdateServerURL'):
oldsuserver = ''
cmd = ['/usr/bin/defaults', 'read', '/Library/Preferences/com.apple.SoftwareUpdate', 'CatalogURL']
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
if p.returncode == 0:
oldsusserver = out.rstrip('\n')
cmd = ['/usr/bin/defaults', 'write', '/Library/Preferences/com.apple.SoftwareUpdate',
'CatalogURL', munkilib.pref('SoftwareUpdateServerURL')]
retcode = subprocess.call(cmd)
swupdldir = '/var/root/Downloads'
runcheck = True
# check downloads dir and skip checking if there's
# anything in it
for item in os.listdir(swupdldir):
itempath = os.path.join(swupdldir,item)
if isSUinstallItem(itempath):
runcheck = False
break
if runcheck:
cmd = ['/usr/sbin/softwareupdate', '-d', '-a']
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True:
swupdout = p.stdout.readline()
if not swupdout and (p.poll() != None):
#process completed and we've deal with all the output
break
printandlog(swupdout.rstrip("\n"))
sys.stdout.flush()
retcode = p.poll()
if retcode:
# some problem occurred.
# right now, we'll just stumble through in case some downloads
# succeeded
pass
# now check for downloads and process
for item in os.listdir(swupdldir):
itempath = os.path.join(swupdldir,item)
if isSUinstallItem(itempath):
pl = munkilib.getPackageMetaData(itempath)
if pl:
# check to see if there is enough free space to install
if enoughDiskSpace(pl):
iteminfo = {}
iteminfo["installer_item"] = item
iteminfo["name"] = pl.get('name', '')
iteminfo["description"] = pl.get('description', '')
if iteminfo["description"] == '':
iteminfo["description"] = "Updated Apple software."
iteminfo["version_to_install"] = pl.get('version',"UNKNOWN")
iteminfo['display_name'] = pl.get('display_name','')
if 'RestartAction' in pl:
iteminfo['RestartAction'] = pl['RestartAction']
installinfo['apple_updates'].append(iteminfo)
# switch back to original Software Update server
if munkilib.pref('SoftwareUpdateServerURL'):
if oldsuserver:
cmd = ['/usr/bin/defaults', 'write', '/Library/Preferences/com.apple.SoftwareUpdate',
'CatalogURL', oldsuserver]
else:
cmd = ['/usr/bin/defaults', 'delete', '/Library/Preferences/com.apple.SoftwareUpdate']
retcode = subprocess.call(cmd)
return installinfo
def checkServer():
managedinstallprefs = munkilib.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
# some globals
logdir = ''
logginglevel = 1
mytmpdir = tempfile.mkdtemp()
errors = ''
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('--randomsleep', '-r', type='int', default=0,
help='Randomly sleeps up to the given number of seconds before checking.')
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, errors
# check to see if we're root
if os.geteuid() != 0:
print >>sys.stderr, "You must run this as root!"
exit(-1)
if not options.quiet: print "Managed Software Check\n"
managedinstallprefs = munkilib.prefs()
ManagedInstallDir = managedinstallprefs['ManagedInstallDir']
logginglevel = managedinstallprefs.get('LoggingLevel', 1)
manifestsdir = os.path.join(ManagedInstallDir, "manifests")
catalogsdir = os.path.join(ManagedInstallDir, "catalogs")
cachedir = os.path.join(ManagedInstallDir, "Cache")
logdir = os.path.join(ManagedInstallDir, "Logs")
if not createDirsIfNeeded([ManagedInstallDir, manifestsdir, catalogsdir, cachedir, logdir]):
# can't use logerror function since logdir might not exist yet
errormessage = "No write access to managed install directory: %s" % ManagedInstallDir
print >>sys.stderr, errormessage
errors = errormessage
reporterrors()
exit(-1)
log("### Beginning managed software check ###")
if options.randomsleep:
randomsleepseconds = random.randrange(options.randomsleep+1)
printandlog("Sleeping %i seconds..." % randomsleepseconds)
time.sleep(randomsleepseconds)
if munkilib.pythonScriptRunning("managedinstaller"):
# managedinstaller is running, so we should quit
printandlog("managedinstaller is running. Exiting.")
log("### End managed software check ###")
exit(0)
mainmanifestpath = getPrimaryManifest(options.id)
if not mainmanifestpath:
logerror("Could not retreive managed install primary manifest.")
# we can't continue.
# we need a way to notify the admin of this problem --
# logging to a local file isn't really sufficient.
reporterrors()
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)
printandlog("**Checking for Apple Software Updates**", 1)
installinfo = doSoftwareUpdate(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(ManagedInstallDir, "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(ManagedInstallDir, "InstallInfo.plist"))
try:
# clean up our tmp dir
os.rmdir(mytmpdir)
except:
# not fatal if it fails
pass
installcount = getInstallCount(installinfo)
removalcount = getRemovalCount(installinfo)
appleupdatecount = len(installinfo.get('apple_updates',[]))
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 appleupdatecount:
printandlog("The following Apple updates will be installed:")
for item in installinfo['apple_updates']:
printandlog (" + %s-%s" % (item.get('name'), item.get('version_to_install')))
if item.get('description') != "Updated Apple software.":
printandlog(" %s" % item['description'])
if item.get('RestartAction') == 'RequireRestart':
printandlog(" *Restart required")
if errors:
reporterrors()
if installcount == 0 and removalcount == 0 and appleupdatecount == 0:
printandlog("No changes to managed software scheduled.")
else:
if munkilib.getconsoleuser() == None:
# eventually trigger managedinstaller here
# we could:
# - call managedinstaller directly, but we'd have to either
# hard-code its path or search a few paths for it
# - call `launchctl start com.googlecode.munki-managedinstaller`, but that
# relies on that launchd job being installed
# - indirectly trigger `launchctl start com.googlecode.munki-managedinstaller`
# by touching its watch file, just like Managed Software Update.app does
#
pass
else:
# some one is logged in, and we have updates.
# if we haven't notified in a (admin-configurable) while, notify:
lastNotifiedString = munkilib.pref('LastNotifiedDate')
daysBetweenNotifications = munkilib.pref('DaysBetweenNotifications')
nowString = munkilib.NSDateNowString()
now = dateutil.parser.parse(nowString)
if lastNotifiedString:
lastNotifiedDate = dateutil.parser.parse(lastNotifiedString)
interval = datetime.timedelta(days=daysBetweenNotifications)
nextNotifyDate = lastNotifiedDate + interval
if now >= nextNotifyDate:
# record current notification date
cmd = ['/usr/bin/defaults', 'write', '/Library/Preferences/ManagedInstalls',
'LastNotifiedDate', '-date', now.ctime()]
retcode = subprocess.call(cmd)
# notify user of available updates
result = osascript('tell application "Managed Software Update" to activate')
log("### End managed software check ###")
if __name__ == '__main__':
main()