This commit is contained in:
Justin McWilliams
2012-06-06 14:12:36 -04:00
9 changed files with 616 additions and 246 deletions
@@ -21,9 +21,9 @@
</object>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="29"/>
<integer value="639"/>
<integer value="511"/>
<integer value="29"/>
<integer value="516"/>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -617,7 +617,7 @@
<object class="NSTabViewItem" id="705868753">
<string key="NSIdentifier">1</string>
<object class="NSView" key="NSView" id="78996069">
<reference key="NSNextResponder" ref="901524889"/>
<nil key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<object class="NSMutableArray" key="NSSubviews">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -1071,13 +1071,29 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<string>WebKitDefaultFixedFontSize</string>
<string>WebKitDefaultFontSize</string>
<string>WebKitFixedFont</string>
<string>WebKitJavaEnabled</string>
<string>WebKitJavaScriptCanOpenWindowsAutomatically</string>
<string>WebKitJavaScriptEnabled</string>
<string>WebKitMinimumFontSize</string>
<string>WebKitPluginsEnabled</string>
<string>WebKitSansSerifFont</string>
<string>WebKitSerifFont</string>
<string>WebKitStandardFont</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="12"/>
<integer value="12"/>
<string>Lucida Sans</string>
<boolean value="NO"/>
<boolean value="NO"/>
<boolean value="NO"/>
<integer value="1"/>
<boolean value="NO"/>
<string>Lucida Sans</string>
<string>Lucida Sans</string>
<string>Lucida Sans</string>
</object>
</object>
</object>
@@ -1152,7 +1168,6 @@
</object>
</object>
<string key="NSFrameSize">{512, 512}</string>
<reference key="NSSuperview" ref="901524889"/>
<int key="NSViewLayerContentsRedrawPolicy">2</int>
</object>
<string key="NSLabel">Updates</string>
@@ -1162,7 +1177,7 @@
<object class="NSTabViewItem" id="311465176">
<string key="NSIdentifier">2</string>
<object class="NSView" key="NSView" id="574199769">
<nil key="NSNextResponder"/>
<reference key="NSNextResponder" ref="901524889"/>
<int key="NSvFlags">256</int>
<object class="NSMutableArray" key="NSSubviews">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -1416,6 +1431,7 @@
<reference key="NSHScroller" ref="563381393"/>
<reference key="NSContentView" ref="720875694"/>
<reference key="NSHeaderClipView" ref="307161175"/>
<reference key="NSCornerView" ref="97640332"/>
<bytes key="NSScrollAmts">QSAAAEEgAABBmAAAQZgAAA</bytes>
</object>
<object class="WebView" id="800312662">
@@ -1627,20 +1643,21 @@
</object>
</object>
<string key="NSFrameSize">{512, 512}</string>
<reference key="NSSuperview" ref="901524889"/>
</object>
<string key="NSLabel">Optional software</string>
<reference key="NSColor" ref="204534777"/>
<reference key="NSTabView" ref="901524889"/>
</object>
</object>
<reference key="NSSelectedTabViewItem" ref="705868753"/>
<reference key="NSSelectedTabViewItem" ref="311465176"/>
<reference key="NSFont" ref="901400326"/>
<int key="NSTvFlags">6</int>
<bool key="NSAllowTruncatedLabels">YES</bool>
<bool key="NSDrawsBackground">YES</bool>
<object class="NSMutableArray" key="NSSubviews">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="78996069"/>
<reference ref="574199769"/>
</object>
</object>
</object>
+13 -4
View File
@@ -110,7 +110,7 @@ def main():
ID = 'com.googlecode.munki.logouthelper'
munkicommon.log('%s invoked' % ID)
sent_notifications = []
logout_time_override = False
logout_time_override = None
minimum_notifications_logout_time = NSDate.date().addTimeInterval_(
60 * MINIMUM_NOTIFICATION_MINS + 30)
while True:
@@ -121,8 +121,17 @@ def main():
munkicommon.log('%s exited' % ID)
exit(0)
if not logout_time_override:
logout_time = earliestForceInstallDate()
tmp_logout_time = earliestForceInstallDate()
if logout_time_override is None:
logout_time = tmp_logout_time
else:
# allow the new (tmp_)logout_time from InstallInfo to be used
# if it has changed since when we decided to override it.
if logout_time_override != tmp_logout_time:
logout_time = tmp_logout_time
logout_time_override = None
sent_notifications = []
if not logout_time:
# no forced logout needed, so bail
munkicommon.log('%s: no forced installs found' % ID)
@@ -137,7 +146,7 @@ def main():
MINIMUM_NOTIFICATION_MINS))
logout_time = minimum_notifications_logout_time
munkicommon.log('Reset logout_time to: %s' % logout_time)
logout_time_override = True
logout_time_override = logout_time
minutes_until_logout = int(logout_time.timeIntervalSinceNow() / 60)
info = {'logout_time': logout_time}
+3
View File
@@ -107,6 +107,9 @@ def makecatalogs(repopath):
try:
pkginfo = plistlib.readPlist(filepath)
# don't copy admin notes to catalogs.
if pkginfo.get('notes'):
del(pkginfo['notes'])
except IOError, inst:
errors.append("IO error for %s: %s" % (filepath, inst))
continue
+402 -171
View File
@@ -37,7 +37,6 @@ import os
import re
import optparse
from optparse import OptionValueError
import subprocess
from munkilib import munkicommon
from munkilib import FoundationPlist
@@ -62,6 +61,10 @@ def getCatalogInfoFromDmg(dmgpath, options):
pkgpath = os.path.join(mountpoints[0], options.pkgname)
if os.path.exists(pkgpath):
cataloginfo = munkicommon.getPackageMetaData(pkgpath)
if options.installer_choices_xml:
installer_choices_xml = munkicommon.getChoiceChangesXML(pkgpath)
if installer_choices_xml:
cataloginfo['installer_choices_xml'] = installer_choices_xml
if cataloginfo:
cataloginfo['package_path'] = options.pkgname
elif not options.item:
@@ -70,6 +73,12 @@ def getCatalogInfoFromDmg(dmgpath, options):
itempath = os.path.join(mountpoints[0], fsitem)
if itempath.endswith('.pkg') or itempath.endswith('.mpkg'):
cataloginfo = munkicommon.getPackageMetaData(itempath)
if options.installer_choices_xml:
installer_choices_xml = munkicommon.getChoiceChangesXML(
itempath)
if installer_choices_xml:
cataloginfo['installer_choices_xml'] = \
installer_choices_xml
# get out of fsitem loop
break
@@ -129,26 +138,21 @@ def getCatalogInfoFromDmg(dmgpath, options):
cataloginfo['version'] = \
iteminfo.get('CFBundleShortVersionString', "0")
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.group:
item_to_copy['group'] = options.group
if options.mode:
item_to_copy['mode'] = options.mode
cataloginfo['items_to_copy'] = [item_to_copy]
cataloginfo['uninstallable'] = True
cataloginfo['uninstall_method'] = "remove_copied_items"
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.group:
item_to_copy['group'] = options.group
if options.mode:
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
@@ -185,6 +189,20 @@ def readfile(path):
return ""
def readFileOrString(option_value):
"""
If option_value is a path to a file,
return contents of file.
Otherwise, return the string.
"""
if os.path.exists(os.path.expanduser(option_value)):
string = readfile(option_value)
else:
string = option_value
return string
def getiteminfo(itempath):
"""
Gets info for filesystem items passed to makecatalog item, to be used for
@@ -256,149 +274,289 @@ def main():
usage = """usage: %prog [options] [/path/to/installeritem]
%prog --help for more information."""
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('--displayname',
help='''Optional flag.
String display name of the package.
Note: overrides any display_name in the package itself''')
p.add_option('--description',
help='''Optional flag.
String description of the package.
Note: overrides any description in the package itself''')
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('--postinstall_script',
metavar='SCRIPT_PATH',
help='''Optional flag.
Path to an optional postinstall script to be run after
installation of the item. The script will be read and
embedded into the pkginfo.''')
p.add_option('--preinstall_script',
metavar='SCRIPT_PATH',
help='''Optional flag.
Path to an optional preinstall script to be run before
installation of the item. The script will be read and
embedded into the pkginfo.''')
p.add_option('--postuninstall_script',
metavar='SCRIPT_PATH',
help='''Optional flag.
p.add_option(
'--verify-options-only',
action="store_true",
help=optparse.SUPPRESS_HELP
)
p.add_option(
'--version', '-V',
action='store_true',
help='Print the version of the munki tools and exit.'
)
Path to an optional postuninstall script to be run after
removal of the item. The script will be read and
embedded into the pkginfo.''')
p.add_option('--preuninstall_script',
metavar='SCRIPT_PATH',
help='''Optional flag.
Path to an optional preuninstall script to be run before
removal of the item. The script will be read and
embedded into the pkginfo.''')
p.add_option('--uninstall_script',
metavar='SCRIPT_PATH',
help='''Optional flag.
Path to an uninstall script to be run in order to
uninstall this item. The script will be read and
embedded into the pkginfo.''')
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.''')
p.add_option('--version', '-V', action='store_true',
help='Print the version of the munki tools and exit.')
# Default override options
default_override_options = optparse.OptionGroup(
p, 'Default Override Options',
('Options specified will override information automatically derived '
'from the package.'))
default_override_options.add_option(
'--name',
metavar='NAME',
help='Name of the package.'
)
default_override_options.add_option(
'--displayname',
metavar='DISPLAY_NAME',
help='Display name of the package.'
)
default_override_options.add_option(
'--description',
metavar='STRING|PATH',
help=('Description of the package. '
'Can be a PATH to a file (plain text or html).')
)
default_override_options.add_option(
'--pkgvers',
metavar='PACKAGE_VERSION',
help='Version of the package.'
)
default_override_options.add_option(
'--RestartAction',
metavar='ACTION',
help=('Specify a \'RestartAction\' for the package. '
'Supported actions: RequireRestart, RequireLogout, or '
'RecommendRestart')
)
default_override_options.add_option(
'--uninstall_method', '--uninstall-method',
metavar='METHOD|PATH',
help=('Specify an \'uninstall_method\' for the package. '
'Default method depends on the package type: i.e. '
'drag-n-drop, Apple package, or an embedded uninstall script. '
'Can be a path to a script on the client computer.')
)
p.add_option_group(default_override_options)
# Script options
script_options = optparse.OptionGroup(
p, 'Script Options',
'All scripts are read and embedded into the pkginfo.')
script_options.add_option(
'--preinstall_script', '--preinstall-script',
metavar='SCRIPT_PATH',
help=('Path to an optional preinstall script to be '
'run before installation of the item.')
)
script_options.add_option(
'--postinstall_script', '--postinstall-script',
metavar='SCRIPT_PATH',
help=('Path to an optional postinstall script to be '
'run after installation of the item.')
)
script_options.add_option(
'--preuninstall_script', '--preuninstall-script',
metavar='SCRIPT_PATH',
help=('Path to an optional preuninstall script to be run '
'before removal of the item.')
)
script_options.add_option(
'--postuninstall_script', '--postuninstall-script',
metavar='SCRIPT_PATH',
help=('Path to an optional postuninstall script to be run '
'after removal of the item.')
)
script_options.add_option(
'--uninstall_script', '--uninstall-script',
metavar='SCRIPT_PATH',
help=('Path to an uninstall script to be run in order '
'to uninstall this item.')
)
p.add_option_group(script_options)
# Drag-n-Drop options
dragdrop_options = optparse.OptionGroup(
p, 'Drag-n-Drop Options',
('These options apply to installer items that are "drag-n-drop" '
'disk images.')
)
dragdrop_options.add_option(
'--itemname', '-i', '--appname', '-a',
metavar='ITEM',
dest='item',
help=('Name or relative path of the item to be installed. '
'Useful if there is more than one item at the root of the dmg.')
)
dragdrop_options.add_option(
'--destinationpath', '-d',
metavar='PATH',
help=('Path to which the item should be copied. Defaults to '
'"/Applications".')
)
dragdrop_options.add_option(
'-o', '--owner',
metavar='USER',
dest='user',
help=('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.')
)
dragdrop_options.add_option(
'-g', '--group',
metavar='GROUP',
dest='group',
help=('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.')
)
dragdrop_options.add_option(
'-m', '--mode',
metavar='MODE',
dest='mode',
action='callback',
type='string',
callback=check_mode,
help=('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.')
)
p.add_option_group(dragdrop_options)
# Apple package specific options
apple_options = optparse.OptionGroup(p, 'Apple Package Options')
apple_options.add_option(
'--pkgname', '-p',
help=('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.')
)
apple_options.add_option(
'--installer_choices_xml', '--installer-choices-xml',
action='store_true',
help=('Generate installer choices for metapackages. '
'Note: Requires Mac OS X 10.6.6 or later.')
)
apple_options.add_option(
'--installer_environment', '--installer-environment', '-E',
action="append",
metavar='KEY=VALUE',
help=('Specifies key/value pairs to set environment variables for use '
'by /usr/sbin/installer. A key/value pair of '
'USER=CURRENT_CONSOLE_USER indicates that USER be set to the '
'GUI user, otherwise root. Can be specified multiple times.')
)
p.add_option_group(apple_options)
# Adobe package specific options
adobe_options = optparse.OptionGroup(p, 'Adobe-specific Options')
adobe_options.add_option(
'--uninstallerdmg', '-U',
help=('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_group(adobe_options)
# Forced/Unattended (install) options
forced_unattended_options = optparse.OptionGroup(
p, 'Forced/Unattended Options')
forced_unattended_options.add_option(
'--unattended_install', '--unattended-install',
action='store_true',
help='Item can be installed without notifying the user.')
forced_unattended_options.add_option(
'--unattended_uninstall', '--unattended-uninstall',
action='store_true',
help='Item can be uninstalled without notifiying the user.')
forced_unattended_options.add_option(
'--force_install_after_date', '--force-install-after-date',
metavar='DATE',
help=('Specify a date, in local time, after which the package will '
'be forcefully installed. DATE format: yyyy-mm-ddThh:mm:ssZ '
'Example: \'2011-08-11T12:55:00Z\' equates to 11 August 2011 '
'at 12:55 PM local time.')
)
p.add_option_group(forced_unattended_options)
# 'installs' generation options
# (by itself since no installer_item needs to be specified)
gen_installs_options = optparse.OptionGroup(
p, 'Generating \'installs\' items')
gen_installs_options.add_option(
'--file', '-f',
action="append",
metavar='PATH',
help=('Path to a filesystem item installed by this package, typically '
'an application. This generates an "installs" item for the '
'pkginfo, to be used to determine if this software has been '
'installed. Can be specified multiple times.')
)
p.add_option_group(gen_installs_options)
# Additional options - misc. options that don't fit into other categories,
# and don't necessarily warrant the creation of their own option group
additional_options = optparse.OptionGroup(p, 'Additional Options')
additional_options.add_option(
'--autoremove',
action='store_true',
help=('Indicates this package should be automatically removed if it is '
'not listed in any applicable \'managed_installs\'.')
)
additional_options.add_option(
'--minimum_munki_version', '--minimum-munki-version',
metavar='VERSION',
help=('Minimum version of munki required to perform installation. '
'Uses format produced by \'--version\' query from any munki '
'utility.')
)
additional_options.add_option(
'--minimum_os_version', '--minimum-os-version', '--min-os-ver',
metavar='VERSION',
help='Minimum OS version for the installer item.'
)
additional_options.add_option(
'--maximum_os_version', '--maximum-os-version', '--max-os-ver',
metavar='VERSION',
help='Maximum OS version for the installer item.'
)
additional_options.add_option(
'--update_for', '--update-for', '-u',
action="append",
metavar='PKG_NAME',
help=('Specifies a package for which the current package is an update. '
'Can be specified multiple times to build an array of packages.')
)
additional_options.add_option(
'--requires', '-r',
action="append",
metavar='PKG_NAME',
help=('Specifies a package required by the current package. Can be '
'specified multiple times to build an array of required '
'packages.')
)
additional_options.add_option(
'--blocking_application', '--blocking-application', '-b',
action="append",
metavar='APP_NAME',
help=('Specifies an application that blocks installation. Can be '
'specified multiple times to build an array of blocking '
'applications.')
)
additional_options.add_option(
'--catalog', '-c',
action="append",
metavar='CATALOG_NAME',
help=('Specifies in which catalog the item should appear. The default '
'is \'testing\'. Can be specified multiple times to add the item '
'to multiple catalogs.')
)
additional_options.add_option(
'--notes',
metavar='STRING|PATH',
help=('Specifies administrator provided notes to be embedded into the '
'pkginfo. Can be a PATH to a file.')
)
p.add_option_group(additional_options)
options, arguments = p.parse_args()
@@ -406,8 +564,22 @@ def main():
print munkicommon.get_version()
exit(0)
if options.verify_options_only:
if len(arguments) == 0:
print >> sys.stderr, 'makepkginfo options FAILED verification'
print >> sys.stderr, 'No installer item was provided.'
exit(1)
if len(arguments) > 1:
print >> sys.stderr, 'makepkginfo options FAILED verification'
print >> sys.stderr, \
'Can process only one installer item at a time.'
exit(3)
print >> sys.stdout, arguments[0]
exit(0)
if (len(arguments) == 0
and not options.file
and not options.file
and not options.installer_environment
and not options.preinstall_script
and not options.postinstall_script
and not options.preuninstall_script
@@ -421,6 +593,12 @@ def main():
print >> sys.stderr, 'Ignoring additional installer items:'
print >> sys.stderr, '\t', '\n\t'.join(arguments[1:])
if options.installer_choices_xml:
os_version = munkicommon.getOsVersion(
only_major_minor=False, as_tuple=True)
if os_version < (10, 6, 6):
options.installer_choices_xml = False
catinfo = {}
installs = []
if arguments:
@@ -452,10 +630,15 @@ def main():
elif item.endswith('.pkg') or item.endswith('.mpkg'):
catinfo = munkicommon.getPackageMetaData(item)
if options.installer_choices_xml:
installer_choices_xml = munkicommon.getChoiceChangesXML(
item)
if installer_choices_xml:
catinfo['installer_choices_xml'] = installer_choices_xml
if not catinfo:
print >> sys.stderr, \
"%s doesn't appear to be a valid installer item!" % \
item
print >> sys.stderr, (
"%s doesn't appear to be a valid installer item!" %
item)
exit(-1)
if os.path.isdir(item):
print >> sys.stderr, (
@@ -470,19 +653,23 @@ def main():
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)
if options.description:
catinfo['description'] = options.description
catinfo['description'] = readFileOrString(options.description)
if options.displayname:
catinfo['display_name'] = options.displayname
if options.name:
catinfo['name'] = options.name
if options.pkgvers:
catinfo['version'] = options.pkgvers
catinfo['installer_item_size'] = int(itemsize/1024)
catinfo['installer_item_hash'] = itemhash
# try to generate the correct item location
temppath = item
location = ""
@@ -600,6 +787,50 @@ def main():
if scriptstring:
catinfo['uninstall_script'] = scriptstring
catinfo['uninstall_method'] = 'uninstall_script'
if options.autoremove:
catinfo['autoremove'] = True
if options.minimum_munki_version:
catinfo['minimum_munki_version'] = options.minimum_munki_version
if options.unattended_install:
catinfo['unattended_install'] = True
if options.unattended_uninstall:
catinfo['unattended_uninstall'] = True
if options.minimum_os_version:
catinfo['minimum_os_version'] = options.minimum_os_version
if options.maximum_os_version:
catinfo['maximum_os_version'] = options.maximum_os_version
if options.force_install_after_date:
force_install_after_date = (
munkicommon.validateDateFormat(options.force_install_after_date))
if force_install_after_date:
catinfo['force_install_after_date'] = force_install_after_date
if options.RestartAction:
validActions = ['RequireRestart', 'RequireLogout', 'RecommendRestart']
if options.RestartAction in validActions:
catinfo['RestartAction'] = options.RestartAction
elif 'restart' in options.RestartAction.lower():
catinfo['RestartAction'] = 'RequireRestart'
elif 'logout' in options.RestartAction.lower():
catinfo['RestartAction'] = 'RequireLogout'
if options.update_for:
catinfo['update_for'] = options.update_for
if options.requires:
catinfo['requires'] = options.requires
if options.blocking_application:
catinfo['blocking_application'] = options.blocking_application
if options.uninstall_method:
catinfo['uninstall_method'] = options.uninstall_method
if options.installer_environment:
try:
installer_environment_dict = dict(
(k,v) for k,v in (
kv.split('=') for kv in options.installer_environment))
except Exception:
installer_environment_dict = {}
if installer_environment_dict:
catinfo['installer_environment'] = installer_environment_dict
if options.notes:
catinfo['notes'] = readFileOrString(options.notes)
# and now, what we've all been waiting for...
print FoundationPlist.writePlistToString(catinfo)
+91 -49
View File
@@ -25,14 +25,34 @@ Assists with importing installer items into the munki repo
import sys
import os
import optparse
import subprocess
import time
#from distutils import version
from optparse import OptionParser, BadOptionError, AmbiguousOptionError
from munkilib import munkicommon
from munkilib import FoundationPlist
class PassThroughOptionParser(OptionParser):
"""
An unknown option pass-through implementation of OptionParser.
When unknown arguments are encountered, bundle with largs and try again,
until rargs is depleted.
sys.exit(status) will still be called if a known argument is passed
incorrectly (e.g. missing arguments or bad argument types, etc.)
"""
def _process_args(self, largs, rargs, values):
while rargs:
try:
OptionParser._process_args(self, largs, rargs, values)
except (BadOptionError, AmbiguousOptionError), e:
largs.append(e.opt_str)
def format_epilog(self, formatter):
if not self.epilog:
self.epilog = ""
return self.epilog
def makeDMG(pkgpath):
"""Wraps a non-flat package into a disk image.
@@ -59,7 +79,7 @@ def makeDMG(pkgpath):
else:
print 'Disk image created at: %s' % diskimagepath
return diskimagepath
def repoAvailable():
"""Checks the repo path for proper directory structure.
@@ -401,7 +421,7 @@ def findMatchingPkginfo(pkginfo):
return {}
def makePkgInfo(item_path):
def makePkgInfo(options=None, test_mode=False):
"""Calls makepkginfo to generate the pkginfo for item_path."""
# first look for a makepkginfo in the same dir as us
mydir = os.path.dirname(os.path.abspath(__file__))
@@ -409,14 +429,36 @@ def makePkgInfo(item_path):
if not os.path.exists(makepkginfo_path):
# didn't find it; assume the default install path
makepkginfo_path = '/usr/local/munki/makepkginfo'
proc = subprocess.Popen([makepkginfo_path, item_path],
if test_mode:
# prepend verification option if in test mode
options = ['--verify-options-only'] + options
# build makepkginfo command from discovered path and options
cmd = [makepkginfo_path] + options
proc = subprocess.Popen(cmd,
bufsize=-1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(pliststr, err) = proc.communicate()
(stdout, stderr) = proc.communicate()
if test_mode:
if proc.returncode == 2:
# option syntax error or unknown option
syntax_error = [error for error in stderr.splitlines()
if 'error' in error]
print >> sys.stderr, ('Option syntax error: %s' %
syntax_error[-1].split(': ',2)[-1])
print >> sys.stderr, ('See \'%s --help\' for valid options that '
'can be used with munkiimport.') % makepkginfo_path
exit(-1)
elif proc.returncode:
# catch-all for any other error
if stderr:
print >> sys.stderr, stderr.rstrip('\n')
return {}
else:
return stdout.rstrip('\n')
if proc.returncode:
print >> sys.stderr, err
print >> sys.stderr, stderr.rstrip('\n')
return {}
return FoundationPlist.readPlistFromString(pliststr)
return FoundationPlist.readPlistFromString(stdout)
def makeCatalogs():
@@ -449,6 +491,7 @@ def makeCatalogs():
def cleanupAndExit(exitcode):
"""Unmounts the repo if we mounted it, then exits"""
result = 0
if WE_MOUNTED_THE_REPO:
if not NOINTERACTIVE:
@@ -501,14 +544,27 @@ VERBOSE = False
def main():
"""Main routine"""
global NOINTERACTIVE
global VERBOSE
usage = """usage: %prog [options] [/path/to/installer_item]
usage = """usage: %prog [options] /path/to/installer_item
Imports an installer item into a munki repo.
Installer item can be a pkg, mpkg, dmg, or app.
Bundle-style pkgs and apps are wrapped in a dmg
file before upload."""
Bundle-style pkgs and apps are wrapped in a dmg file before upload.
Example:
munkiimport --subdirectory apps /path/to/installer_item
"""
epilog = """\nExtended Options: (makepkginfo options)
In addition to the options described above, options used with
'makepkginfo' may also be specified to customize the resulting
pkginfo file.
Example:
munkiimport --subdirectory apps -c production --minimum_os_vers 10.6.8 /path/to/installer_item\n"""
p = PassThroughOptionParser(usage=usage, epilog=epilog)
p = optparse.OptionParser(usage=usage)
p.add_option('--configure', action='store_true',
help="""Configure munkiimport with details about your
munki repo, preferred editor, and the like. Any other
@@ -521,21 +577,13 @@ def main():
p.add_option('--nointeractive', '-n', action='store_true',
help="""No interactive prompts. May cause a failure
if repo path is unavailable.""")
p.add_option('--unattended-install', action='store_true',
help="""Specify 'unattended_install' for the installer item.""")
p.add_option('--catalog', default=[], action='append',
help="""Specify target catalogs.""")
p.add_option('--min-os-ver', default=None, metavar='VERSION',
help="""Minimum OS version for the installer item.""")
p.add_option('--max-os-ver', default=None, metavar='VERSION',
help="""Maximum OS version for the installer item.""")
p.add_option('--version', '-V', action='store_true',
help='Print the version of the munki tools and exit.')
p.add_option('--verbose', '-v', action='store_true',
help='Print more output.')
options, arguments = p.parse_args()
if options.version:
print munkicommon.get_version()
exit(0)
@@ -550,22 +598,27 @@ def main():
if len(arguments) == 0:
p.print_usage()
exit(0)
if len(arguments) > 1:
print >> sys.stderr, \
'This tool supports importing only one item at a time.'
print >> sys.stderr, 'Too many items given:'
print >> sys.stderr, '\t', '\n\t'.join(arguments)
exit(-1)
installer_item = arguments[0]
if not os.path.exists(installer_item):
print >> sys.stderr, '%s does not exist!' % installer_item
exit(-1)
# Verify that arguments, presumed to be for
# 'makepkginfo' are valid and return installer_item
installer_item = makePkgInfo(arguments, True)
if not installer_item:
cleanupAndExit(-1)
# Remove the installer_item from arguments
arguments.remove(installer_item)
# Strip trailing '/' from installer_item
installer_item = installer_item.rstrip('/')
item_ext = os.path.splitext(installer_item)[1]
if item_ext not in ['.pkg', '.mpkg', '.dmg', '.app']:
print >> sys.stderr, '%s is an unknown type.' % installer_item
print >> sys.stderr, (
'Unknown installer item type: "%s"' % installer_item)
exit(-1)
if not os.path.exists(installer_item):
print >> sys.stderr, '%s does not exist!' % installer_item
exit(-1)
if not pref('repo_path'):
@@ -593,12 +646,14 @@ def main():
print >> sys.stderr, ('Could not convert %s to a disk image.'
% installer_item)
cleanupAndExit(-1)
# generate pkginfo for the item
pkginfo = makePkgInfo(installer_item)
# append the installer_item to arguments which
# may have changed if bundle was wrapped into dmg
arguments.append(installer_item)
pkginfo = makePkgInfo(arguments, False)
if not pkginfo:
# makepkginfo returned an error
print >> sys.stderr, 'Getting package info failed.'
cleanupAndExit(-1)
if not options.nointeractive:
@@ -662,9 +717,6 @@ def main():
if newvalue:
pkginfo[key] = newvalue
if options.catalog:
pkginfo['catalogs'] = options.catalog
newvalue = raw_input('%15s [%s]: ' % ('Catalogs',
', '.join(pkginfo['catalogs'])))
if newvalue:
@@ -711,16 +763,6 @@ def main():
print >> sys.stderr, errmsg
cleanupAndExit(-1)
if options.unattended_install:
pkginfo['unattended_install'] = True
# default OS versions
if options.min_os_ver:
pkginfo['minimum_os_version'] = options.min_os_ver
if options.max_os_ver:
pkginfo['maximum_os_version'] = options.max_os_ver
# adjust the installer_item_location to match the actual location and name
pkginfo['installer_item_location'] = uploaded_pkgpath
+1 -1
View File
@@ -675,7 +675,7 @@ def runAdobeCS5AAMEEInstall(dmgpath):
'--setupBasePath=%s' % tmpdir, '--installDirPath=/',
'--mode=install'])
munkicommon.display_status_minor('Starting Adobe CS5 installer...')
munkicommon.display_status_minor('Starting Adobe installer...')
retcode = runAdobeInstallTool(cmd, number_of_payloads,
killAdobeAIR=True)
# now clean up our symlink hackfest
+1
View File
@@ -53,6 +53,7 @@ DEFAULT_CATALOG_URLS = {
'10.5': 'http://swscan.apple.com/content/catalogs/others/index-leopard.merged-1.sucatalog',
'10.6': 'http://swscan.apple.com/content/catalogs/others/index-leopard-snowleopard.merged-1.sucatalog',
'10.7': 'http://swscan.apple.com/content/catalogs/others/index-lion-snowleopard-leopard.merged-1.sucatalog.gz',
'10.8': 'http://swscan.apple.com/content/catalogs/others/index-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz'
}
# Preference domain for Apple Software Update.
+45 -7
View File
@@ -465,6 +465,16 @@ def format_time(timestamp=None):
return str(NSDate.dateWithTimeIntervalSince1970_(timestamp))
def validateDateFormat(datetime_string):
formatted_datetime_string = ''
try:
formatted_datetime_string = time.strftime(
'%Y-%m-%dT%H:%M:%SZ', time.strptime(datetime_string,
'%Y-%m-%dT%H:%M:%SZ'))
except:
pass
return formatted_datetime_string
def log(msg, logname=''):
"""Generic logging function"""
# date/time format string
@@ -1510,6 +1520,27 @@ def isInstallerItem(path):
return False
def getChoiceChangesXML(pkgitem):
"""Queries package for 'ChoiceChangesXML'"""
choices = []
try:
proc = subprocess.Popen(
['/usr/sbin/installer', '-showChoiceChangesXML', '-pkg', pkgitem],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, unused_err) = proc.communicate()
if out:
plist = FoundationPlist.readPlistFromString(out)
# list comprehension to populate choices with those items
# whose 'choiceAttribute' value is 'selected'
choices = [item for item in plist
if 'selected' in item['choiceAttribute']]
except:
# No choices found or something went wrong
pass
return choices
def getPackageMetaData(pkgitem):
"""
Queries an installer item (.pkg, .mpkg, .dist)
@@ -1847,7 +1878,7 @@ def getSPApplicationData():
# system_profiler xml is an array
SP_APPCACHE = {}
for item in plist[0]['_items']:
SP_APPCACHE[item.get('path')] = item
SP_APPCACHE[item.get('path')] = item
except Exception:
pass
return SP_APPCACHE
@@ -1983,6 +2014,7 @@ def getMachineFacts():
MACHINE['machine_model'] = hardware_info.get('machine_model', 'UNKNOWN')
MACHINE['munki_version'] = get_version()
MACHINE['ipv4_address'] = get_ipv4_addresses()
MACHINE['serial_number'] = hardware_info.get('serial_number', 'UNKNOWN')
return MACHINE
@@ -1992,11 +2024,13 @@ def getConditions():
which can be placed into /usr/local/munki/conditions"""
global CONDITIONS
if not CONDITIONS:
# define path to conditions directory which would contain admin created scripts
# define path to conditions directory which would contain
# admin created scripts
scriptdir = os.path.realpath(os.path.dirname(sys.argv[0]))
conditionalscriptdir = os.path.join(scriptdir, "conditions")
# define path to ConditionalItems.plist
conditionalitemspath = os.path.join(pref('ManagedInstallDir'), 'ConditionalItems.plist')
conditionalitemspath = os.path.join(
pref('ManagedInstallDir'), 'ConditionalItems.plist')
try:
# delete CondtionalItems.plist so that we're starting fresh
os.unlink(conditionalitemspath)
@@ -2008,13 +2042,15 @@ def getConditions():
if conditionalscript.startswith('.'):
# skip files that start with a period
continue
conditionalscriptpath = os.path.join(conditionalscriptdir, conditionalscript)
conditionalscriptpath = os.path.join(
conditionalscriptdir, conditionalscript)
if os.path.isdir(conditionalscriptpath):
# skip directories in conditions directory
continue
try:
# attempt to execute condition script
result, stdout, stderr = utils.runExternalScript(conditionalscriptpath)
result, stdout, stderr = utils.runExternalScript(
conditionalscriptpath)
except utils.ScriptNotFoundError:
pass # script is not required, so pass
except utils.RunExternalScriptError, e:
@@ -2022,12 +2058,14 @@ def getConditions():
else:
# /usr/local/munki/conditions does not exist
pass
if os.path.exists(conditionalitemspath) and validPlist(conditionalitemspath):
if (os.path.exists(conditionalitemspath) and
validPlist(conditionalitemspath)):
# import conditions into CONDITIONS dict
CONDITIONS = FoundationPlist.readPlist(conditionalitemspath)
os.unlink(conditionalitemspath)
else:
# either ConditionalItems.plist does not exist or does not pass validation
# either ConditionalItems.plist does not exist
# or does not pass validation
CONDITIONS = {}
return CONDITIONS
+36 -7
View File
@@ -977,6 +977,27 @@ def getItemDetail(name, cataloglist, vers=''):
for index in indexlist:
item = CATALOG[catalogname]['items'][index]
# we have an item whose name and version matches the request.
if item.get('minimum_munki_version'):
min_munki_vers = item['minimum_munki_version']
munkicommon.display_debug1(
'Considering item %s, ' % item['name'] +
'version %s ' % item['version'] +
'with minimum Munki version required %s'
% min_munki_vers)
munkicommon.display_debug1('Our Munki version is %s' %
MACHINE['munki_version'])
if (munkicommon.MunkiLooseVersion(MACHINE['munki_version'])
< munkicommon.MunkiLooseVersion(min_munki_vers)):
# skip this one, go to the next
reason = (('Rejected item %s, version %s '
'with minimum Munki version required %s. '
"Our Munki version is %s.")
% (item['name'], item['version'],
item['minimum_munki_version'],
MACHINE['munki_version']))
rejected_items.append(reason)
continue
# now check to see if it meets os and cpu requirements
if item.get('minimum_os_version', ''):
min_os_vers = item['minimum_os_version']
@@ -1323,7 +1344,8 @@ def lookForUpdates(itemname, cataloglist):
num_updates = len(update_list)
# format the update list for better on-screen viewing
update_list_display = ", ".join(str(x) for x in update_list)
munkicommon.display_debug1('Found %s update(s): %s' % (num_updates, update_list_display))
munkicommon.display_debug1(
'Found %s update(s): %s' % (num_updates, update_list_display))
return update_list
@@ -2341,12 +2363,19 @@ def getPrimaryManifest(alternate_id):
clientidentifier)
manifest = getmanifest(manifesturl + clientidentifier,
suppress_errors=True)
if not manifest:
# last resort - try for the site_default manifest
clientidentifier = 'site_default'
munkicommon.display_detail('Request failed. ' +
'Trying %s...' %
if not manifest:
# try the machine serial number
clientidentifier = MACHINE['serial_number']
if clientidentifier != 'UNKNOWN':
munkicommon.display_detail('Request failed. Trying %s...' %
clientidentifier)
manifest = getmanifest(manifesturl + clientidentifier,
suppress_errors=True)
if not manifest:
# last resort - try for the site_default manifest
clientidentifier = 'site_default'
munkicommon.display_detail('Request failed. Trying %s...' %
clientidentifier)
if not manifest:
manifest = getmanifest(
@@ -2447,7 +2476,6 @@ def check(client_id='', localmanifestpath=None):
global CONDITIONS
munkicommon.getConditions()
CONDITIONS = munkicommon.getConditions()
munkicommon.report['ConditionalItems'] = CONDITIONS
ManagedInstallDir = munkicommon.pref('ManagedInstallDir')
if munkicommon.munkistatusoutput:
@@ -2476,6 +2504,7 @@ def check(client_id='', localmanifestpath=None):
# set up INFO_OBJECT for conditional item comparisons
makePredicateInfoObject()
munkicommon.report['Conditions'] = INFO_OBJECT
munkicommon.display_detail('**Checking for installs**')
processManifestForKey(mainmanifestpath, 'managed_installs',