installer.py: new copyFromDMG method that copies an arbitrary list of items from a mounted DMG to specified locations.

New removeCopiedItems method that removes the same list of items from the startup disk.


updatecheck.py: support for new copy_from_dmg and remove_copied_items methods.

makepkginfo: support for making pkginfo with new copy_from_dmg method,

git-svn-id: http://munki.googlecode.com/svn/trunk@603 a4e17f2e-e282-11dd-95e1-755cbddbdd66
This commit is contained in:
Greg Neagle
2010-07-26 23:26:25 +00:00
parent 9a47af9605
commit 83ed22f3af
4 changed files with 412 additions and 166 deletions
+120 -39
View File
@@ -36,6 +36,7 @@ import sys
import os
import re
import optparse
from optparse import OptionValueError
from distutils import version
import subprocess
import hashlib
@@ -63,7 +64,7 @@ def DMGhasSLA(dmgpath):
return hasSLA
def getCatalogInfoFromDmg(dmgpath, pkgname='', appname=''):
def getCatalogInfoFromDmg(dmgpath, options):
"""
* Mounts a disk image
* Gets catalog info for the first installer item found at the root level.
@@ -84,13 +85,13 @@ def getCatalogInfoFromDmg(dmgpath, pkgname='', appname=''):
print >>sys.stderr, "Could not mount %s!" % dmgpath
exit(-1)
if pkgname:
if options.pkgname:
pkgpath = os.path.join(mountpoints[0], pkgname)
if os.path.exists(pkgpath):
cataloginfo = munkicommon.getPackageMetaData(pkgpath)
if cataloginfo:
cataloginfo['package_path'] = pkgname
elif not appname:
elif not options.item:
# search for first package at root
for fsitem in os.listdir(mountpoints[0]):
itempath = os.path.join(mountpoints[0], fsitem)
@@ -114,39 +115,61 @@ def getCatalogInfoFromDmg(dmgpath, pkgname='', appname=''):
else:
# maybe an Adobe installer/updater/patcher?
cataloginfo = adobeutils.getAdobeCatalogInfo(mountpoints[0],
pkgname)
options.pkgname or '')
if not cataloginfo:
# maybe this is an appdmg
# look for an app at the top level of the dmg
appinfo = {}
if appname:
itempath = os.path.join(mountpoints[0], appname)
if munkicommon.isApplication(itempath):
appinfo = getiteminfo(itempath)
# 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:
appname = ''
for item in os.listdir(mountpoints[0]):
itempath = os.path.join(mountpoints[0], item)
# 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):
appname = item
appinfo = getiteminfo(itempath)
if appinfo:
item = itemname
iteminfo = getiteminfo(itempath)
if iteminfo:
break
if appinfo:
appinfo['path'] = os.path.join("/Applications", appname)
if iteminfo:
if options.destinationpath:
iteminfo['path'] = os.path.join(options.destinationpath,
item)
else:
iteminfo['path'] = os.path.join("/Applications", item)
cataloginfo = {}
cataloginfo['name'] = appinfo.get('CFBundleName',
os.path.splitext(appname)[0])
cataloginfo['name'] = iteminfo.get('CFBundleName',
os.path.splitext(item)[0])
cataloginfo['version'] = \
munkicommon.padVersionString(
appinfo.get('CFBundleShortVersionString', "0")
iteminfo.get('CFBundleShortVersionString', "0")
,5)
cataloginfo['installs'] = [appinfo]
cataloginfo['installer_type'] = "appdmg"
cataloginfo['installs'] = [iteminfo]
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_app"
cataloginfo['uninstall_method'] = "remove_copied_items"
#eject the dmg
munkicommon.unmountdmg(mountpoints[0])
@@ -240,7 +263,20 @@ def getiteminfo(itempath):
if os.path.isfile(itempath):
infodict['md5checksum'] = 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():
@@ -267,14 +303,23 @@ def main():
If this flag is missing, the AdobeUber* files should
be at the top level of the mounted dmg.''')
p.add_option('--appname', '-a',
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 application, APPNAME is the name or
relative path of the application to be installed.
Useful if there is more than one application at the
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.
@@ -301,6 +346,46 @@ def main():
If the installer item is a disk image containing an
Adobe CS5 product, SERIALNUMBER is the product
serial number.''')
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 with a
drag-and-drop application, this sets the owner
of the application. This flag may be either a
UID or a symbolic name. The owner will be
set recursively on the application.''')
p.add_option('-g','--group',
metavar='GROUP',
dest='group',
help='''Optional flag.
If the installer item is a disk image with a
drag-and-drop application, this sets the group
of the application. This flag may be either a
GID or a symbolic name. The group will be
set recursively on the application.''')
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 image with a
drag-and-drop application, this sets the mode
of the application. 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()
@@ -335,14 +420,7 @@ def main():
itemsize += int(os.lstat(filename).st_size)
if item.endswith('.dmg'):
pkgname = ''
appname = ''
if options.pkgname:
pkgname = options.pkgname
if options.appname:
appname = options.appname
catinfo = getCatalogInfoFromDmg(item, pkgname, appname)
catinfo = getCatalogInfoFromDmg(item, options)
if not catinfo:
print >>sys.stderr, \
"Could not find a supported installer item in %s!" % \
@@ -402,7 +480,10 @@ def main():
uninstallerpath
# some metainfo
catinfo['catalogs'] = ['testing']
if options.catalog:
catinfo['catalogs'] = options.catalog
else:
catinfo['catalogs'] = ['testing']
if catinfo.get('receipts',None):
catinfo['uninstallable'] = True
catinfo['uninstall_method'] = "removepackages"
+1 -1
View File
@@ -945,7 +945,7 @@ def getAdobeInstallInfo(mountpoint=None,
return adobeInstallInfo
def getAdobeCatalogInfo(mountpoint, pkgname):
def getAdobeCatalogInfo(mountpoint, pkgname=""):
'''Used by makepkginfo to build pkginfo data for Adobe
installers/updaters'''
+284 -126
View File
@@ -23,7 +23,6 @@ munki module to automatically install pkgs, mpkgs, and dmgs
import os
import subprocess
import sys
#import tempfile
import adobeutils
import munkicommon
@@ -309,7 +308,152 @@ def copyAppFromDMG(dmgpath):
os.path.basename(dmgpath))
return -1
def copyFromDMG(dmgpath, itemlist):
# copies items from DMG
if not itemlist:
munkicommon.display_error("No items to copy!")
return -1
munkicommon.display_status("Mounting disk image %s" %
os.path.basename(dmgpath))
mountpoints = munkicommon.mountdmg(dmgpath)
if mountpoints:
mountpoint = mountpoints[0]
retcode = 0
for item in itemlist:
itemname = item.get("source_item")
if not itemname:
munkicommon.display_error("Missing name of item to copy!")
retcode = -1
if retcode == 0:
itempath = os.path.join(mountpoint, itemname)
if os.path.exists(itempath):
destpath = item.get("destination_path")
if os.path.exists(destpath):
# remove item if it already exists
olditem = os.path.join(destpath, itemname)
if os.path.exists(olditem):
retcode = subprocess.call(
["/bin/rm", "-rf", olditem])
if retcode:
munkicommon.display_error(
"Error removing existing %s" % olditem)
else:
munkicommon.display_error(
"Destination path %s does not exist!" % destpath)
retcode = -1
else:
munkicommon.display_error(
"Source item %s does not exist!" % itemname)
retcode = -1
if retcode == 0:
munkicommon.display_status(
"Copying %s to %s" % (itemname, destpath))
retcode = subprocess.call(["/bin/cp", "-R",
itempath, destpath])
if retcode:
munkicommon.display_error(
"Error copying %s to %s" %
(itempath, destpath))
destitem = os.path.join(destpath, itemname)
if (retcode == 0) and ('user' in item):
munkicommon.display_detail(
"Setting owner for '%s'" % destitem)
cmd = ['/usr/sbin/chown', '-R', item['user'], destitem]
retcode = subprocess.call(cmd)
if retcode:
munkicommon.display_error("Error setting owner for %s" %
(destitem))
if (retcode == 0) and ('group' in item):
munkicommon.display_detail(
"Setting group for '%s'" % destitem)
cmd = ['/usr/bin/chgrp', '-R', item['group'], destitem]
retcode = subprocess.call(cmd)
if retcode:
munkicommon.display_error("Error setting group for %s" %
(destitem))
if (retcode == 0) and ('mode' in item):
munkicommon.display_detail("Setting mode for '%s'" % destitem)
cmd = ['/bin/chmod', '-R', options['mode'], destitem]
retcode = subprocess.call(cmd)
if retcode:
munkicommon.display_error("Error setting mode for %s" %
(destitem))
if retcode == 0:
# remove com.apple.quarantine attribute from copied item
cmd = ["/usr/bin/xattr", destitem]
p = subprocess.Popen(cmd, shell=False, bufsize=1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(out, err) = p.communicate()
if out:
xattrs = out.splitlines()
if "com.apple.quarantine" in xattrs:
err = subprocess.call(["/usr/bin/xattr", "-d",
"com.apple.quarantine",
destitem])
if retcode:
# we encountered an error on this iteration;
# should not continue.
break
if retcode == 0:
# let the user know we completed successfully
munkicommon.display_status(
"The software was successfully installed.")
munkicommon.unmountdmg(mountpoint)
return retcode
else:
munkicommon.display_error("No mountable filesystems on %s" %
os.path.basename(dmgpath))
return -1
def removeCopiedItems(itemlist):
# removes filesystem items based on info in itemlist
# typically installed via DMG
retcode = 0
if not itemlist:
munkicommon.display_error("Nothing to remove!")
return -1
for item in itemlist:
itemname = item.get("source_item")
if not itemname:
munkicommon.display_error("Missing item name to remove.")
retcode = -1
break
destpath = item.get("destination_path")
if not destpath:
munkicommon.display_error("Missing path for item to remove.")
retcode = -1
break
path_to_remove = os.path.join(destpath, itemname)
if os.path.exists(path_to_remove):
munkicommon.display_status("Removing %s" % path_to_remove)
retcode = subprocess.call(["/bin/rm", "-rf", path_to_remove])
if retcode:
munkicommon.display_error("Removal error for %s" %
path_to_remove)
break
else:
# path_to_remove doesn't exist
# note it, but not an error
munkicommon.display_detail("Path %s doesn't exist." %
path_to_remove)
return retcode
def installWithInfo(dirpath, installlist):
"""
Uses the installlist to install items in the
@@ -345,8 +489,17 @@ def installWithInfo(dirpath, installlist):
installer_type = item.get("installer_type","")
if installer_type.startswith("Adobe"):
retcode = adobeutils.doAdobeInstall(item)
elif installer_type == "copy_from_dmg":
retcode = copyFromDMG(itempath, item.get('items_to_copy'))
elif installer_type == "appdmg":
retcode = copyAppFromDMG(itempath)
retcode = copyAppFromDMG(itempath,
item.get('installer_options',{}))
elif installer_type != "":
# we've encountered an installer type
# we don't know how to handle
munkicommon.log("Unsupported install type: %s" %
installer_type)
retcode = -99
else:
# must be Apple installer package
suppressBundleRelocation = item.get(
@@ -472,134 +625,139 @@ def processRemovals(removallist):
for item in removallist:
if munkicommon.stopRequested():
return restartFlag
if 'installed' in item:
if item['installed']:
index += 1
name = item.get('display_name') or item.get('name') or \
item.get('manifestitem')
if munkicommon.munkistatusoutput:
munkistatus.message("Removing %s (%s of %s)..." %
(name, index, len(removallist)))
munkistatus.detail("")
munkistatus.percent(-1)
else:
munkicommon.display_status("Removing %s (%s of %s)..." %
(name, index, len(removallist)))
if 'uninstall_method' in item:
uninstallmethod = item['uninstall_method'].split(' ')
if uninstallmethod[0] == "removepackages":
if 'packages' in item:
if item.get('RestartAction') == "RequireRestart":
restartFlag = True
retcode = removepackages(item['packages'],
forcedeletebundles=True)
if retcode:
if retcode == -128:
message = ("Uninstall of %s was "
"cancelled." % name)
else:
message = "Uninstall of %s failed." % name
munkicommon.display_error(message)
else:
munkicommon.log("Uninstall of %s was "
"successful." % name)
elif uninstallmethod[0].startswith("Adobe"):
retcode = adobeutils.doAdobeRemoval(item)
elif uninstallmethod[0] == "remove_app":
remove_app_info = item.get('remove_app_info',None)
if remove_app_info:
path_to_remove = remove_app_info['path']
munkicommon.display_status("Removing %s" %
path_to_remove)
retcode = subprocess.call(["/bin/rm", "-rf",
path_to_remove])
if retcode:
munkicommon.display_error("Removal error "
"for %s" %
path_to_remove)
if not item.get('installed'):
# not installed, so skip it
continue
index += 1
name = item.get('display_name') or item.get('name') or \
item.get('manifestitem')
if munkicommon.munkistatusoutput:
munkistatus.message("Removing %s (%s of %s)..." %
(name, index, len(removallist)))
munkistatus.detail("")
munkistatus.percent(-1)
else:
munkicommon.display_status("Removing %s (%s of %s)..." %
(name, index, len(removallist)))
if 'uninstall_method' in item:
uninstallmethod = item['uninstall_method'].split(' ')
if uninstallmethod[0] == "removepackages":
if 'packages' in item:
if item.get('RestartAction') == "RequireRestart":
restartFlag = True
retcode = removepackages(item['packages'],
forcedeletebundles=True)
if retcode:
if retcode == -128:
message = ("Uninstall of %s was "
"cancelled." % name)
else:
munkicommon.display_error("Application removal "
"info missing from %s" %
name)
elif os.path.exists(uninstallmethod[0]) and \
os.access(uninstallmethod[0], os.X_OK):
# it's a script or program to uninstall
if munkicommon.munkistatusoutput:
munkistatus.message("Running uninstall script "
"for %s..." % name)
munkistatus.detail("")
# set indeterminate progress bar
munkistatus.percent(-1)
if item.get('RestartAction') == "RequireRestart":
restartFlag = True
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().decode('UTF-8')
# 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 munkicommon.munkistatusoutput:
# do nothing with the output
pass
else:
print msg
retcode = p.poll()
if retcode:
message = "Uninstall of %s failed." % name
print >>sys.stderr, message
munkicommon.log(message)
message = \
"-------------------------------------------------"
print >>sys.stderr, message
munkicommon.log(message)
for line in uninstalleroutput:
print >>sys.stderr, " ", line.rstrip("\n")
munkicommon.log(line.rstrip("\n"))
message = \
"-------------------------------------------------"
print >>sys.stderr, message
munkicommon.log(message)
else:
munkicommon.log("Uninstall of %s was "
"successful." % name)
if munkicommon.munkistatusoutput:
# clear indeterminate progress bar
munkistatus.percent(0)
munkicommon.display_error(message)
else:
munkicommon.log("Uninstall of %s failed because "
"there was no valid uninstall "
"method." % name)
retcode = -99
munkicommon.log("Uninstall of %s was "
"successful." % name)
elif uninstallmethod[0].startswith("Adobe"):
retcode = adobeutils.doAdobeRemoval(item)
elif uninstallmethod[0] == "remove_copied_items":
retcode = removeCopiedItems(item.get('items_to_remove'))
elif uninstallmethod[0] == "remove_app":
remove_app_info = item.get('remove_app_info',None)
if remove_app_info:
path_to_remove = remove_app_info['path']
munkicommon.display_status("Removing %s" %
path_to_remove)
retcode = subprocess.call(["/bin/rm", "-rf",
path_to_remove])
if retcode:
munkicommon.display_error("Removal error "
"for %s" %
path_to_remove)
else:
munkicommon.display_error("Application removal "
"info missing from %s" %
name)
elif os.path.exists(uninstallmethod[0]) and \
os.access(uninstallmethod[0], os.X_OK):
# it's a script or program to uninstall
if munkicommon.munkistatusoutput:
munkistatus.message("Running uninstall script "
"for %s..." % name)
munkistatus.detail("")
# set indeterminate progress bar
munkistatus.percent(-1)
if item.get('RestartAction') == "RequireRestart":
restartFlag = True
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().decode('UTF-8')
# 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 munkicommon.munkistatusoutput:
# do nothing with the output
pass
else:
print msg
retcode = p.poll()
if retcode:
message = "Uninstall of %s failed." % name
print >>sys.stderr, message
munkicommon.log(message)
message = \
"-------------------------------------------------"
print >>sys.stderr, message
munkicommon.log(message)
for line in uninstalleroutput:
print >>sys.stderr, " ", line.rstrip("\n")
munkicommon.log(line.rstrip("\n"))
message = \
"-------------------------------------------------"
print >>sys.stderr, message
munkicommon.log(message)
else:
munkicommon.log("Uninstall of %s was "
"successful." % name)
# record removal success/failure
if retcode == 0:
success_msg = "Removal of %s: SUCCESSFUL" % name
munkicommon.log(success_msg, "Install.log")
munkicommon.report[
'RemovalResults'].append(success_msg)
else:
failure_msg = "Removal of %s: " % name + \
" FAILED with return code: %s" % retcode
munkicommon.log(failure_msg, "Install.log")
munkicommon.report[
'RemovalResults'].append(failure_msg)
if munkicommon.munkistatusoutput:
# clear indeterminate progress bar
munkistatus.percent(0)
else:
munkicommon.log("Uninstall of %s failed because "
"there was no valid uninstall "
"method." % name)
retcode = -99
# record removal success/failure
if retcode == 0:
success_msg = "Removal of %s: SUCCESSFUL" % name
munkicommon.log(success_msg, "Install.log")
munkicommon.report[
'RemovalResults'].append(success_msg)
else:
failure_msg = "Removal of %s: " % name + \
" FAILED with return code: %s" % retcode
munkicommon.log(failure_msg, "Install.log")
munkicommon.report[
'RemovalResults'].append(failure_msg)
return restartFlag
+7
View File
@@ -1327,6 +1327,9 @@ def processInstall(manifestitem, cataloglist, installinfo):
iteminfo['adobe_package_name'] = pl['adobe_package_name']
if 'package_path' in pl:
iteminfo['package_path'] = pl['package_path']
if 'items_to_copy' in pl:
# used with copy_from_dmg installer type
iteminfo['items_to_copy'] = pl['items_to_copy']
installinfo['managed_installs'].append(iteminfo)
if nameAndVersion(manifestitemname)[1] == '':
# didn't specify a specific version, so
@@ -1566,6 +1569,8 @@ def processRemoval(manifestitem, cataloglist, installinfo):
elif uninstallmethod.startswith('Adobe'):
# Adobe CS3/CS4/CS5 product
uninstall_item = item
elif uninstallmethod == 'remove_copied_items':
uninstall_item = item
elif uninstallmethod == 'remove_app':
uninstall_item = item
else:
@@ -1714,6 +1719,8 @@ def processRemoval(manifestitem, cataloglist, installinfo):
"uninstaller for %s"
% iteminfo["name"])
return False
elif uninstallmethod == "remove_copied_items":
iteminfo['items_to_remove'] = item.get('items_to_copy',[])
elif uninstallmethod == "remove_app":
if uninstall_item.get('installs',None):
iteminfo['remove_app_info'] = uninstall_item['installs'][0]