Initial import

git-svn-id: http://munki.googlecode.com/svn/trunk@2 a4e17f2e-e282-11dd-95e1-755cbddbdd66
This commit is contained in:
Greg Neagle
2009-01-14 21:41:25 +00:00
parent 456efb8289
commit 55bce871e4
22 changed files with 2765 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
MIDIR=`defaults read /Library/Preferences/ManagedInstalls managed_install_dir`
IHOOK="/Applications/Utilities/radmind/iHook.app/Contents/MacOS/iHook"
SCRIPT="/Users/gneagle/Documents/managedinstalls/code/client/ManagedInstaller"
ARGS="-i"
if [ "$MIDIR" != "" ]; then
CACHEDIR="$MIDIR/Cache"
if [ -d "$CACHEDIR" ]; then
CACHECOUNT=`ls -1 "$CACHEDIR" | wc -l`
if [ $CACHECOUNT -gt 0 ]; then
"$IHOOK" --script="$SCRIPT" "$ARGS"
fi
fi
fi
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python
import os
import subprocess
import managedinstalls
managedinstalldir = managedinstalls.managed_install_dir()
installinfo = os.path.join(managedinstalldir, "InstallInfo.plist")
ihook = "/Applications/Utilities/radmind/iHook.app/Contents/MacOS/iHook"
script = "/Users/gneagle/Documents/managedinstalls/code/client/ManagedInstaller"
args = "-i"
if os.path.exists(installinfo):
retcode = subprocess.call([ihook, "--script=" + script, args])
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python
import os
import subprocess
import managedinstalls
managedinstalldir = managedinstalls.managed_install_dir()
installinfo = os.path.join(managedinstalldir, "InstallInfo.plist")
ihook = "/Applications/Utilities/radmind/iHook.app/Contents/MacOS/iHook"
script = "/Users/gneagle/Documents/managedinstalls/code/client/ManagedInstaller"
args = "-i"
if os.path.exists(installinfo):
retcode = subprocess.call([ihook, "--script=" + script, args])
+412
View File
@@ -0,0 +1,412 @@
#!/usr/bin/env python
"""
installomatic
Tool to automatically install pkgs, mpkgs, and dmgs
(containing pkgs and mpkgs) from a defined folder. Intended
to be run as part of a logout hook, but can be run manually
"""
import os
import subprocess
import sys
import time
import plistlib
import optparse
import managedinstalls
def log(message):
global logdir
logfile = os.path.join(logdir,'autoinstall.log')
f = open(logfile, mode='a', buffering=1)
if f:
print >>f, time.ctime(), message
f.close()
def countinstallcandidates(dirpath):
"""
Counts the number of pkgs, mpkgs, and dmgs
in dirpath
"""
candidatecount = 0
items = os.listdir(dirpath)
for item in items:
if (item.endswith(".pkg") or item.endswith(".mpkg") or item.endswith(".dmg")):
candidatecount += 1
return candidatecount
def install(pkgpath):
"""
Uses the apple installer to install the package or metapackage
at pkgpath. Prints status messages to STDOUT.
Returns the installer return code and true if a restart is needed.
"""
global installablecount
global currentinstallable
global options
currentinstallable += 1
restartneeded = False
installeroutput = []
cmd = ['/usr/sbin/installer', '-pkginfo', '-pkg', pkgpath]
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
packagename = output.splitlines()[0]
print >>sys.stderr, "Package name is", packagename
if options.ihookoutput:
print "%TITLE Installing " + packagename + "..."
print "%%%s Item %s of %s" % (0, currentinstallable, installablecount)
log("Installing %s from %s" % (packagename, os.path.basename(pkgpath)))
cmd = ['/usr/sbin/installer', '-query', 'RestartAction', '-pkg', pkgpath]
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
restartaction = output.rstrip("\n")
if restartaction == "RequireRestart":
message = "%s requires a restart after installation." % packagename
print message
sys.stdout.flush()
log(message)
restartneeded = True
cmd = ['/usr/sbin/installer', '-verboseR', '-pkg', pkgpath, '-target', '/']
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while (p.poll() == None):
installinfo = p.stdout.readline()
if installinfo.startswith("installer:"):
# save all installer output in case there is
# an error so we can dump it to the log
installeroutput.append(installinfo)
msg = installinfo[10:].rstrip("\n")
if msg.startswith("PHASE:"):
phase = msg[6:]
if phase:
print phase
sys.stdout.flush()
elif msg.startswith("STATUS:"):
status = msg[7:]
if status:
print status
sys.stdout.flush()
elif msg.startswith("%"):
if options.ihookoutput:
percent = float(msg[1:])
percent = int(percent * 100)
print "%%%s Item %s of %s" % (percent, currentinstallable, installablecount)
if percent == 100:
overallpercentage = min(100,int(currentinstallable/installablecount * 100))
print "%%%s Item %s of %s" % (overallpercentage, currentinstallable, installablecount)
sys.stdout.flush()
elif msg.startswith(" Error"):
print msg
sys.stdout.flush()
print >>sys.stderr, msg
log(msg)
elif msg.startswith(" Cannot install"):
print msg
sys.stdout.flush()
print >>sys.stderr, msg
log(msg)
else:
print >>sys.stderr, msg
retcode = p.poll()
if retcode:
message = "Install of %s failed." % packagename
print >>sys.stderr, message
log(message)
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
for line in installeroutput:
print >>sys.stderr, " ", line.rstrip("\n")
log(line.rstrip("\n"))
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
restartneeded = False
else:
log("Install of %s was successful." % packagename)
return (retcode, restartneeded)
def installall(dirpath):
"""
Attempts to install all pkgs and mpkgs in a given directory.
Will mount dmg files and install pkgs and mpkgs found at the
root of any mountpoints.
"""
restartflag = False
installitems = os.listdir(dirpath)
for item in installitems:
itempath = os.path.join(dirpath, item)
if (item.endswith(".pkg") or item.endswith(".mpkg")):
(retcode, needsrestart) = install(itempath)
if needsrestart:
restartflag = True
if item.endswith(".dmg"):
mountpoints = mountdmg(itempath)
for mountpoint in mountpoints:
# install all the pkgs and mpkgs at the root
# of the mountpoint -- call us recursively!
needtorestart = installall(mountpoint)
if needtorestart:
restartflag = True
unmountdmg(mountpoint)
return restartflag
def installWithInfo(dirpath, installlist):
"""
Uses the installlist to install items in the
correct order.
"""
restartflag = False
for item in installlist:
if "installer_item" in item:
itempath = os.path.join(dirpath, item["installer_item"])
if not os.path.exists(itempath):
#can't install, so we should stop
return restartFlag
if (itempath.endswith(".pkg") or itempath.endswith(".mpkg")):
(retcode, needsrestart) = install(itempath)
if needsrestart:
restartflag = True
if itempath.endswith(".dmg"):
mountpoints = mountdmg(itempath)
for mountpoint in mountpoints:
# install all the pkgs and mpkgs at the root
# of the mountpoint -- call us recursively!
needtorestart = installall(mountpoint)
if needtorestart:
restartflag = True
unmountdmg(mountpoint)
# now remove the item from the install cache
# (using rm -f in case it's a bundle pkg)
retcode = subprocess.call(["/bin/rm", "-rf", itempath])
return restartflag
def processRemovals(removalList):
restartFlag = False
for item in removalList:
name = ""
if 'name' in item:
name = item['name']
elif 'catalogitem' in item:
name = item['catalogitem']
if 'uninstall_method' in item:
uninstallmethod = item['uninstall_method'].split(' ')
if uninstallmethod[0] == "removepackages":
if 'packages' in item:
if options.ihookoutput:
print "%TITLE Removing " + name + "..."
#print "%%%s Item %s of %s" % (0, currentinstallable, installablecount)
cmd = ['/Users/Shared/bin/removepackages', '-f']
if options.ihookoutput:
cmd.append('-i')
for package in item['packages']:
cmd.append(package)
uninstalleroutput = []
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while (p.poll() == None):
msg = p.stdout.readline()
# save all uninstaller output in case there is
# an error so we can dump it to the log
uninstalleroutput.append(msg)
msg = msg.rstrip("\n")
if msg.startswith("STATUS: "):
status = msg[8:]
if status:
print status
sys.stdout.flush()
elif msg.startswith("INFO: "):
info = msg[6:]
if info:
print >>sys.stderr, info
elif msg.startswith("ERROR: "):
error = msg[7:]
if error:
print >>sys.stderr, error
else:
print msg
sys.stdout.flush()
retcode = p.poll()
if retcode:
message = "Uninstall of %s failed." % name
print >>sys.stderr, message
log(message)
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
for line in uninstalleroutput:
print >>sys.stderr, " ", line.rstrip("\n")
log(line.rstrip("\n"))
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
else:
log("Uninstall of %s was successful." % name)
elif os.path.exists(uninstallmethod[0]) and os.access(uninstallmethod[0], os.X_OK):
# it's a script or program to uninstall
if options.ihookoutput:
print "%TITLE Running uninstall script for " + name + "..."
print "%BEGINPOLE"
sys.stdout.flush()
cmd = uninstallmethod
uninstalleroutput = []
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while (p.poll() == None):
msg = p.stdout.readline()
# save all uninstaller output in case there is
# an error so we can dump it to the log
uninstalleroutput.append(msg)
msg = msg.rstrip("\n")
if options.ihookoutput:
# redirect output to the iHook drawer
print >>sys.stderr, msg
else:
print msg
retcode = p.poll()
if retcode:
message = "Uninstall of %s failed." % name
print >>sys.stderr, message
log(message)
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
for line in uninstalleroutput:
print >>sys.stderr, " ", line.rstrip("\n")
log(line.rstrip("\n"))
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
else:
log("Uninstall of %s was successful." % name)
else:
log("Uninstall of %s failed because there was no valid uninstall method." % name)
return restartFlag
def mountdmg(dmgpath):
"""
Attempts to mount the dmg at dmgpath
and returns a list of mountpoints
"""
mountpoints = []
dmgname = os.path.basename(dmgpath)
print "Mounting disk image %s" % dmgname
log("Mounting disk image %s" % dmgname)
p = subprocess.Popen(['/usr/bin/hdiutil', 'attach', dmgpath, '-mountRandom', '/tmp', '-nobrowse', '-plist'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(plist, err) = p.communicate()
if plist:
pl = plistlib.readPlistFromString(plist)
for entity in pl['system-entities']:
if 'mount-point' in entity:
mountpoints.append(entity['mount-point'])
return mountpoints
def unmountdmg(mountpoint):
"""
Unmounts the dmg at mountpoint
"""
p = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
if err:
print >>sys.stderr, err
p = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint, '-force'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
# module (global) variables
managedinstallbase = managedinstalls.managed_install_dir()
installdir = os.path.join(managedinstallbase , 'Cache')
logdir = os.path.join(managedinstallbase, 'Logs')
installablecount = 0
currentinstallable = 0
p = optparse.OptionParser()
p.add_option('--ihookoutput', '-i', action='store_true')
options, arguments = p.parse_args()
def main():
global installdir
global installablecount
if options.ihookoutput:
print '%WINDOWSIZE 512 232'
print '%BACKGROUND /Users/Shared/Installer.png'
print '%BECOMEKEY'
print '%BEGINPOLE'
sys.stdout.flush()
needtorestart = False
#installablecount = countinstallcandidates(installdir)
#if installablecount:
installablecount = 5
log("### Beginning automated install session ###")
if os.path.exists(installdir):
installinfo = os.path.join(managedinstallbase, 'InstallInfo.plist')
if os.path.exists(installinfo):
try:
pl = plistlib.readPlist(installinfo)
except:
print >>sys.stderr, "Invalid %s" % installinfo
exit(-1)
if "removals" in pl:
log("Processing removals")
needtorestart = processRemovals(pl['removals'])
if "managed_installs" in pl:
log("Processing installs")
needtorestart = installWithInfo(installdir, pl['managed_installs'])
# remove the install info
os.unlink(installinfo)
else:
log("No %s found." % installinfo)
log("Installing everything in the cache.")
# install all pkgs and mpkgs
needtorestart = installall(installdir)
if needtorestart:
print "Software installed requires a restart."
log("Software installed requires a restart.")
sys.stdout.flush()
log("### End automated install session ###")
if needtorestart:
time.sleep(5)
# uncomment this when testing is done so it will restart.
#retcode = subprocess.call(["/sbin/shutdown", "-r", "now"])
if __name__ == '__main__':
main()
+9
View File
@@ -0,0 +1,9 @@
The actual tools are:
catalogcheck.py - gets the catalog for the client and processes it to determine what, if anything, needs to be installed.
installAtLogout.py - meant to be run as part of a logout hook. Does the actual installs.
makecatalogitem.py: Helper tool to help create catalogitem plists for each installer item.
Supporting library:
managedinstalls.py
+842
View File
@@ -0,0 +1,842 @@
#!/usr/bin/env python
# encoding: utf-8
"""
catalogcheck.py
Created by Greg Neagle on 2008-11-13.
"""
import sys
import os
import plistlib
import tempfile
import subprocess
from distutils import version
import urlparse
import optparse
import managedinstalls
# 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 == {}:
print "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 = 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
"""
if version.LooseVersion(thisvers) < version.LooseVersion(thatvers):
print "\tInstalled version is older (%s)." % thisvers
return -1
elif version.LooseVersion(thisvers) == version.LooseVersion(thatvers):
print "\tThis version is currently installed."
return 1
else:
print "\tA newer version is currently installed (%s)." % thisvers
return 2
def isSameOrNewerApplicationInstalled(app):
"""
app is a dict with application
bundle info
uses system profiler data to look for
an app that is the same or newer version
"""
name = bundleid = ''
versionstring = '0.0.0'
if 'CFBundleName' in app:
name = app['CFBundleName']
if 'CFBundleIdentifier' in app:
bundleid = app['CFBundleIdentifier']
if 'CFBundleShortVersionString' in app:
versionstring = app['CFBundleShortVersionString']
if name == '' and bundleid == '':
print "No application name or bundleid was specified!"
# return True so we don't install
return True
print "Looking for application %s with bundleid: %s, version %s..." % (name, bundleid, versionstring)
appinfo = []
appdata = getAppData()
if appdata:
for item in appdata:
if bundleid:
if 'path' in item:
if getAppBundleID(item['path']) == bundleid:
appinfo.append(item)
elif name:
if '_name' in item:
if item['_name'] == name:
appinfo.append(item)
for item in appinfo:
if '_name' in item:
print "\tName: \t %s" % item['_name'].encode("UTF-8")
if 'path' in item:
print "\tPath: \t %s" % item['path'].encode("UTF-8")
print "\tCFBundleIdentifier: \t %s" % getAppBundleID(item['path'])
if 'version' in item:
print "\tVersion: \t %s" % item['version'].encode("UTF-8")
if compareVersions(item['version'], versionstring) > 0:
return True
# if we got this far, we didn't find the same or newer
print "Did not find the same or newer application on the startup disk."
return False
def compareBundleVersion(item):
"""
Gets the CFBundleShortVersionString from the Info.plist
in bundlepath/Contents and compares versions.
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:
print "Missing bundle path or version!"
return -2
print "Checking %s for version %s..." % (filepath, vers)
if not os.path.exists(filepath):
print "\tNo Info.plist found at %s" % filepath
return 0
try:
pl = plistlib.readPlist(filepath)
except:
print "\t%s may not be a plist!" % filepath
return 0
if 'CFBundleShortVersionString' in pl:
installedvers = pl['CFBundleShortVersionString']
return compareVersions(installedvers, vers)
else:
print "\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:
print "Missing plist path or version!"
return -2
print "Checking %s for version %s..." % (filepath, vers)
if not os.path.exists(filepath):
print "\tNo plist found at %s" % filepath
return 0
try:
pl = plistlib.readPlist(filepath)
except:
print "\t%s may not be a plist!" % filepath
return 0
if 'CFBundleShortVersionString' in pl:
installedvers = pl['CFBundleShortVersionString']
return compareVersions(installedvers, vers)
else:
print "\tNo version info in %s." % filepath
return 0
def filesystemItemExists(item):
"""
Checks to see if a filesystem item exists
To do: add checksum support
"""
if 'path' in item:
filepath = item['path']
print "Checking existence of %s..." % filepath
if os.path.exists(filepath):
print "\tExists."
return 1
else:
print "\tDoes not exist."
return 0
else:
print "No path specified!"
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
print "Looking for package %s, version %s" % (pkgid, vers)
installedvers = managedinstalls.getInstalledPackageVersion(pkgid)
if installedvers:
return compareVersions(installedvers, vers)
else:
print "\tThis package is not currently installed."
return 0
def download_installeritem(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
print "Downloading %s from %s" % (pkgname, pkgurl)
tempfilepath = os.path.join(mytmpdir, pkgname)
result = managedinstalls.getfilefromhttpurl(pkgurl, tempfilepath, showprogress=True, ifmodifiedsince=itemmodtime)
if result == 0:
os.rename(tempfilepath, destinationpath)
return True
elif result == 304:
# not modified
print "Installer item %s is already in the install cache." % pkgname
return True
else:
print >>sys.stderr, "Error code: %s" % result
if os.path.exists(tempfilepath):
os.remove(tempfilepath)
print "Couldn't get %s: %s" % (pkgname, result)
return False
def isItemInInstallList(catalogitem_pl, thelist):
"""
Returns True if the filename from the download location
for catalogitem is already in the install list,
and therefore already scheduled to be installed.
"""
if 'installer_item_location' in catalogitem_pl:
location = catalogitem_pl['installer_item_location']
filename = os.path.split(location)[1]
for item in thelist:
if "installer_item" in item:
if filename == item['installer_item']:
return True
return False
def getCatalogItemDetail(item, defaultbranch=''):
managedinstallprefs = managedinstalls.prefs()
sw_repo_baseurl = managedinstallprefs['sw_repo_url']
managed_install_dir = managedinstallprefs['managed_install_dir']
catalogitempath = os.path.join(managed_install_dir, "catalogitems")
catalogbaseurl = sw_repo_baseurl + "/catalogitems"
if defaultbranch == '':
defaultbranch = "production"
itemname = os.path.split(item)[1]
itempath = os.path.join(catalogitempath, itemname)
if os.path.exists(itempath):
itemmodtime = os.stat(itempath).st_mtime
else:
itemmodtime = None
if item.startswith("/"):
# branch in item name
itemurl = catalogbaseurl + item
else:
# use default branch
itemurl = catalogbaseurl + "/" + defaultbranch + "/" + item
tempfilepath = os.path.join(mytmpdir, itemname)
print "Getting detail for %s from %s..." % (item, itemurl)
result = managedinstalls.getfilefromhttpurl(itemurl, tempfilepath, showprogress=True, ifmodifiedsince=itemmodtime)
if result == 0:
os.rename(tempfilepath, itempath)
elif result == 304:
# not modified, just return existing item
print "Item %s in local cache is up-to-date." % item
return itempath
else:
print >>sys.stderr, "Error code: %s" % result
if os.path.exists(tempfilepath):
os.remove(tempfilepath)
if os.path.exists(itempath):
return itempath
else:
print "Couldn't get detail for item %s: %s" % (item, result)
return ""
def processInstalls(catalogitem, defaultbranch, installlist):
"""
Processes a catalog 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 the
installlist (a list of filenames)
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']
catalogitempath = os.path.join(managed_install_dir, "catalogitems")
downloadbaseurl = sw_repo_baseurl + "/pkgs/"
catalogitemname = os.path.split(catalogitem)[1]
print "\nProcessing catalog item %s..." % catalogitemname
catalogitemdetail = getCatalogItemDetail(catalogitem, defaultbranch)
if not catalogitemdetail:
return False
try:
pl = plistlib.readPlist(catalogitemdetail)
except:
print >>sys.stderr, "%s is not a valid plist!" % catalogitem
return False
# check dependencies
dependenciesMet = True
if 'dependencies' in pl:
dependencies = pl['dependencies']
for item in dependencies:
print "%s is dependent on %s. Getting info on %s..." % (catalogitemname, item, item)
success = processInstalls(item, defaultbranch, installlist)
if not success:
dependenciesMet = False
if not dependenciesMet:
print "Didn't attempt to install %s because could not resolve all dependencies." % catalogitemname
return False
# check to see if item is already in the installlist:
if isItemInInstallList(pl, installlist):
print "%s is already scheduled to be installed." % catalogitemname
return True
needToInstall = False
if 'installs' in pl:
installitems = pl['installs']
for item in installitems:
if 'type' in item:
if item['type'] == 'application':
if not isSameOrNewerApplicationInstalled(item):
print "Need to install %s" % catalogitemname
needToInstall = True
# once we know we need to install this one,
# no need to keep checking
break
if item['type'] == 'bundle':
comparisonResult = compareBundleVersion(item)
if comparisonResult == -1 or comparisonResult == 0 :
# not there or older
print "Need to install %s" % catalogitemname
needToInstall = True
# once we know we need to install this one,
# no need to keep checking
break
if item['type'] == 'plist':
comparisonResult = comparePlistVersion(item)
if comparisonResult == -1 or comparisonResult == 0 :
# not there or older
print "Need to install %s" % catalogitemname
needToInstall = True
# once we know we need to install this one,
# no need to keep checking
break
if item['type'] == 'file':
if filesystemItemExists(item) == 0 :
# not there
print "Need to install %s" % catalogitemname
needToInstall = True
# once we know we need to install this one,
# no need to keep checking
break
elif 'receipts' in pl:
receipts = pl['receipts']
for item in receipts:
comparisonResult = compareReceiptVersion(item)
if comparisonResult == -1 or comparisonResult == 0 :
# not there or older
print "Need to install %s" % catalogitemname
needToInstall = True
# once we know we need to install this one,
# no need to keep checking
break
name = description = ""
try:
name = pl['name']
description = pl['description']
except:
pass
iteminfo = {}
iteminfo["name"] = name
iteminfo["catalogitem"] = catalogitemname
iteminfo["description"] = description
if needToInstall:
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
installlist.append(iteminfo)
return True
else:
iteminfo["installed"] = False
installlist.append(iteminfo)
return False
else:
print "Can't install %s because there's no download info for the installer item" % catalogitemname
iteminfo["installed"] = False
installlist.append(iteminfo)
return False
else:
print "No need to install %s" % catalogitemname
iteminfo["installed"] = True
installlist.append(iteminfo)
return True
def processCatalogForInstalls(catalogpath, listofinstalls=[]):
"""
Processes catalogs. Can be recursive if catalogs inlcude other catalogs.
Probably doesn't handle circular catalog references well...
"""
defaultbranch = getCatalogValueForKey(catalogpath, 'default_branch')
nestedcatalogs = getCatalogValueForKey(catalogpath, "included_catalogs")
if nestedcatalogs:
for item in nestedcatalogs:
nestedcatalogpath = getcatalog(item)
if nestedcatalogpath:
listofinstalls = processCatalogForInstalls(nestedcatalogpath, listofinstalls)
installitems = getCatalogValueForKey(catalogpath, "managed_installs")
if installitems:
for item in installitems:
result = processInstalls(item, defaultbranch, listofinstalls)
return listofinstalls
def processRemovals(catalogitem, defaultbranch, removallist):
"""
Processes a catalog item. Determines if it needs to be
removed, {{and if so, if any items dependent on it need to
be removed first.}} Items to be removed are added to the
removallist
{{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']
catalogitempath = os.path.join(managed_install_dir, "catalogitems")
downloadbaseurl = sw_repo_baseurl + "/pkgs/"
catalogitemname = os.path.split(catalogitem)[1]
print "\nProcessing catalog item %s..." % catalogitemname
catalogitemdetail = getCatalogItemDetail(catalogitem, defaultbranch)
if not catalogitemdetail:
return False
try:
pl = plistlib.readPlist(catalogitemdetail)
except:
print >>sys.stderr, "%s is not a valid plist!" % catalogitem
return False
# check for uninstall info
if not 'uninstallable' in pl or not pl['uninstallable']:
print "%s is not uninstallable." % catalogitemname
return False
if not 'uninstall_method' in pl:
print "No uninstall info for %s." % catalogitemname
return False
# check for dependent items
# look at all the items in the local catalogitem cache
# and see if any depend on the current item; if so
# we should remove them as well
dependentitemsremoved = True
for item in os.listdir(catalogitempath):
if item != catalogitemname:
try:
itempath = os.path.join(catalogitempath, item)
itempl = plistlib.readPlist(itempath)
if 'dependencies' in itempl:
if catalogitemname in itempl['dependencies']:
print "%s is dependent on %s and must be removed as well" % (item, catalogitemname)
success = processRemovals(item, defaultbranch, removallist)
if not success:
dependentitemsremoved = False
except:
pass
if not dependentitemsremoved:
print "Didn't attempt to remove %s because could not remove all items dependent on it." % catalogitemname
return False
# check to see if item is already in the removallist:
if isItemInInstallList(pl, removallist):
print "%s is already scheduled to be removed." % catalogitemname
return True
needToRemove = False
if 'installs' in pl:
installitems = pl['installs']
for item in installitems:
if 'type' in item:
if item['type'] == 'application':
if isSameOrNewerApplicationInstalled(item):
print "Need to remove %s" % catalogitemname
needToRemove = True
# once we know we need to remove this one,
# no need to keep checking
break
if item['type'] == 'bundle':
comparisonResult = compareBundleVersion(item)
if comparisonResult == 1:
# same version is installed
print "Need to remove %s" % catalogitemname
needToRemove = True
# once we know we need to remove this one,
# no need to keep checking
break
if item['type'] == 'plist':
comparisonResult = comparePlistVersion(item)
if comparisonResult == 1:
# same version is installed
print "Need to remove %s" % catalogitemname
needToRemove = True
# once we know we need to remove this one,
# no need to keep checking
break
if item['type'] == 'file':
if filesystemItemExists(item) == 1:
print "Need to remove %s" % catalogitemname
needToRemove = True
# once we know we need to remove this one,
# no need to keep checking
break
elif 'receipts' in pl:
receipts = pl['receipts']
for item in receipts:
comparisonResult = compareReceiptVersion(item)
if comparisonResult == 1:
# same version is installed
print "Need to remove %s" % catalogitemname
needToRemove = True
# once we know we need to remove this one,
# no need to keep checking
break
name = description = ""
try:
name = pl['name']
description = pl['description']
except:
pass
iteminfo = {}
iteminfo["name"] = name
iteminfo["catalogitem"] = catalogitemname
iteminfo["description"] = description
if needToRemove:
uninstallmethod = pl['uninstall_method']
if uninstallmethod == 'removepackages':
# build list of packages based on receipts
print "Building list of packages to remove"
packages = []
if 'receipts' in pl:
for item in pl['receipts']:
if compareReceiptVersion(item) == 1:
packages.append(item['packageid'])
iteminfo['packages'] = packages
iteminfo["uninstall_method"] = uninstallmethod
iteminfo["installed"] = True
removallist.append(iteminfo)
return True
else:
print "No need to remove %s" % catalogitemname
iteminfo["installed"] = False
removallist.append(iteminfo)
return True
def processCatalogForRemovals(catalogpath, listofremovals=[]):
"""
Processes catalogs for removals. Can be recursive if catalogs inlcude other catalogs.
Probably doesn't handle circular catalog references well...
"""
defaultbranch = getCatalogValueForKey(catalogpath, 'default_branch')
nestedcatalogs = getCatalogValueForKey(catalogpath, "included_catalogs")
if nestedcatalogs:
for item in nestedcatalogs:
nestedcatalogpath = getcatalog(item)
if nestedcatalogpath:
listofremovals = processCatalogForRemovals(nestedcatalogpath, listofremovals)
removalitems = getCatalogValueForKey(catalogpath, "managed_uninstalls")
if removalitems:
for item in removalitems:
result = processRemovals(item, defaultbranch, listofremovals)
return listofremovals
def getCatalogValueForKey(catalogpath, keyname):
try:
pl = plistlib.readPlist(catalogpath)
except:
print >>sys.stderr, "Could not read plist %s" % catalogpath
return None
if keyname in pl:
return pl[keyname]
else:
return None
def createDirsIfNeeded(dirlist):
for dir in dirlist:
if not os.path.exists(dir):
try:
os.mkdir(dir)
except:
print >>sys.stderr, "Could not create %s" % dir
return False
return True
def getcatalog(partialurl):
"""
Gets a catalog from the server
"""
managedinstallprefs = managedinstalls.prefs()
sw_repo_baseurl = managedinstallprefs['sw_repo_url']
catalog_dir = os.path.join(managedinstallprefs['managed_install_dir'], "catalogs")
if not createDirsIfNeeded([catalog_dir]):
exit(-1)
if partialurl.startswith("http"):
# then it's really a request for the main catalog
catalogurl = partialurl
catalogname = "MainCatalog.plist"
else:
# request for nested catalog
catalogname = os.path.split(partialurl)[1]
catalogurl = sw_repo_baseurl + "/catalogs/" + partialurl
catalogpath = os.path.join(catalog_dir, catalogname)
if os.path.exists(catalogpath):
catalogmodtime = os.stat(catalogpath).st_mtime
else:
catalogmodtime = None
tempfilepath = os.path.join(mytmpdir, catalogname)
print "Getting catalog %s from %s..." % (catalogname, catalogurl)
result = managedinstalls.getfilefromhttpurl(catalogurl, tempfilepath, showprogress=True, ifmodifiedsince=catalogmodtime)
if result == 0:
try:
os.rename(tempfilepath, catalogpath)
return catalogpath
except:
print >>sys.stderr, "Could not write to %s" % catalogpath
return ""
elif result == 304:
# not modified, do nothing
print "Catalog %s in local cache is up-to-date." % catalogname
return catalogpath
else:
print >>sys.stderr, "Error code: %s retreiving catalog %s" % (result, catalogname)
if os.path.exists(tempfilepath):
os.remove(tempfilepath)
return ""
def getMainCatalog(alternate_id):
"""
Gets the main client catalog (aka 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 getcatalog(manifesturl)
mytmpdir = ""
def main():
global mytmpdir
mytmpdir = tempfile.mkdtemp()
p = optparse.OptionParser()
p.add_option('--id', '-i', default='',
help='Alternate identifier for catalog retreival')
options, arguments = p.parse_args()
managedinstallprefs = managedinstalls.prefs()
managed_install_dir = managedinstallprefs['managed_install_dir']
catalogitemsdir = os.path.join(managed_install_dir, "catalogitems")
cachedir = os.path.join(managed_install_dir, "Cache")
if not createDirsIfNeeded([managed_install_dir, catalogitemsdir, cachedir]):
print >>sys.stderr, "No write access to managed install directory: %s" % managed_install_dir
exit(-1)
maincatalogpath = getMainCatalog(options.id)
if not maincatalogpath:
print >>sys.stderr, "Could not retreive managed install catalog."
exit(-1)
installlist = processCatalogForInstalls(maincatalogpath)
# 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 installlist:
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:
print "Removing %s from cache" % item
os.unlink(os.path.join(cachedir, item))
# now generate a list of items to be uninstalled
removallist = processCatalogForRemovals(maincatalogpath)
# need to write out install list so the autoinstaller
# can use it to install things in the right order
pldict = {}
pldict['managed_installs'] = installlist
pldict['removals'] = removallist
plistlib.writePlist(pldict, os.path.join(managed_install_dir, "InstallInfo.plist"))
# now clean up catalogitem dir, removing items no longer needed
currentcatalogitems = []
for item in installlist:
currentcatalogitems.append(item['catalogitem'])
for item in removallist:
currentcatalogitems.append(item['catalogitem'])
for item in os.listdir(catalogitemsdir):
if item not in currentcatalogitems:
os.unlink(os.path.join(catalogitemsdir,item))
try:
# clean up our tmp dir
os.rmdir(mytmpdir)
except:
# not fatal if it fails
pass
if __name__ == '__main__':
main()
+294
View File
@@ -0,0 +1,294 @@
#!/usr/bin/env python
"""
installomatic
Tool to automatically install pkgs, mpkgs, and dmgs
(containing pkgs and mpkgs) from a defined folder. Intended
to be run as part of a logout hook, but can be run manually
"""
import os
import subprocess
import sys
import time
import plistlib
import optparse
import managedinstalls
def log(message):
global logdir
logfile = os.path.join(logdir,'install.log')
f = open(logfile, mode='a', buffering=1)
if f:
print >>f, time.ctime(), message
f.close()
def countinstallcandidates(dirpath):
"""
Counts the number of pkgs, mpkgs, and dmgs
in dirpath
"""
candidatecount = 0
items = os.listdir(dirpath)
for item in items:
if (item.endswith(".pkg") or item.endswith(".mpkg") or item.endswith(".dmg")):
candidatecount += 1
return candidatecount
def install(pkgpath):
"""
Uses the apple installer to install the package or metapackage
at pkgpath. Prints status messages to STDOUT.
Returns the installer return code and true if a restart is needed.
"""
global installablecount
global currentinstallable
global options
currentinstallable += 1
restartneeded = False
installeroutput = []
cmd = ['/usr/sbin/installer', '-pkginfo', '-pkg', pkgpath]
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
packagename = output.splitlines()[0]
print >>sys.stderr, "Package name is", packagename
if options.ihookoutput:
print "%TITLE Installing " + packagename + "..."
print "%%%s Item %s of %s" % (0, currentinstallable, installablecount)
log("Installing %s from %s" % (packagename, os.path.basename(pkgpath)))
cmd = ['/usr/sbin/installer', '-query', 'RestartAction', '-pkg', pkgpath]
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
restartaction = output.rstrip("\n")
if restartaction == "RequireRestart":
message = "%s requires a restart after installation." % packagename
print message
sys.stdout.flush()
log(message)
restartneeded = True
cmd = ['/usr/sbin/installer', '-verboseR', '-pkg', pkgpath, '-target', '/']
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while (p.poll() == None):
installinfo = p.stdout.readline()
if installinfo.startswith("installer:"):
# save all installer output in case there is
# an error so we can dump it to the log
installeroutput.append(installinfo)
msg = installinfo[10:].rstrip("\n")
if msg.startswith("PHASE:"):
phase = msg[6:]
if phase:
print phase
sys.stdout.flush()
elif msg.startswith("STATUS:"):
status = msg[7:]
if status:
print status
sys.stdout.flush()
elif msg.startswith("%"):
if options.ihookoutput:
percent = float(msg[1:])
percent = int(percent * 100)
print "%%%s Item %s of %s" % (percent, currentinstallable, installablecount)
if percent == 100:
overallpercentage = min(100,int(currentinstallable/installablecount * 100))
print "%%%s Item %s of %s" % (overallpercentage, currentinstallable, installablecount)
sys.stdout.flush()
elif msg.startswith(" Error"):
print msg
sys.stdout.flush()
print >>sys.stderr, msg
log(msg)
elif msg.startswith(" Cannot install"):
print msg
sys.stdout.flush()
print >>sys.stderr, msg
log(msg)
else:
print >>sys.stderr, msg
retcode = p.poll()
if retcode:
message = "Install of %s failed." % packagename
print >>sys.stderr, message
log(message)
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
for line in installeroutput:
print >>sys.stderr, " ", line.rstrip("\n")
log(line.rstrip("\n"))
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
restartneeded = False
else:
log("Install of %s was successful." % packagename)
return (retcode, restartneeded)
def installall(dirpath):
"""
Attempts to install all pkgs and mpkgs in a given directory.
Will mount dmg files and install pkgs and mpkgs found at the
root of any mountpoints.
"""
restartflag = False
installitems = os.listdir(dirpath)
for item in installitems:
itempath = os.path.join(dirpath, item)
if (item.endswith(".pkg") or item.endswith(".mpkg")):
(retcode, needsrestart) = install(itempath)
if needsrestart:
restartflag = True
if item.endswith(".dmg"):
mountpoints = mountdmg(itempath)
for mountpoint in mountpoints:
# install all the pkgs and mpkgs at the root
# of the mountpoint -- call us recursively!
needtorestart = installall(mountpoint)
if needtorestart:
restartflag = True
unmountdmg(mountpoint)
return restartflag
def installWithInfo(dirpath, installlist):
"""
Uses the installlist to install items in the
correct order.
"""
restartflag = False
for item in installlist:
itempath = os.path.join(dirpath, item)
if not os.path.exists(itempath):
#can't install, so we should stop
return restartFlag
if (item.endswith(".pkg") or item.endswith(".mpkg")):
(retcode, needsrestart) = install(itempath)
if needsrestart:
restartflag = True
if item.endswith(".dmg"):
mountpoints = mountdmg(itempath)
for mountpoint in mountpoints:
# install all the pkgs and mpkgs at the root
# of the mountpoint -- call us recursively!
needtorestart = installall(mountpoint)
if needtorestart:
restartflag = True
unmountdmg(mountpoint)
# now remove the item from the install cache
# (using rm -f in case it's a bundle pkg)
retcode = subprocess.call(["/bin/rm", "-rf", itempath])
return restartflag
def mountdmg(dmgpath):
"""
Attempts to mount the dmg at dmgpath
and returns a list of mountpoints
"""
mountpoints = []
dmgname = os.path.basename(dmgpath)
print "Mounting disk image %s" % dmgname
log("Mounting disk image %s" % dmgname)
p = subprocess.Popen(['/usr/bin/hdiutil', 'attach', dmgpath, '-mountRandom', '/tmp', '-nobrowse', '-plist'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(plist, err) = p.communicate()
if plist:
pl = plistlib.readPlistFromString(plist)
for entity in pl['system-entities']:
if 'mount-point' in entity:
mountpoints.append(entity['mount-point'])
return mountpoints
def unmountdmg(mountpoint):
"""
Unmounts the dmg at mountpoint
"""
p = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
if err:
print >>sys.stderr, err
p = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint, '-force'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
# module (global) variables
managedinstallbase = managedinstalls.managed_install_dir()
installdir = os.path.join(managedinstallbase , 'Cache')
logdir = os.path.join(managedinstallbase, 'Logs')
installablecount = 0
currentinstallable = 0
p = optparse.OptionParser()
p.add_option('--ihookoutput', '-i', action='store_true')
options, arguments = p.parse_args()
def main():
global installdir
global installablecount
if options.ihookoutput:
print '%WINDOWSIZE 512 232'
print '%BACKGROUND /Users/Shared/Installer.png'
print '%BECOMEKEY'
print '%BEGINPOLE'
sys.stdout.flush()
needtorestart = False
installablecount = countinstallcandidates(installdir)
if installablecount:
log("### Beginning automated install session ###")
if os.path.exists(installdir):
installinfo = os.path.join(managedinstallbase, 'InstallInfo.plist')
if os.path.exists(installinfo):
try:
pl = plistlib.readPlist(installinfo)
except:
print >>sys.stderr, "Invalid %s" % installinfo
exit(0)
if "install" in pl:
needtorestart = installWithInfo(installdir, pl['install'])
# remove the install info
os.unlink(installinfo)
else:
print "No %s found." % installinfo
# install all pkgs and mpkgs
needtorestart = installall(installdir)
if needtorestart:
print "Software installed requires a restart."
log("Software installed requires a restart.")
sys.stdout.flush()
log("### End automated install session ###")
if needtorestart:
time.sleep(5)
# uncomment this when testing is done so it will restart.
#retcode = subprocess.call(["/sbin/shutdown", "-r", "now"])
if __name__ == '__main__':
main()
+242
View File
@@ -0,0 +1,242 @@
#!/usr/bin/env python
# encoding: utf-8
"""
makecatalogitem.py
Created by Greg Neagle on 2008-11-25.
Creates a managed install catalog item plist given an Installer item:
a .pkg, a .mpkg, or a .dmg containing a .pkg or .mpkg
at the root of the mounted disk image.
You may also pass additional items that are installed by the package. These
are added to the 'installs' key of the catalog item plist and are used when
processing the catalog to check if the package needs to be installed or
reinstalled.
The generated plist is printed to STDOUT.
Usage: makecatalogitem /path/to/package_or_dmg [-f /path/to/item/it/installs ...]
"""
import sys
import os
import re
import optparse
from distutils import version
import plistlib
import subprocess
import managedinstalls
def mountdmg(dmgpath):
"""
Attempts to mount the dmg at dmgpath
and returns a list of mountpoints
"""
mountpoints = []
dmgname = os.path.basename(dmgpath)
p = subprocess.Popen(['/usr/bin/hdiutil', 'attach', dmgpath, '-mountRandom', '/tmp', '-nobrowse', '-plist'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(plist, err) = p.communicate()
if err:
print >>sys.stderr, "Error %s mounting %s." % (err, dmgpath)
if plist:
pl = plistlib.readPlistFromString(plist)
for entity in pl['system-entities']:
if 'mount-point' in entity:
mountpoints.append(entity['mount-point'])
return mountpoints
def unmountdmg(mountpoint):
"""
Unmounts the dmg at mountpoint
"""
p = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
if err:
print >>sys.stderr, err
p = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint, '-force'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
def nameAndVersion(s):
"""
Splits a string into the name and version numbers:
'TextWrangler2.3b1' becomes ('TextWrangler', '2.3b1')
'AdobePhotoshopCS3-11.2.1' becomes ('AdobePhotoshopCS3', '11.2.1')
'MicrosoftOffice2008v12.2.1' becomes ('MicrosoftOffice2008', '12.2.1')
"""
index = 0
for char in s:
if char in "0123456789":
possibleVersion = s[index:]
if not (" " in possibleVersion or "_" in possibleVersion or "-" in possibleVersion or "v" in possibleVersion):
return (s[0:index].rstrip(" .-_v"), possibleVersion)
index += 1
# no version number found, just return original string and empty string
return (s, '')
def getCatalogInfo(pkgitem):
info = managedinstalls.getPkgInfo(pkgitem)
highestpkgversion = "0.0"
for infoitem in info:
if version.LooseVersion(infoitem['version']) > version.LooseVersion(highestpkgversion):
highestpkgversion = infoitem['version']
name = os.path.split(pkgitem)[1]
shortname = os.path.splitext(name)[0]
metaversion = nameAndVersion(shortname)[1]
if not len(metaversion):
metaversion = highestpkgversion
cataloginfo = {}
cataloginfo['name'] = nameAndVersion(shortname)[0]
cataloginfo['version'] = metaversion
cataloginfo['description'] = ""
cataloginfo['receipts'] = []
for infoitem in info:
pkginfo = {}
pkginfo['packageid'] = infoitem['id']
pkginfo['version'] = infoitem['version']
cataloginfo['receipts'].append(pkginfo)
return cataloginfo
def getCatalogInfoFromDmg(dmgpath):
cataloginfo = None
mountpoints = mountdmg(dmgpath)
for mountpoint in mountpoints:
for fsitem in os.listdir(mountpoint):
itempath = os.path.join(mountpoint, fsitem)
if itempath.endswith('.pkg') or itempath.endswith('.mpkg'):
cataloginfo = getCatalogInfo(itempath)
# get out of fsitem loop
break
if cataloginfo:
# get out of moutpoint loop
break
#unmount all the mountpoints from the dmg
for mountpoint in mountpoints:
unmountdmg(mountpoint)
return cataloginfo
def getBundleInfo(path):
"""
Returns Info.plist data if available
for bundle at path
"""
infopath = os.path.join(path, "Contents", "Info.plist")
if not os.path.exists(infopath):
infopath = os.path.join(path, "Resources", "Info.plist")
if os.path.exists(infopath):
try:
pl = plistlib.readPlist(infopath)
return pl
except:
pass
return None
def getiteminfo(itempath):
infodict = {}
if itempath.endswith('.app'):
infodict['type'] = 'application'
pl = getBundleInfo(itempath)
if 'CFBundleName' in pl:
infodict['CFBundleName'] = pl['CFBundleName']
if 'CFBundleIdentifier' in pl:
infodict['CFBundleIdentifier'] = pl['CFBundleIdentifier']
if 'CFBundleShortVersionString' in pl:
infodict['CFBundleShortVersionString'] = pl['CFBundleShortVersionString']
if 'LSMinimumSystemVersion' in pl:
infodict['minosversion'] = pl['LSMinimumSystemVersion']
elif 'SystemVersionCheck:MinimumSystemVersion' in pl:
infodict['minosversion'] = pl['SystemVersionCheck:MinimumSystemVersion']
elif os.path.exists(os.path.join(itempath,'Contents','Info.plist')) or os.path.exists(os.path.join(itempath,'Resources','Info.plist')):
infodict['type'] = 'bundle'
infodict['path'] = itempath
pl = getBundleInfo(itempath)
if 'CFBundleShortVersionString' in pl:
infodict['CFBundleShortVersionString'] = pl['CFBundleShortVersionString']
elif itempath.endswith("Info.plist") or itempath.endswith("version.plist"):
infodict['type'] = 'plist'
infodict['path'] = itempath
try:
pl = plistlib.readPlist(itempath)
if 'CFBundleShortVersionString' in pl:
infodict['CFBundleShortVersionString'] = pl['CFBundleShortVersionString']
except:
pass
if not 'CFBundleShortVersionString' in infodict:
infodict['type'] = 'file'
infodict['path'] = itempath
return infodict
def main():
usage = "usage: %prog [options] /path/to/installeritem"
p = optparse.OptionParser(usage=usage)
p.add_option('--file', '-f', action="append",
help='Path to a filesystem item installed by this package. Can be specified multiple times.')
options, arguments = p.parse_args()
if len(arguments) == 0:
print >>sys.stderr, "Need to specify an installer item (.pkg, .mpkg, .dmg)!"
exit(-1)
if len(arguments) > 1:
print >>sys.stderr, "Can process only one installer item at a time. Ignoring additional installer items."
item = arguments[0].rstrip("/")
if os.path.exists(item):
if item.endswith('.dmg'):
catinfo = getCatalogInfoFromDmg(item)
elif item.endswith('.pkg') or item.endswith('.mpkg'):
catinfo = getCatalogInfo(item)
else:
print >>sys.stderr, "%s is not an installer package!" % item
exit(-1)
if catinfo:
minosversion = ""
if options.file:
installs = []
for fitem in options.file:
if os.path.exists(fitem):
iteminfodict = getiteminfo(fitem)
if 'minosversion' in iteminfodict:
thisminosversion = iteminfodict.pop('minosversion')
if not minosversion:
minosversion = thisminosversion
elif version.LooseVersion(thisminosversion) < version.LooseVersion(minosversion):
minosversion = thisminosversion
installs.append(iteminfodict)
else:
print >>sys.stderr, "Item %s doesn't exist. Skipping." % fitem
catinfo['installs'] = installs
name = os.path.split(item)[1]
catinfo['installer_item_location'] = name
if minosversion:
catinfo['minimum_os_version'] = minosversion
else:
catinfo['minimum_os_version'] = "10.4.0"
# and now, what we've all been waiting for...
print plistlib.writePlistToString(catinfo)
if __name__ == '__main__':
main()
+407
View File
@@ -0,0 +1,407 @@
#!/usr/bin/env python
# encoding: utf-8
"""
managedinstallslib.py
Created by Greg Neagle on 2008-11-18.
Copyright (c) 2008 Walt Disney Animation Studios. All rights reserved.
Common functions used by the managedinstalls tools.
"""
import sys
import os
import plistlib
import urllib2
import urlparse
import time
import calendar
import subprocess
import tempfile
import shutil
from xml.dom import minidom
#####################################################
# managed installs preferences/metadata
#####################################################
def getManagedInstallsPrefs():
# define default values
prefs = {}
prefs['managed_install_dir'] = "/Library/Managed Installs"
prefs['manifest_url'] = "http:/managedinstalls/cgi-bin/getmanifest"
prefs['sw_repo_url'] = "http://managedinstalls/swrepo"
prefs['client_identifier'] = ""
prefsfile = "/Library/Preferences/ManagedInstalls.plist"
if os.path.exists(prefsfile):
try:
pl = plistlib.readPlist(prefsfile)
except:
pass
if pl:
if 'managed_install_dir' in pl:
prefs['managed_install_dir'] = pl['managed_install_dir']
if 'manifest_url' in pl:
prefs['manifest_url'] = pl['manifest_url']
if 'sw_repo_url' in pl:
prefs['sw_repo_url'] = pl['sw_repo_url']
if 'client_identifier' in pl:
prefs['client_identifier'] = pl['client_identifier']
return prefs
def managed_install_dir():
prefs = getManagedInstallsPrefs()
return prefs['managed_install_dir']
def manifest_url():
prefs = getManagedInstallsPrefs()
return prefs['manifest_url']
def sw_repo_url():
prefs = getManagedInstallsPrefs()
return prefs['sw_repo_url']
def pref(prefname):
prefs = getManagedInstallsPrefs()
if prefname in prefs:
return prefs[prefname]
else:
return ''
def prefs():
return getManagedInstallsPrefs()
#####################################################
# Apple package utilities
#####################################################
def normalizeVersion(majorVersion, minorVersion="0"):
majorVersionParts = majorVersion.split(".")
if len(majorVersionParts) == 5 and minorVersion == "0":
minorVersion = majorVersionParts[4]
while len(majorVersionParts) < 3:
majorVersionParts.append("0")
version = majorVersionParts[0:3]
version.append(str(minorVersion))
return ".".join(version)
def parsePkgRefs(filename):
info = []
dom = minidom.parse(filename)
pkgrefs = dom.getElementsByTagName("pkg-ref")
if pkgrefs:
for ref in pkgrefs:
keys = ref.attributes.keys()
if 'id' in keys and 'version' in keys:
if debug:
for key in keys:
print key, "=>", ref.attributes[key].value.encode('UTF-8')
pkginfo = {}
pkginfo['id'] = ref.attributes['id'].value.encode('UTF-8')
pkginfo['version'] = normalizeVersion(ref.attributes['version'].value.encode('UTF-8'))
if not pkginfo in info:
info.append(pkginfo)
else:
pkgrefs = dom.getElementsByTagName("pkg-info")
if pkgrefs:
for ref in pkgrefs:
keys = ref.attributes.keys()
if 'identifier' in keys and 'version' in keys:
if debug:
for key in keys:
print key, "=>", ref.attributes[key].value.encode('UTF-8')
pkginfo = {}
pkginfo['id'] = ref.attributes['identifier'].value.encode('UTF-8')
pkginfo['version'] = normalizeVersion(ref.attributes['version'].value.encode('UTF-8'))
if not pkginfo in info:
info.append(pkginfo)
return info
def getFlatPackageInfo(pkgpath):
"""
returns array of dictionaries with info on packages
contained in the flat package
"""
infoarray = []
mytmpdir = tempfile.mkdtemp()
os.chdir(mytmpdir)
p = subprocess.Popen(["/usr/bin/xar", "-xf", pkgpath, "--exclude", "Payload"])
returncode = p.wait()
if returncode == 0:
currentdir = mytmpdir
packageinfofile = os.path.join(currentdir, "PackageInfo")
if os.path.exists(packageinfofile):
infoarray = parsePkgRefs(packageinfofile)
else:
distributionfile = os.path.join(currentdir, "Distribution")
if os.path.exists(distributionfile):
infoarray = parsePkgRefs(distributionfile)
shutil.rmtree(mytmpdir)
return infoarray
def getBundlePackageInfo(pkgpath):
infoarray = []
pkginfo = {}
if pkgpath.endswith(".pkg"):
plistpath = os.path.join(pkgpath, "Contents", "Info.plist")
if os.path.exists(plistpath):
pl = plistlib.readPlist(plistpath)
if debug:
for key in pl:
print key, "=>", pl[key]
if "CFBundleIdentifier" in pl:
pkginfo['id'] = pl["CFBundleIdentifier"];
if "CFBundleShortVersionString" in pl:
majorVersion = pl["CFBundleShortVersionString"]
minorVersion = "0"
if "IFMinorVersion" in pl:
minorVersion = str(pl["IFMinorVersion"])
pkginfo['version'] = normalizeVersion(majorVersion, minorVersion)
infoarray.append(pkginfo)
return infoarray
bundlecontents = os.path.join(pkgpath, "Contents")
if os.path.exists(bundlecontents):
for item in os.listdir(bundlecontents):
if item.endswith(".dist"):
filename = os.path.join(bundlecontents, item)
infoarray = parsePkgRefs(filename)
return infoarray
return infoarray
def getPkgInfo(p):
info = []
if p.endswith(".pkg") or p.endswith(".mpkg"):
if debug:
print "Examining %s" % p
if os.path.isfile(p): # new flat package
info = getFlatPackageInfo(p)
if os.path.isdir(p): # bundle-style package?
info = getBundlePackageInfo(p)
return info
def examinePackage(p):
info = []
if p.endswith(".pkg") or p.endswith(".mpkg"):
if debug:
print "Examining %s" % p
if os.path.isfile(p): # new flat package
info = getFlatPackageInfo(p)
if os.path.isdir(p): # bundle-style package?
info = getBundlePackageInfo(p)
if len(info) == 0:
print >>sys.stderr, "Can't determine bundle ID of %s." % p
return
# print info
for pkg in info:
#print pkg
pkg_id = pkg['id']
vers = pkg['version']
print "packageid: %s \t version: %s" % (pkg_id, vers)
else:
print >>sys.stderr, "%s doesn't appear to be an Installer package." % p
def getInstalledPackageVersion(pkgid):
"""
Checks a package id against the receipts to
determine if a package is already installed.
Returns the version string of the installed pkg
if it exists, or an empty string if it does not
"""
# Check /Library/Receipts
receiptsdir = "/Library/Receipts"
if os.path.exists(receiptsdir):
installitems = os.listdir(receiptsdir)
for item in installitems:
if item.endswith(".pkg"):
info = getBundlePackageInfo(os.path.join(receiptsdir, item))
if len(info):
infoitem = info[0]
foundbundleid = infoitem['id']
foundvers = infoitem['version']
if pkgid == foundbundleid:
return foundvers
# If we got to this point, we haven't found the pkgid yet.
# Now check new (Leopard) package database
p = subprocess.Popen(["/usr/sbin/pkgutil", "--pkg-info-plist", pkgid], bufsize=1,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
if out:
pl = plistlib.readPlistFromString(out)
if "pkgid" in pl:
foundbundleid = pl["pkgid"];
if "pkg-version" in pl:
foundvers = pl["pkg-version"]
if pkgid == foundbundleid:
return normalizeVersion(foundvers)
# This package does not appear to be currently installed
return ""
#
# Handles http downloads for the managed installer tools.
#
# 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.
#
def getsteps(num_of_steps, limit):
"""
Helper function for display_percent_done
"""
steps = []
current = 0.0
for i in range(0,num_of_steps):
if i == num_of_steps-1:
steps.append(int(round(limit)))
else:
steps.append(int(round(current)))
current += float(limit)/float(num_of_steps-1)
return steps
def display_percent_done(current,maximum):
"""
Mimics the command-line progress meter seen in some
of Apple's tools (like softwareupdate)
"""
step = getsteps(16, maximum)
output = ''
indicator = ['\t0','.','.','20','.','.','40','.','.',
'60','.','.','80','.','.','100\n']
for i in range(0,16):
if current == step[i]:
output += indicator[i]
if output:
sys.stdout.write(output)
sys.stdout.flush()
def httpDownload(url, filename, headers={}, postData=None, reporthook=None):
reqObj = urllib2.Request(url, postData, headers)
fp = urllib2.urlopen(reqObj)
headers = fp.info()
#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,showprogress=False,ifmodifiedsince=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 showprogress and (file_size > 0):
max_blocks = file_size/block_size
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)
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
except IOError, err:
return err
except:
return (-1, "Unexpected error")
return 0
debug = False
def main():
pass
if __name__ == '__main__':
main()