Files
munki/code/client/makepkginfo
Greg Neagle 121ecb0484 munkicommon: pylint cleanup, better handling of items in the pkgutil database that don't have a version number (I didn't know this was possible!).
appleupdates: pylint cleanup; provide installed_size data for Managed Software Update display

managedsoftwareupdate: move "Checking for available Apple Software Updates..." message to appleupdates.checkForSoftwareUpdates()

makepkginfo: fix installer_item_size determination when we add up the contents of a directory (was bytes, now Kbytes like everything else)

git-svn-id: http://munki.googlecode.com/svn/trunk@725 a4e17f2e-e282-11dd-95e1-755cbddbdd66
2010-09-07 18:04:40 +00:00

552 lines
23 KiB
Python
Executable File

#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2010 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 optparse import OptionValueError
from distutils import version
import subprocess
from munkilib import munkicommon
from munkilib import FoundationPlist
from munkilib import adobeutils
def DMGhasSLA(dmgpath):
'''Returns true if dmg has a Software License Agreement.
These dmgs cannot be attached without user intervention'''
hasSLA = False
proc = subprocess.Popen(
['/usr/bin/hdiutil', 'imageinfo', dmgpath, '-plist'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(pliststr, err) = proc.communicate()
if err:
print >> sys.stderr, ("hdiutil error %s with image %s."
% (err, dmgpath))
if pliststr:
try:
plist = FoundationPlist.readPlistFromString(pliststr)
properties = plist.get('Properties')
if properties:
hasSLA = properties.get('Software License Agreement', False)
except FoundationPlist.NSPropertyListSerializationException:
pass
return hasSLA
def getCatalogInfoFromDmg(dmgpath, options):
"""
* 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(?)
"""
if DMGhasSLA(dmgpath):
print >> sys.stderr, \
"%s has an attached Software License Agreement." % dmgpath
print >> sys.stderr, \
"It cannot be automatically mounted. You'll need to create a new dmg."
exit(-1)
cataloginfo = None
mountpoints = munkicommon.mountdmg(dmgpath)
if not mountpoints:
print >> sys.stderr, "Could not mount %s!" % dmgpath
exit(-1)
if options.pkgname:
pkgpath = os.path.join(mountpoints[0], options.pkgname)
if os.path.exists(pkgpath):
cataloginfo = munkicommon.getPackageMetaData(pkgpath)
if cataloginfo:
cataloginfo['package_path'] = options.pkgname
elif not options.item:
# search for first package at root
for fsitem in os.listdir(mountpoints[0]):
itempath = os.path.join(mountpoints[0], fsitem)
if itempath.endswith('.pkg') or itempath.endswith('.mpkg'):
cataloginfo = munkicommon.getPackageMetaData(itempath)
# get out of fsitem loop
break
if cataloginfo:
# we found a package, but let's see if it's an Adobe CS5 install
# (AAMEE) package
if 'receipts' in cataloginfo:
try:
pkgid = cataloginfo['receipts'][0].get('packageid')
except IndexError:
pkgid = ""
if pkgid.startswith("com.adobe.Enterprise.install"):
# we have an Adobe CS5 install package, process
# as Adobe install
adobepkgname = cataloginfo['receipts'][0].get('filename')
cataloginfo = adobeutils.getAdobeCatalogInfo(
mountpoints[0], adobepkgname)
else:
# maybe an Adobe installer/updater/patcher?
cataloginfo = adobeutils.getAdobeCatalogInfo(mountpoints[0],
options.pkgname or '')
if not cataloginfo:
# maybe this is a drag-n-drop dmg
# look for given item or an app at the top level of the dmg
iteminfo = {}
if options.item:
item = options.item
itempath = os.path.join(mountpoints[0], item)
if os.path.exists(itempath):
iteminfo = getiteminfo(itempath)
else:
print >> sys.stderr, \
"%s not found on disk image." % item
else:
# no item specified; look for an application at root of
# mounted dmg
item = ''
for itemname in os.listdir(mountpoints[0]):
itempath = os.path.join(mountpoints[0], itemname)
if munkicommon.isApplication(itempath):
item = itemname
iteminfo = getiteminfo(itempath)
if iteminfo:
break
if iteminfo:
if options.destinationpath:
iteminfo['path'] = os.path.join(options.destinationpath,
item)
else:
iteminfo['path'] = os.path.join("/Applications", item)
cataloginfo = {}
cataloginfo['name'] = iteminfo.get('CFBundleName',
os.path.splitext(item)[0])
cataloginfo['version'] = \
munkicommon.padVersionString(
iteminfo.get('CFBundleShortVersionString', "0")
,5)
cataloginfo['installs'] = [iteminfo]
if options.appdmg:
cataloginfo['installer_type'] = "appdmg"
cataloginfo['uninstallable'] = True
cataloginfo['uninstall_method'] = "remove_app"
else:
cataloginfo['installer_type'] = "copy_from_dmg"
item_to_copy = {}
item_to_copy['source_item'] = item
item_to_copy['destination_path'] = \
options.destinationpath or "/Applications"
if options.user:
item_to_copy['user'] = options.user
if options.user:
item_to_copy['group'] = options.group
if options.user:
item_to_copy['mode'] = options.mode
cataloginfo['items_to_copy'] = [item_to_copy]
cataloginfo['uninstallable'] = True
cataloginfo['uninstall_method'] = "remove_copied_items"
#eject the dmg
munkicommon.unmountdmg(mountpoints[0])
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:
plist = FoundationPlist.readPlist(infopath)
return plist
except FoundationPlist.NSPropertyListSerializationException:
pass
return None
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 munkicommon.isApplication(itempath):
infodict['type'] = 'application'
infodict['path'] = itempath
plist = getBundleInfo(itempath)
if 'CFBundleName' in plist:
infodict['CFBundleName'] = plist['CFBundleName']
if 'CFBundleIdentifier' in plist:
infodict['CFBundleIdentifier'] = plist['CFBundleIdentifier']
infodict['CFBundleShortVersionString'] = \
munkicommon.getVersionString(plist)
if 'LSMinimumSystemVersion' in plist:
infodict['minosversion'] = plist['LSMinimumSystemVersion']
elif 'SystemVersionCheck:MinimumSystemVersion' in plist:
infodict['minosversion'] = \
plist['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
plist = getBundleInfo(itempath)
infodict['CFBundleShortVersionString'] = \
munkicommon.getVersionString(plist)
elif itempath.endswith("Info.plist") or \
itempath.endswith("version.plist"):
infodict['type'] = 'plist'
infodict['path'] = itempath
try:
plist = FoundationPlist.readPlist(itempath)
infodict['CFBundleShortVersionString'] = \
munkicommon.getVersionString(plist)
except FoundationPlist.NSPropertyListSerializationException:
pass
if not 'CFBundleShortVersionString' in infodict and \
not 'CFBundleVersion' in infodict:
infodict['type'] = 'file'
infodict['path'] = itempath
if os.path.isfile(itempath):
infodict['md5checksum'] = munkicommon.getmd5hash(itempath)
return infodict
def check_mode(option, opt, value, parser):
'''Callback to check --mode options'''
modes = value.lower().replace(',', ' ').split()
value = None
rex = re.compile("[augo]+[=+-][rstwxXugo]+")
for mode in modes:
if rex.match(mode):
value = mode if not value else (value + "," + mode)
else:
raise OptionValueError("option %s: invalid mode: %s" %
(opt, mode))
setattr(parser.values, option.dest, value)
def main():
'''Main routine'''
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, typically an application. This generates an
"installs" item for the pkginfo, an item munki can
use to determine if this software has been installed.
Can be specified multiple times.''')
p.add_option('--pkgname', '-p',
help='''Optional flag.
-If the installer item is a disk image containing
multiple packages, or the package to be installed
is not at the root of the mounted disk image, PKGNAME
is a relative path from the root of the mounted
disk image to the specific package to be installed.
-If the installer item is a disk image containing
an Adobe CS4 Deployment Toolkit installation, PKGNAME
is the name of an Adobe CS4 Deployment Toolkit
installer package folder at the top level of the
mounted dmg.
If this flag is missing, the AdobeUber* files should
be at the top level of the mounted dmg.''')
p.add_option('--appdmg', action="store_true",
help='''Optional flag.
Causes makepkginfo to create a pkginfo item describing
an appdmg install instead of the newer copy_from_dmg
installer type. Meant for use with older munki
clients,as copy_from_dmg replaces appdmg in munki
0.6.0 and later.''')
p.add_option('--itemname', '-i', '--appname', '-a',
metavar='ITEM',
dest='item',
help='''Optional flag.
-If the installer item is a disk image with a
drag-and-drop item, ITEMNAME is the name or
relative path of the item to be installed.
Useful if there is more than one item at the
root of the dmg.''')
p.add_option('--destinationpath', '-d',
help='''Optional flag.
If the installer item is a disk image with a
drag-and-drop item, this is the path to which
the item should be copied. Defaults to
"/Applications".''')
p.add_option('--uninstallerdmg', '-u',
help='''Optional flag.
If the installer item is a disk image containing an
Adobe CS4 Deployment Toolkit installation package or
Adobe CS3 deployment package, UNINSTALLERDMG is a path
to a disk image containing an AdobeUberUninstaller for
this item.''')
p.add_option('--catalog', '-c', action="append",
help='''Optional flag.
Specifies in which catalog the item should appear. The
default is 'testing'. Can be specified multiple times
to add the item to multiple catalogs.''')
p.add_option('-o', '--owner',
metavar='USER',
dest='user',
help='''Optional flag.
If the installer item is a disk image used with
the copy_from_dmg installer type, this sets the
owner of the item specified by the --item flag.
The owner may be either a UID or a symbolic name.
The owner will be set recursively on the item.''')
p.add_option('-g', '--group',
metavar='GROUP',
dest='group',
help='''Optional flag.
If the installer item is a disk image used with
the copy_from_dmg installer type, this sets the
group of the item specified by the --item flag.
The group may be either a GID or a symbolic name.
The group will be set recursively on the item.''')
p.add_option('-m', '--mode',
metavar='MODE',
dest='mode',
action='callback',
type='string',
callback=check_mode,
help='''Optional flag.
If the installer item is a disk used with
the copy_from_dmg installer type, this sets the
mode of the item specified by the --item flag.
The specified mode must be in symbolic form.
See the manpage for chmod(1) for more information.
The mode is applied recursively.''')
options, arguments = p.parse_args()
if len(arguments) == 0 and \
not (options.file or options.installxml or options.uninstallxml
or options.serialnumber):
print >> sys.stderr, \
("Need to specify an installer item (.pkg, .mpkg, .dmg) "
"and/or --file and/or Adobe installer options!")
exit(-1)
if len(arguments) > 1:
print >> sys.stderr, \
("Can process only one installer item at a time. "
"Ignoring additional installer items.")
catinfo = {}
installs = []
if arguments:
item = arguments[0].rstrip("/")
if item and os.path.exists(item):
# get size of installer item
itemsize = 0
itemhash = "N/A"
if os.path.isfile(item):
itemsize = int(os.path.getsize(item))
itemhash = munkicommon.getsha256hash(item)
if item.endswith('.dmg'):
catinfo = getCatalogInfoFromDmg(item, options)
if (catinfo and
catinfo.get('installer_type') == "AdobeCS5Installer"):
print >> sys.stderr, (
"This disk image appears to contain an Adobe CS5 "
"product install.\n"
"Please use Adobe Application Manager, Enterprise "
"Edition (AAMEE) to create an installation package "
"for this product.")
exit(-1)
if not catinfo:
print >> sys.stderr, \
"Could not find a supported installer item in %s!" % \
item
exit(-1)
elif item.endswith('.pkg') or item.endswith('.mpkg'):
catinfo = munkicommon.getPackageMetaData(item)
if not catinfo:
print >> sys.stderr, \
"%s doesn't appear to be a valid installer item!" % \
item
exit(-1)
if os.path.isdir(item):
print >> sys.stderr, (
"WARNING: %s is a bundle-style package!\n"
"To use it with munki, you should encapsulate it "
"in a disk image.\n") % item
# need to walk the dir and add it all up
for (path, dirs, files) in os.walk(item):
for name in files:
filename = os.path.join(path, name)
# use os.lstat so we don't follow symlinks
itemsize += int(os.lstat(filename).st_size)
# convert to kbytes
itemsize = int(itemsize/1024)
else:
print >> sys.stderr, "%s is not an installer package!" % item
exit(-1)
catinfo['installer_item_size'] = int(itemsize/1024)
catinfo['installer_item_hash'] = itemhash
# try to generate the correct item location
temppath = item
location = ""
while len(temppath) > 4:
if temppath.endswith('/pkgs'):
location = item[len(temppath)+1:]
break
else:
temppath = os.path.dirname(temppath)
if not location:
#just the filename
location = os.path.split(item)[1]
catinfo['installer_item_location'] = location
# ADOBE STUFF - though maybe generalizable in the future?
if options.uninstallerdmg:
uninstallerpath = options.uninstallerdmg
if os.path.exists(uninstallerpath):
# try to generate the correct item location
temppath = uninstallerpath
location = ""
while len(temppath) > 4:
if temppath.endswith('/pkgs'):
location = uninstallerpath[len(temppath)+1:]
break
else:
temppath = os.path.dirname(temppath)
if not location:
#just the filename
location = os.path.split(uninstallerpath)[1]
catinfo['uninstaller_item_location'] = location
itemsize = int(os.path.getsize(uninstallerpath))
itemhash = munkicommon.getsha256hash(uninstallerpath)
catinfo['uninstaller_item_size'] = int(itemsize/1024)
catinfo['uninstaller_item_hash'] = itemhash
else:
print >> sys.stderr, "No uninstaller at %s" % \
uninstallerpath
# some metainfo
if options.catalog:
catinfo['catalogs'] = options.catalog
else:
catinfo['catalogs'] = ['testing']
if catinfo.get('receipts', None):
catinfo['uninstallable'] = True
catinfo['uninstall_method'] = "removepackages"
minosversion = ""
maxfileversion = "0.0.0.0.0"
if options.file:
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
if 'CFBundleShortVersionString' in iteminfodict:
thisitemversion = \
munkicommon.padVersionString(
iteminfodict['CFBundleShortVersionString'],5)
if version.LooseVersion(thisitemversion) > \
version.LooseVersion(maxfileversion):
maxfileversion = thisitemversion
installs.append(iteminfodict)
else:
print >> sys.stderr, (
"Item %s doesn't exist. Skipping." % fitem)
if catinfo:
catinfo['autoremove'] = False
if minosversion:
catinfo['minimum_os_version'] = minosversion
else:
catinfo['minimum_os_version'] = "10.4.0"
if not 'version' in catinfo:
if maxfileversion != "0.0.0.0.0":
catinfo['version'] = maxfileversion
else:
catinfo['version'] = "1.0.0.0.0 (Please edit me!)"
if installs:
catinfo['installs'] = installs
# and now, what we've all been waiting for...
print FoundationPlist.writePlistToString(catinfo)
if __name__ == '__main__':
main()