mirror of
https://github.com/munki/munki.git
synced 2026-03-15 22:31:22 -05:00
installcheck replaces catalogcheck.py. installcheck supports the new catalog format and the new dependencies. Cleaned up output and logging. ManagedInstaller and removepackages tweaked for better logging and MunkiStatus output. Removed the logout hook examples (for now) makecatalogitem is now makepkginfo New makecatalogs tool. git-svn-id: http://munki.googlecode.com/svn/trunk@50 a4e17f2e-e282-11dd-95e1-755cbddbdd66
348 lines
12 KiB
Python
Executable File
348 lines
12 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
#
|
|
# Copyright 2009 Greg Neagle.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""
|
|
makepkginfo
|
|
|
|
Created by Greg Neagle on 2008-11-25.
|
|
Creates a managed install pkg info 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: makepkginfo /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 hashlib
|
|
|
|
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):
|
|
"""
|
|
The core function here. Queries an installer item (.pkg, .mpkg)
|
|
and gets metadata. There are a lot of valid Apple package formats
|
|
and this function may not deal with them all equally well.
|
|
Standard bundle packages are probably the best understood and documented,
|
|
so this code deals with those pretty well.
|
|
|
|
metadata items include:
|
|
installer_item_size: size of the installer item (.dmg, .pkg, etc)
|
|
installed_size: size of items that will be installed
|
|
RestartAction: will a restart be needed after installation?
|
|
name
|
|
version
|
|
description
|
|
receipts: an array of packageids that may be installed (some may be optional)
|
|
"""
|
|
installedsize = 0
|
|
installerinfo = managedinstalls.getInstallerPkgInfo(pkgitem)
|
|
info = managedinstalls.getPkgInfo(pkgitem)
|
|
|
|
highestpkgversion = "0.0"
|
|
for infoitem in info:
|
|
if version.LooseVersion(infoitem['version']) > version.LooseVersion(highestpkgversion):
|
|
highestpkgversion = infoitem['version']
|
|
if "installed_size" in infoitem:
|
|
# note this is in KBytes
|
|
installedsize += infoitem['installed_size']
|
|
|
|
name = os.path.split(pkgitem)[1]
|
|
shortname = os.path.splitext(name)[0]
|
|
metaversion = nameAndVersion(shortname)[1]
|
|
if not len(metaversion):
|
|
metaversion = highestpkgversion
|
|
elif highestpkgversion.startswith(metaversion):
|
|
#for example, highestpkgversion is 2.0.3124.0, version in filename is 2.0
|
|
metaversion = highestpkgversion
|
|
|
|
if 'installed_size' in installerinfo:
|
|
if installerinfo['installed_size'] > 0:
|
|
installedsize = installerinfo['installed_size']
|
|
|
|
cataloginfo = {}
|
|
cataloginfo['name'] = nameAndVersion(shortname)[0]
|
|
cataloginfo['version'] = metaversion
|
|
for key in ('display_name', 'RestartAction', 'description'):
|
|
if key in installerinfo:
|
|
cataloginfo[key] = installerinfo[key]
|
|
|
|
if installedsize > 0:
|
|
cataloginfo['installed_size'] = installedsize
|
|
|
|
cataloginfo['receipts'] = []
|
|
for infoitem in info:
|
|
pkginfo = {}
|
|
pkginfo['packageid'] = infoitem['id']
|
|
pkginfo['version'] = infoitem['version']
|
|
cataloginfo['receipts'].append(pkginfo)
|
|
return cataloginfo
|
|
|
|
|
|
def getCatalogInfoFromDmg(dmgpath):
|
|
"""
|
|
* Mounts a disk image
|
|
* Gets catalog info for the first installer item found at the root level.
|
|
* Unmounts the disk image
|
|
|
|
To-do: handle multiple installer items on a disk image
|
|
"""
|
|
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 getmd5hash(filename):
|
|
"""
|
|
Returns hex of MD5 checksum of a file
|
|
"""
|
|
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 getiteminfo(itempath):
|
|
"""
|
|
Gets info for filesystem items passed to makecatalog item, to be used for
|
|
the "installs" key.
|
|
Determines if the item is an application, bundle, Info.plist, or a file or directory
|
|
and gets additional metadata for later comparison.
|
|
"""
|
|
infodict = {}
|
|
if itempath.endswith('.app'):
|
|
infodict['type'] = 'application'
|
|
infodict['path'] = itempath
|
|
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
|
|
if os.path.isfile(itempath):
|
|
infodict['md5checksum'] = getmd5hash(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):
|
|
# get size of installer item
|
|
itemsize = 0
|
|
if os.path.isfile(item):
|
|
itemsize = int(os.path.getsize(item))
|
|
if os.path.isdir(item):
|
|
# need to walk the dir and add it all up
|
|
for (path, dirs, files) in os.walk(item):
|
|
for f in files:
|
|
filename = os.path.join(path, f)
|
|
# use os.lstat so we don't follow symlinks
|
|
itemsize += int(os.lstat(filename).st_size)
|
|
|
|
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:
|
|
catinfo['installer_item_size'] = int(itemsize/1024)
|
|
minosversion = ""
|
|
if options.file:
|
|
installs = []
|
|
for fitem in options.file:
|
|
# no trailing slashes, please.
|
|
fitem = fitem.rstrip('/')
|
|
if fitem.startswith('/Library/Receipts'):
|
|
# no receipts, please!
|
|
print >>sys.stderr, "Item %s appears to be a receipt. Skipping." % fitem
|
|
continue
|
|
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()
|
|
|