diff --git a/code/client/makepkginfo b/code/client/makepkginfo index f3829d5b..2be48eb1 100755 --- a/code/client/makepkginfo +++ b/code/client/makepkginfo @@ -40,8 +40,8 @@ import re # our libs from munkilib import info from munkilib import osutils -from munkilib import pkginfolib from munkilib import FoundationPlist +from munkilib.admin import pkginfolib def has_valid_install_critieria(pkginfo): diff --git a/code/client/munkiimport b/code/client/munkiimport index c4d3cb7f..77fb6277 100755 --- a/code/client/munkiimport +++ b/code/client/munkiimport @@ -38,12 +38,12 @@ from munkilib.cliutils import raw_input_with_default from munkilib import info from munkilib import dmgutils from munkilib import munkihash -from munkilib import munkiimportlib from munkilib import munkirepo from munkilib import osutils -from munkilib import pkginfolib from munkilib import pkgutils from munkilib import FoundationPlist +from munkilib.admin import munkiimportlib +from munkilib.admin import pkginfolib def make_dmg(pkgpath): diff --git a/code/client/munkilib/admin/__init__.py b/code/client/munkilib/admin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/code/client/munkilib/admin/munkiimportlib.py b/code/client/munkilib/admin/munkiimportlib.py new file mode 100644 index 00000000..3f7c5ad7 --- /dev/null +++ b/code/client/munkilib/admin/munkiimportlib.py @@ -0,0 +1,494 @@ +# encoding: utf-8 +# +# Copyright 2017 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 +# +# https://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. +""" +munkiimportlib + +Created by Greg Neagle on 2017-11-18. +Routines used by munkimport to import items into Munki repo +""" + +# std lib imports +import os +import sys + +# our lib imports +from .. import iconutils +from .. import dmgutils +from .. import munkihash +from .. import munkirepo +from .. import osinstaller +from .. import osutils +from .. import pkgutils +from .. import FoundationPlist + +from ..cliutils import pref + + +class RepoCopyError(Exception): + '''Exception raised when copying a file to the repo fails''' + pass + + +def list_items_of_kind(repo, kind): + '''Returns a list of items of kind. Relative pathnames are prepended + with kind. (example: ['icons/Bar.png', 'icons/Foo.png'])''' + return [os.path.join(kind, item) for item in repo.itemlist(kind)] + + +def copy_item_to_repo(repo, itempath, vers, subdirectory=''): + """Copies an item to the appropriate place in the repo. + If itempath is a path within the repo/pkgs directory, copies nothing. + Renames the item if an item already exists with that name. + Returns the relative path to the item.""" + + destination_path = os.path.join('pkgs', subdirectory) + item_name = os.path.basename(itempath) + destination_path_name = os.path.join(destination_path, item_name) + + name, ext = os.path.splitext(item_name) + if vers: + if not name.endswith(vers): + # add the version number to the end of the filename + item_name = '%s-%s%s' % (name, vers, ext) + destination_path_name = os.path.join(destination_path, item_name) + + index = 0 + try: + pkgs_list = list_items_of_kind(repo, 'pkgs') + except munkirepo.RepoError, err: + raise RepoCopyError(u'Unable to get list of current pkgs: %s' + % unicode(err)) + while destination_path_name in pkgs_list: + print 'File %s already exists...' % destination_path_name + # try appending numbers until we have a unique name + index += 1 + item_name = '%s__%s%s' % (name, index, ext) + destination_path_name = os.path.join(destination_path, item_name) + + print 'Copying %s to %s...' % (os.path.basename(itempath), + destination_path_name) + try: + repo.put_from_local_file(destination_path_name, itempath) + except munkirepo.RepoError, err: + raise RepoCopyError(u'Unable to copy %s to %s: %s' + % (itempath, destination_path_name, unicode(err))) + else: + return os.path.join(subdirectory, item_name) + + +def copy_pkginfo_to_repo(repo, pkginfo, subdirectory=''): + """Saves pkginfo to /pkgsinfo/subdirectory""" + # less error checking because we copy the installer_item + # first and bail if it fails... + destination_path = os.path.join('pkgsinfo', subdirectory) + pkginfo_ext = pref('pkginfo_extension') or '' + if pkginfo_ext and not pkginfo_ext.startswith('.'): + pkginfo_ext = '.' + pkginfo_ext + pkginfo_name = '%s-%s%s' % (pkginfo['name'], pkginfo['version'], + pkginfo_ext) + pkginfo_path = os.path.join(destination_path, pkginfo_name) + index = 0 + try: + pkgsinfo_list = list_items_of_kind(repo, 'pkgsinfo') + except munkirepo.RepoError, err: + raise RepoCopyError(u'Unable to get list of current pkgsinfo: %s' + % unicode(err)) + while pkginfo_path in pkgsinfo_list: + index += 1 + pkginfo_name = '%s-%s__%s%s' % (pkginfo['name'], pkginfo['version'], + index, pkginfo_ext) + pkginfo_path = os.path.join(destination_path, pkginfo_name) + + print 'Saving pkginfo to %s...' % pkginfo_path + try: + pkginfo_str = FoundationPlist.writePlistToString(pkginfo) + except FoundationPlist.NSPropertyListWriteException, errmsg: + raise RepoCopyError(errmsg) + try: + repo.put(pkginfo_path, pkginfo_str) + except munkirepo.RepoError, err: + raise RepoCopyError('Unable to save pkginfo to %s: %s' + % (pkginfo_path, unicode(err))) + + +class CatalogDBException(Exception): + '''Exception to throw if we can't make a pkginfo DB''' + pass + + +class CatalogReadException(CatalogDBException): + '''Exception to throw if we can't read the all catalog''' + pass + + +class CatalogDecodeException(CatalogDBException): + '''Exception to throw if we can't decode the all catalog''' + pass + + +def make_catalog_db(repo): + """Returns a dict we can use like a database""" + + try: + plist = repo.get('catalogs/all') + except munkirepo.RepoError, err: + raise CatalogReadException(err) + + try: + catalogitems = FoundationPlist.readPlistFromString(plist) + except FoundationPlist.NSPropertyListSerializationException, err: + raise CatalogDecodeException(err) + + pkgid_table = {} + app_table = {} + installer_item_table = {} + hash_table = {} + profile_table = {} + + itemindex = -1 + for item in catalogitems: + itemindex = itemindex + 1 + name = item.get('name', 'NO NAME') + vers = item.get('version', 'NO VERSION') + + if name == 'NO NAME' or vers == 'NO VERSION': + print >> sys.stderr, 'WARNING: Bad pkginfo: %s' % item + + # add to hash table + if 'installer_item_hash' in item: + if not item['installer_item_hash'] in hash_table: + hash_table[item['installer_item_hash']] = [] + hash_table[item['installer_item_hash']].append(itemindex) + + # add to installer item table + if 'installer_item_location' in item: + installer_item_name = os.path.basename( + item['installer_item_location']) + (name, ext) = os.path.splitext(installer_item_name) + if '-' in name: + (name, vers) = pkgutils.nameAndVersion(name) + installer_item_name = name + ext + if not installer_item_name in installer_item_table: + installer_item_table[installer_item_name] = {} + if not vers in installer_item_table[installer_item_name]: + installer_item_table[installer_item_name][vers] = [] + installer_item_table[installer_item_name][vers].append(itemindex) + + # add to table of receipts + for receipt in item.get('receipts', []): + try: + if 'packageid' in receipt and 'version' in receipt: + pkgid = receipt['packageid'] + pkgvers = receipt['version'] + if not pkgid in pkgid_table: + pkgid_table[pkgid] = {} + if not pkgvers in pkgid_table[pkgid]: + pkgid_table[pkgid][pkgvers] = [] + pkgid_table[pkgid][pkgvers].append(itemindex) + except TypeError: + print >> sys.stderr, ( + 'Bad receipt data for %s-%s: %s' % (name, vers, receipt)) + + # add to table of installed applications + for install in item.get('installs', []): + try: + if install.get('type') == 'application': + if 'path' in install: + if not install['path'] in app_table: + app_table[install['path']] = {} + if not vers in app_table[install['path']]: + app_table[install['path']][vers] = [] + app_table[install['path']][vers].append(itemindex) + except TypeError: + print >> sys.stderr, ( + 'Bad install data for %s-%s: %s' % (name, vers, install)) + + # add to table of PayloadIdentifiers + if 'PayloadIdentifier' in item: + if not item['PayloadIdentifier'] in profile_table: + profile_table[item['PayloadIdentifier']] = {} + if not vers in profile_table[item['PayloadIdentifier']]: + profile_table[item['PayloadIdentifier']][vers] = [] + profile_table[item['PayloadIdentifier']][vers].append(itemindex) + + pkgdb = {} + pkgdb['hashes'] = hash_table + pkgdb['receipts'] = pkgid_table + pkgdb['applications'] = app_table + pkgdb['installer_items'] = installer_item_table + pkgdb['profiles'] = profile_table + pkgdb['items'] = catalogitems + + return pkgdb + + +def find_matching_pkginfo(repo, pkginfo): + """Looks through repo catalogs looking for matching pkginfo + Returns a pkginfo dictionary, or an empty dict""" + + def compare_version_keys(value_a, value_b): + """Internal comparison function for use in sorting""" + return cmp(pkgutils.MunkiLooseVersion(value_b), + pkgutils.MunkiLooseVersion(value_a)) + + try: + catdb = make_catalog_db(repo) + except CatalogReadException, err: + # could not retreive catalogs/all + # do we have any existing pkgsinfo items? + pkgsinfo_items = repo.itemlist('pkgsinfo') + if len(pkgsinfo_items): + # there _are_ existing pkgsinfo items. + # warn about the problem since we can't seem to read catalogs/all + print (u'Could not get a list of existing items from the repo: %s' + % unicode(err)) + return {} + except CatalogDBException, err: + # other error while processing catalogs/all + print (u'Could not get a list of existing items from the repo: %s' + % unicode(err)) + return {} + + if 'installer_item_hash' in pkginfo: + matchingindexes = catdb['hashes'].get( + pkginfo['installer_item_hash']) + if matchingindexes: + return catdb['items'][matchingindexes[0]] + + if 'receipts' in pkginfo: + pkgids = [item['packageid'] + for item in pkginfo['receipts'] + if 'packageid' in item] + if pkgids: + possiblematches = catdb['receipts'].get(pkgids[0]) + if possiblematches: + versionlist = possiblematches.keys() + versionlist.sort(compare_version_keys) + # go through possible matches, newest version first + for versionkey in versionlist: + testpkgindexes = possiblematches[versionkey] + for pkgindex in testpkgindexes: + testpkginfo = catdb['items'][pkgindex] + testpkgids = [item['packageid'] for item in + testpkginfo.get('receipts', []) + if 'packageid' in item] + if set(testpkgids) == set(pkgids): + return testpkginfo + + if 'installs' in pkginfo: + applist = [item for item in pkginfo['installs'] + if item['type'] == 'application' + and 'path' in item] + if applist: + app = applist[0]['path'] + possiblematches = catdb['applications'].get(app) + if possiblematches: + versionlist = possiblematches.keys() + versionlist.sort(compare_version_keys) + indexes = catdb['applications'][app][versionlist[0]] + return catdb['items'][indexes[0]] + + if 'PayloadIdentifier' in pkginfo: + identifier = pkginfo['PayloadIdentifier'] + possiblematches = catdb['profiles'].get(identifier) + if possiblematches: + versionlist = possiblematches.keys() + versionlist.sort(compare_version_keys) + indexes = catdb['profiles'][identifier][versionlist[0]] + return catdb['items'][indexes[0]] + + # no matches by receipts or installed applications, + # let's try to match based on installer_item_name + installer_item_name = os.path.basename( + pkginfo.get('installer_item_location', '')) + possiblematches = catdb['installer_items'].get(installer_item_name) + if possiblematches: + versionlist = possiblematches.keys() + versionlist.sort(compare_version_keys) + indexes = catdb['installer_items'][installer_item_name][versionlist[0]] + return catdb['items'][indexes[0]] + + # if we get here, we found no matches + return {} + + +def get_icon_path(pkginfo): + """Return path for icon""" + icon_name = pkginfo.get('icon_name') or pkginfo['name'] + if not os.path.splitext(icon_name)[1]: + icon_name += u'.png' + return os.path.join(u'icons', icon_name) + + +def icon_exists_in_repo(repo, pkginfo): + """Returns True if there is an icon for this item in the repo""" + icon_path = get_icon_path(pkginfo) + try: + icon_list = list_items_of_kind(repo, 'icons') + except munkirepo.RepoError, err: + raise RepoCopyError(u'Unable to get list of current icons: %s' + % unicode(err)) + if icon_path in icon_list: + return True + return False + + +def add_icon_hash_to_pkginfo(pkginfo): + """Adds the icon hash tp pkginfo if the icon exists in repo""" + icon_path = get_icon_path(pkginfo) + if os.path.isfile(icon_path): + pkginfo['icon_hash'] = munkihash.getsha256hash(icon_path) + + +def generate_png_from_startosinstall_item(repo, dmg_path, pkginfo): + '''Generates a product icon from a startosinstall item + and uploads to the repo''' + mountpoints = dmgutils.mountdmg(dmg_path) + if mountpoints: + mountpoint = mountpoints[0] + app_path = osinstaller.find_install_macos_app(mountpoint) + icon_path = iconutils.findIconForApp(app_path) + if icon_path: + convert_and_install_icon(repo, pkginfo, icon_path) + else: + print 'No application icons found.' + dmgutils.unmountdmg(mountpoint) + + +def generate_png_from_dmg_item(repo, dmg_path, pkginfo): + '''Generates a product icon from a copy_from_dmg item + and uploads to the repo''' + mountpoints = dmgutils.mountdmg(dmg_path) + if mountpoints: + mountpoint = mountpoints[0] + apps = [item for item in pkginfo.get('items_to_copy', []) + if item.get('source_item', '').endswith('.app')] + if len(apps): + app_path = os.path.join(mountpoint, apps[0]['source_item']) + icon_path = iconutils.findIconForApp(app_path) + if icon_path: + convert_and_install_icon(repo, pkginfo, icon_path) + else: + print 'No application icons found.' + else: + print 'No application icons found.' + dmgutils.unmountdmg(mountpoint) + + +def generate_pngs_from_pkg(repo, item_path, pkginfo): + '''Generates a product icon (or candidate icons) from an installer pkg + and uploads to the repo''' + icon_paths = [] + mountpoint = None + pkg_path = None + if pkgutils.hasValidDiskImageExt(item_path): + dmg_path = item_path + mountpoints = dmgutils.mountdmg(dmg_path) + if mountpoints: + mountpoint = mountpoints[0] + if pkginfo.get('package_path'): + pkg_path = os.path.join(mountpoint, pkginfo['package_path']) + else: + # find first item that appears to be a pkg at the root + for fileitem in osutils.listdir(mountpoints[0]): + if pkgutils.hasValidPackageExt(fileitem): + pkg_path = os.path.join(mountpoint, fileitem) + break + elif pkgutils.hasValidPackageExt(item_path): + pkg_path = item_path + if pkg_path: + if os.path.isdir(pkg_path): + icon_paths = iconutils.extractAppIconsFromBundlePkg(pkg_path) + else: + icon_paths = iconutils.extractAppIconsFromFlatPkg(pkg_path) + + if mountpoint: + dmgutils.unmountdmg(mountpoint) + + if len(icon_paths) == 1: + convert_and_install_icon(repo, pkginfo, icon_paths[0]) + elif len(icon_paths) > 1: + index = 1 + for icon_path in icon_paths: + convert_and_install_icon(repo, pkginfo, icon_path, index=index) + index += 1 + else: + print 'No application icons found.' + + +def convert_and_install_icon(repo, pkginfo, icon_path, index=None): + '''Convert icon file to png and save to repo icon path''' + destination_path = 'icons' + if index is not None: + destination_name = pkginfo['name'] + '_' + str(index) + else: + destination_name = pkginfo['name'] + + png_name = destination_name + u'.png' + repo_png_path = os.path.join(destination_path, png_name) + local_png_tmp = os.path.join(osutils.tmpdir(), png_name) + result = iconutils.convertIconToPNG(icon_path, local_png_tmp) + if result: + try: + repo.put_from_local_file(repo_png_path, local_png_tmp) + print 'Created icon: %s' % repo_png_path + except munkirepo.RepoError, err: + print >> sys.stderr, (u'Error uploading icon to %s: %s' + % (repo_png_path, unicode(err))) + else: + print >> sys.stderr, u'Error converting %s to png.' % icon_path + + +def copy_icon_to_repo(repo, iconpath): + """Saves a product icon to the repo""" + destination_path = 'icons' + icon_name = os.path.basename(iconpath) + destination_path_name = os.path.join(destination_path, icon_name) + + try: + icon_list = list_items_of_kind(repo, 'icons') + except munkirepo.RepoError, err: + raise RepoCopyError(u'Unable to get list of current icons: %s' + % unicode(err)) + if destination_path_name in icon_list: + # remove any existing icon in the repo + try: + repo.delete(destination_path_name) + except munkirepo.RepoError, err: + raise RepoCopyError('Could not remove existing %s: %s' + % (destination_path_name, unicode(err))) + print 'Copying %s to %s...' % (icon_name, destination_path_name) + try: + repo.put_from_local_file(destination_path_name, iconpath) + except munkirepo.RepoError, err: + raise RepoCopyError('Unable to copy %s to %s: %s' + % (iconpath, destination_path_name, unicode(err))) + + +def extract_and_copy_icon(repo, installer_item, pkginfo): + '''Extracts an icon from an installer item, converts it to a png, and + copies to repo''' + installer_type = pkginfo.get('installer_type') + if installer_type == 'copy_from_dmg': + generate_png_from_dmg_item(repo, installer_item, pkginfo) + elif installer_type == 'startosinstall': + generate_png_from_startosinstall_item(repo, installer_item, pkginfo) + elif installer_type in [None, '']: + generate_pngs_from_pkg(repo, installer_item, pkginfo) + else: + print >> sys.stderr, ( + 'WARNING: Can\'t generate icons from installer_type: %s.' + % installer_type) diff --git a/code/client/munkilib/admin/pkginfolib.py b/code/client/munkilib/admin/pkginfolib.py new file mode 100755 index 00000000..83c9d00f --- /dev/null +++ b/code/client/munkilib/admin/pkginfolib.py @@ -0,0 +1,1090 @@ +# encoding: utf-8 +# +# Copyright 2017 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 +# +# https://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. +""" +pkginfolib + +Created by Greg Neagle on 2017-11-18. +Routines used by makepkginfo to create pkginfo files +""" + +# standard libs +import optparse +import os +import re +import sys +import time + +# Apple frameworks via PyObjC +# PyLint cannot properly find names inside Cocoa libraries, so issues bogus +# No name 'Foo' in module 'Bar' warnings. Disable them. +# pylint: disable=E0611 +from Foundation import NSDate, NSUserName +# pylint: enable=E0611 + +# our libs +from .. import dmgutils +from .. import info +from .. import munkihash +from .. import osinstaller +from .. import osutils +from .. import pkgutils +from .. import profiles + +from .. import FoundationPlist + +from ..adobeutils import adobeinfo + + +# circumvent cfprefsd plist scanning +os.environ['__CFPREFERENCES_AVOID_DAEMON'] = "1" + + +class PkgInfoGenerationError(Exception): + '''Error to raise if there is a fatal error when generating pkginfo''' + pass + + +def make_pkginfo_metadata(): + '''Records information about the environment in which the pkginfo was +created so we have a bit of an audit trail. Returns a dictionary.''' + metadata = {} + metadata['created_by'] = NSUserName() + metadata['creation_date'] = NSDate.new() + metadata['munki_version'] = info.get_version() + metadata['os_version'] = osutils.getOsVersion(only_major_minor=False) + return metadata + + +def convert_date_string_to_nsdate(datetime_string): + '''Converts a string in the "2013-04-25T20:00:00Z" format or + "2013-04-25 20:00:00 +0000" format to an NSDate''' + nsdate_format = '%Y-%m-%dT%H:%M:%SZ' + iso_format = '%Y-%m-%d %H:%M:%S +0000' + fallback_format = '%Y-%m-%d %H:%M:%S' + try: + tobj = time.strptime(datetime_string, nsdate_format) + except ValueError: + try: + tobj = time.strptime(datetime_string, iso_format) + except ValueError: + try: + tobj = time.strptime(datetime_string, fallback_format) + except ValueError: + return None + iso_date_string = time.strftime(iso_format, tobj) + return NSDate.dateWithString_(iso_date_string) + + +def get_catalog_info_from_path(pkgpath, options): + """Gets package metadata for the package at pathname. + Returns cataloginfo""" + cataloginfo = {} + if os.path.exists(pkgpath): + cataloginfo = pkgutils.getPackageMetaData(pkgpath) + if options.installer_choices_xml: + installer_choices_xml = pkgutils.getChoiceChangesXML(pkgpath) + if installer_choices_xml: + cataloginfo['installer_choices_xml'] = installer_choices_xml + + 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 = adobeinfo.getAdobeCatalogInfo(pkgpath) + #mountpoints[0], adobepkgname) + + else: + # maybe an Adobe installer/updater/patcher? + cataloginfo = adobeinfo.getAdobeCatalogInfo(pkgpath, + options.pkgname or '') + return cataloginfo + + +class ProfileMetadataGenerationError(PkgInfoGenerationError): + '''Error to raise when we can't generate config profile metadata''' + pass + + +def get_catalog_info_for_profile(profile_path): + '''Populates some metadata for profile pkginfo''' + cataloginfo = {} + profile = profiles.read_profile(profile_path) + if profile.get('PayloadType') == 'Configuration': + try: + cataloginfo['PayloadIdentifier'] = profile['PayloadIdentifier'] + except (KeyError, AttributeError): + # this thing is broken! return the empty info + return cataloginfo + cataloginfo['name'] = os.path.basename(profile_path) + cataloginfo['display_name'] = profile.get( + 'PayloadDisplayName', cataloginfo['name']) + cataloginfo['description'] = profile.get('PayloadDescription', '') + cataloginfo['version'] = '1.0' + cataloginfo['installer_type'] = 'profile' + cataloginfo['uninstallable'] = True + cataloginfo['uninstall_method'] = 'remove_profile' + cataloginfo['unattended_install'] = True + cataloginfo['unattended_uninstall'] = True + cataloginfo['minimum_os_version'] = '10.7' + cataloginfo['minimum_munki_version'] = '2.2' + else: + raise ProfileMetadataGenerationError( + 'Profile PayloadType is %s' % profile.get('PayloadType')) + return cataloginfo + + +def get_catalog_info_from_dmg(dmgpath, options): + """ + * Mounts a disk image if it's not already mounted + * Gets catalog info for the first installer item found at the root level. + * Unmounts the disk image if it wasn't already mounted + + To-do: handle multiple installer items on a disk image(?) + """ + cataloginfo = None + was_already_mounted = dmgutils.diskImageIsMounted(dmgpath) + mountpoints = dmgutils.mountdmg(dmgpath, use_existing_mounts=True) + if not mountpoints: + raise PkgInfoGenerationError("Could not mount %s!" % dmgpath) + + if options.pkgname: + pkgpath = os.path.join(mountpoints[0], options.pkgname) + cataloginfo = get_catalog_info_from_path(pkgpath, options) + if cataloginfo: + cataloginfo['package_path'] = options.pkgname + elif not options.item: + # search for first package at root + for fsitem in osutils.listdir(mountpoints[0]): + itempath = os.path.join(mountpoints[0], fsitem) + if pkgutils.hasValidInstallerItemExt(itempath): + cataloginfo = get_catalog_info_from_path(itempath, options) + # get out of fsitem loop + break + + if not cataloginfo and not options.item: + # look for one of the many possible Adobe installer/updaters + cataloginfo = adobeinfo.getAdobeCatalogInfo( + mountpoints[0], options.pkgname or '') + + if not cataloginfo: + # could be a wrapped Install macOS.app + install_macos_app = osinstaller.find_install_macos_app(mountpoints[0]) + if (install_macos_app and options.print_warnings and + osinstaller.install_macos_app_is_stub(install_macos_app)): + print >> sys.stderr, ( + 'WARNING: %s appears to be an Install macOS application, but ' + 'it does not contain Contents/SharedSupport/InstallESD.dmg' + % os.path.basename(install_macos_app)) + cataloginfo = osinstaller.get_catalog_info(mountpoints[0]) + + 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 + + # Create a path by joining the mount point and the provided item + # path. + # The os.path.join method will intelligently take care of the + # following scenarios: + # ("/mountpoint", "relative/path") -> "/mountpoint/relative/path" + # ("/mountpoint", "/absolute/path") -> "/absolute/path" + itempath = os.path.join(mountpoints[0], item) + + # Now check that the item actually exists and is located within the + # mount point + if os.path.exists(itempath) and itempath.startswith(mountpoints[0]): + iteminfo = getiteminfo(itempath) + else: + if not was_already_mounted: + dmgutils.unmountdmg(mountpoints[0]) + raise PkgInfoGenerationError( + "%s not found on disk image." % item) + else: + # no item specified; look for an application at root of + # mounted dmg + item = '' + for itemname in osutils.listdir(mountpoints[0]): + itempath = os.path.join(mountpoints[0], itemname) + if pkgutils.isApplication(itempath): + item = itemname + iteminfo = getiteminfo(itempath) + if iteminfo: + break + + if iteminfo: + item_to_copy = {} + if os.path.isabs(item): + # Absolute path given + # Remove the mountpoint from item path + mountpoint_pattern = "^%s/" % mountpoints[0] + item = re.sub(mountpoint_pattern, '', item) + + if options.destitemname: + # An alternate 'destination_item' name has been specified + dest_item = options.destitemname + item_to_copy['destination_item'] = options.destitemname + else: + dest_item = item + + # Use only the last path component when + # composing the path key of an installs item + dest_item_filename = os.path.split(dest_item)[1] + + if options.destinationpath: + iteminfo['path'] = os.path.join( + options.destinationpath, dest_item_filename) + else: + iteminfo['path'] = os.path.join( + "/Applications", dest_item_filename) + cataloginfo = {} + cataloginfo['name'] = iteminfo.get( + 'CFBundleName', os.path.splitext(item)[0]) + version_comparison_key = iteminfo.get( + 'version_comparison_key', "CFBundleShortVersionString") + cataloginfo['version'] = \ + iteminfo.get(version_comparison_key, "0") + cataloginfo['installs'] = [iteminfo] + cataloginfo['installer_type'] = "copy_from_dmg" + 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 + if not was_already_mounted: + dmgutils.unmountdmg(mountpoints[0]) + return cataloginfo + + +# TO-DO: this (or a similar) function is defined several places. De-dupe. +def readfile(path): + '''Reads file at path. Returns a string.''' + try: + fileobject = open(os.path.expanduser(path), mode='r', buffering=1) + data = fileobject.read() + fileobject.close() + return data + except (OSError, IOError): + print >> sys.stderr, "Couldn't read %s" % path + return "" + + +def read_file_or_string(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 + 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 pkgutils.isApplication(itempath): + infodict['type'] = 'application' + infodict['path'] = itempath + plist = pkgutils.getBundleInfo(itempath) + for key in ['CFBundleName', 'CFBundleIdentifier', + 'CFBundleShortVersionString', 'CFBundleVersion']: + if key in plist: + infodict[key] = plist[key] + if 'LSMinimumSystemVersion' in plist: + infodict['minosversion'] = plist['LSMinimumSystemVersion'] + elif 'LSMinimumSystemVersionByArchitecture' in plist: + # just grab the highest version if more than one is listed + versions = [item[1] for item in + plist['LSMinimumSystemVersionByArchitecture'].items()] + highest_version = str(max([pkgutils.MunkiLooseVersion(version) + for version in versions])) + infodict['minosversion'] = highest_version + 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 = pkgutils.getBundleInfo(itempath) + for key in ['CFBundleShortVersionString', 'CFBundleVersion']: + if key in plist: + infodict[key] = plist[key] + + elif itempath.endswith("Info.plist") or itempath.endswith("version.plist"): + infodict['type'] = 'plist' + infodict['path'] = itempath + try: + plist = FoundationPlist.readPlist(itempath) + for key in ['CFBundleShortVersionString', 'CFBundleVersion']: + if key in plist: + infodict[key] = plist[key] + except FoundationPlist.NSPropertyListSerializationException: + pass + + # let's help the admin -- if CFBundleShortVersionString is empty + # or doesn't start with a digit, and CFBundleVersion is there + # use CFBundleVersion as the version_comparison_key + if (not infodict.get('CFBundleShortVersionString') or + infodict['CFBundleShortVersionString'][0] + not in '0123456789'): + if infodict.get('CFBundleVersion'): + infodict['version_comparison_key'] = 'CFBundleVersion' + elif 'CFBundleShortVersionString' in infodict: + infodict['version_comparison_key'] = 'CFBundleShortVersionString' + + if ('CFBundleShortVersionString' not in infodict and + 'CFBundleVersion' not in infodict): + infodict['type'] = 'file' + infodict['path'] = itempath + if os.path.isfile(itempath): + infodict['md5checksum'] = munkihash.getmd5hash(itempath) + return infodict + + +class AtttributeDict(dict): + '''Class that allow us to access foo['bar'] as foo.bar, and return None + if foo.bar is not defined.''' + def __getattr__(self, name): + '''Allow access to dictionary keys as attribute names.''' + try: + return super(AtttributeDict, self).__getattr__(name) + except AttributeError: + try: + return self[name] + except KeyError: + return None + + +def makepkginfo(installeritem, options): + '''Return a pkginfo dictionary for item''' + + if isinstance(options, dict): + options = AtttributeDict(options) + + pkginfo = {} + installs = [] + if installeritem and os.path.exists(installeritem): + # Check if the item is a mount point for a disk image + if dmgutils.pathIsVolumeMountPoint(installeritem): + # Get the disk image path for the mount point + # and use that instead of the original item + installeritem = dmgutils.diskImageForMountPoint(installeritem) + + # get size of installer item + itemsize = 0 + itemhash = "N/A" + if os.path.isfile(installeritem): + itemsize = int(os.path.getsize(installeritem)) + itemhash = munkihash.getsha256hash(installeritem) + + if pkgutils.hasValidDiskImageExt(installeritem): + if dmgutils.DMGisWritable(installeritem) and options.print_warnings: + print >> sys.stderr, ( + "WARNING: %s is a writable disk image. " + "Checksum verification is not supported." % installeritem) + print >> sys.stderr, ( + "WARNING: Consider converting %s to a read-only disk" + "image." % installeritem) + itemhash = "N/A" + pkginfo = get_catalog_info_from_dmg(installeritem, options) + if (pkginfo and + pkginfo.get('installer_type') == "AdobeCS5Installer"): + raise PkgInfoGenerationError( + "This disk image appears to contain an Adobe CS5/CS6 " + "product install.\n" + "Please use Adobe Application Manager, Enterprise " + "Edition (AAMEE) to create an installation package " + "for this product.") + if not pkginfo: + raise PkgInfoGenerationError( + "Could not find a supported installer item in %s!" + % installeritem) + + elif pkgutils.hasValidPackageExt(installeritem): + pkginfo = get_catalog_info_from_path(installeritem, options) + if not pkginfo: + raise PkgInfoGenerationError( + "%s doesn't appear to be a valid installer item!" + % installeritem) + if os.path.isdir(installeritem) and options.print_warnings: + 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") % installeritem + # need to walk the dir and add it all up + for (path, dummy_dirs, files) in os.walk(installeritem): + 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) + + elif pkgutils.hasValidConfigProfileExt(installeritem): + try: + pkginfo = get_catalog_info_for_profile(installeritem) + except ProfileMetadataGenerationError, err: + print >> sys.stderr, err + raise PkgInfoGenerationError( + "%s doesn't appear to be a supported configuration " + "profile!" % installeritem) + else: + raise PkgInfoGenerationError( + "%s is not a valid installer item!" % installeritem) + + pkginfo['installer_item_size'] = int(itemsize/1024) + if itemhash != "N/A": + pkginfo['installer_item_hash'] = itemhash + + # try to generate the correct item location + temppath = installeritem + location = "" + while len(temppath) > 4: + if temppath.endswith('/pkgs'): + location = installeritem[len(temppath)+1:] + break + else: + temppath = os.path.dirname(temppath) + + if not location: + #just the filename + location = os.path.split(installeritem)[1] + pkginfo['installer_item_location'] = location + + # ADOBE STUFF - though maybe generalizable in the future? + if (pkginfo.get('installer_type') == "AdobeCCPInstaller" and + not options.uninstalleritem) and options.print_warnings: + print >> sys.stderr, ( + "WARNING: This item appears to be an Adobe Creative " + "Cloud product install.\n" + "No uninstaller package was specified so product " + "removal will not be possible.") + pkginfo['uninstallable'] = False + if 'uninstall_method' in pkginfo: + del pkginfo['uninstall_method'] + + if options.uninstalleritem: + uninstallerpath = options.uninstalleritem + 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] + pkginfo['uninstaller_item_location'] = location + itemsize = int(os.path.getsize(uninstallerpath)) + itemhash = munkihash.getsha256hash(uninstallerpath) + pkginfo['uninstaller_item_size'] = int(itemsize/1024) + pkginfo['uninstaller_item_hash'] = itemhash + else: + raise PkgInfoGenerationError( + "No uninstaller item at %s" % uninstallerpath) + + # if we have receipts, assume we can uninstall using them + if pkginfo.get('receipts', None): + pkginfo['uninstallable'] = True + pkginfo['uninstall_method'] = "removepackages" + else: + if options.nopkg: + pkginfo['installer_type'] = "nopkg" + + if options.catalog: + pkginfo['catalogs'] = options.catalog + else: + pkginfo['catalogs'] = ['testing'] + if options.description: + pkginfo['description'] = read_file_or_string(options.description) + if options.displayname: + pkginfo['display_name'] = options.displayname + if options.name: + pkginfo['name'] = options.name + if options.pkgvers: + pkginfo['version'] = options.pkgvers + if options.category: + pkginfo['category'] = options.category + if options.developer: + pkginfo['developer'] = options.developer + if options.icon: + pkginfo['icon_name'] = options.icon + + default_minosversion = "10.4.0" + maxfileversion = "0.0.0.0.0" + if pkginfo: + pkginfo['autoremove'] = False + if not 'version' in pkginfo: + if maxfileversion != "0.0.0.0.0": + pkginfo['version'] = maxfileversion + else: + pkginfo['version'] = "1.0.0.0.0 (Please edit me!)" + + if options.file: + for fitem in options.file: + # no trailing slashes, please. + fitem = fitem.rstrip('/') + if fitem.startswith('/Library/Receipts'): + # no receipts, please! + if options.print_warnings: + print >> sys.stderr, ( + "Item %s appears to be a receipt. Skipping." % fitem) + continue + if os.path.exists(fitem): + iteminfodict = getiteminfo(fitem) + if 'CFBundleShortVersionString' in iteminfodict: + thisitemversion = \ + iteminfodict['CFBundleShortVersionString'] + if (pkgutils.MunkiLooseVersion(thisitemversion) > + pkgutils.MunkiLooseVersion(maxfileversion)): + maxfileversion = thisitemversion + installs.append(iteminfodict) + elif options.print_warnings: + print >> sys.stderr, ( + "Item %s doesn't exist. Skipping." % fitem) + + if installs: + pkginfo['installs'] = installs + + # determine minimum_os_version from identified apps in the installs array + if 'installs' in pkginfo: + # build a list of minosversions using a list comprehension + item_minosversions = [ + pkgutils.MunkiLooseVersion(item['minosversion']) + for item in pkginfo['installs'] + if 'minosversion' in item] + # add the default in case it's an empty list + item_minosversions.append( + pkgutils.MunkiLooseVersion(default_minosversion)) + if 'minimum_os_version' in pkginfo: + # handle case where value may have been set (e.g. flat package) + item_minosversions.append(pkgutils.MunkiLooseVersion( + pkginfo['minimum_os_version'])) + # get the maximum from the list and covert back to string + pkginfo['minimum_os_version'] = str(max(item_minosversions)) + + if not 'minimum_os_version' in pkginfo: + # ensure a minimum_os_version is set unless using --file option only + pkginfo['minimum_os_version'] = default_minosversion + + if options.file and not installeritem: + # remove minimum_os_version as we don't include it for --file only + pkginfo.pop('minimum_os_version') + + if options.installcheck_script: + scriptstring = readfile(options.installcheck_script) + if scriptstring: + pkginfo['installcheck_script'] = scriptstring + if options.uninstallcheck_script: + scriptstring = readfile(options.uninstallcheck_script) + if scriptstring: + pkginfo['uninstallcheck_script'] = scriptstring + if options.postinstall_script: + scriptstring = readfile(options.postinstall_script) + if scriptstring: + pkginfo['postinstall_script'] = scriptstring + if options.preinstall_script: + scriptstring = readfile(options.preinstall_script) + if scriptstring: + pkginfo['preinstall_script'] = scriptstring + if options.postuninstall_script: + scriptstring = readfile(options.postuninstall_script) + if scriptstring: + pkginfo['postuninstall_script'] = scriptstring + if options.preuninstall_script: + scriptstring = readfile(options.preuninstall_script) + if scriptstring: + pkginfo['preuninstall_script'] = scriptstring + if options.uninstall_script: + scriptstring = readfile(options.uninstall_script) + if scriptstring: + pkginfo['uninstall_script'] = scriptstring + pkginfo['uninstall_method'] = 'uninstall_script' + if options.autoremove: + pkginfo['autoremove'] = True + if options.minimum_munki_version: + pkginfo['minimum_munki_version'] = options.minimum_munki_version + if options.OnDemand: + pkginfo['OnDemand'] = True + if options.unattended_install: + pkginfo['unattended_install'] = True + if options.unattended_uninstall: + pkginfo['unattended_uninstall'] = True + if options.minimum_os_version: + pkginfo['minimum_os_version'] = options.minimum_os_version + if options.maximum_os_version: + pkginfo['maximum_os_version'] = options.maximum_os_version + if options.force_install_after_date: + date_obj = convert_date_string_to_nsdate( + options.force_install_after_date) + if date_obj: + pkginfo['force_install_after_date'] = date_obj + else: + raise PkgInfoGenerationError( + "Invalid date format %s for force_install_after_date" + % options.force_install_after_date) + if options.RestartAction: + valid_actions = ['RequireRestart', 'RequireLogout', 'RecommendRestart'] + if options.RestartAction in valid_actions: + pkginfo['RestartAction'] = options.RestartAction + elif 'restart' in options.RestartAction.lower(): + pkginfo['RestartAction'] = 'RequireRestart' + elif 'logout' in options.RestartAction.lower(): + pkginfo['RestartAction'] = 'RequireLogout' + if options.update_for: + pkginfo['update_for'] = options.update_for + if options.requires: + pkginfo['requires'] = options.requires + if options.blocking_application: + pkginfo['blocking_applications'] = options.blocking_application + if options.uninstall_method: + pkginfo['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: + pkginfo['installer_environment'] = installer_environment_dict + if options.notes: + pkginfo['notes'] = read_file_or_string(options.notes) + if options.apple_update: + # remove minimum_os_version as we don't include it for this option + pkginfo.pop('minimum_os_version') + if options.catalog: + pkginfo['catalogs'] = options.catalog + else: + pkginfo['catalogs'] = ['testing'] + if options.pkgvers: + pkginfo['version'] = options.pkgvers + else: + pkginfo['version'] = "1.0" + pkginfo['name'] = options.apple_update + if options.displayname: + pkginfo['display_name'] = options.displayname + pkginfo['installer_type'] = 'apple_update_metadata' + + # add user/environment metadata + pkginfo['_metadata'] = make_pkginfo_metadata() + + # return the info + return pkginfo + + +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 optparse.OptionValueError( + "option %s: invalid mode: %s" % (opt, mode)) + setattr(parser.values, option.dest, value) + + +def add_option_groups(parser): + '''Adds our (many) option groups to the options parser''' + + # Default override options + default_override_options = optparse.OptionGroup( + parser, '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.') + ) + parser.add_option_group(default_override_options) + + # Script options + script_options = optparse.OptionGroup( + parser, 'Script Options', + 'All scripts are read and embedded into the pkginfo.') + script_options.add_option( + '--installcheck_script', '--installcheck-script', + metavar='SCRIPT_PATH', + help=('Path to an optional installcheck script to be ' + 'run to determine if item should be installed. ' + 'An exit code of 0 indicates installation should occur. ' + 'Takes precedence over installs items and receipts.') + ) + script_options.add_option( + '--uninstallcheck_script', '--uninstallcheck-script', + metavar='SCRIPT_PATH', + help=('Path to an optional uninstallcheck script to be ' + 'run to determine if item should be uninstalled. ' + 'An exit code of 0 indicates uninstallation should occur. ' + 'Takes precedence over installs items and receipts.') + ) + 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.') + ) + parser.add_option_group(script_options) + + # Drag-n-Drop options + dragdrop_options = optparse.OptionGroup( + parser, '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 ' + 'or the item is located in a subdirectory. ' + 'Absolute paths can be provided as well but they ' + 'must point to an item located within 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( + '--destinationitemname', '--destinationitem', + metavar='NAME', + dest='destitemname', + help=('Alternate name for which the item should be copied as. ' + 'Specifying this option also alters the corresponding ' + '"installs" item\'s path with the provided name.') + ) + dragdrop_options.add_option( + '-o', '--owner', + metavar='USER', + dest='user', + help=('Sets the owner of the copied item. ' + '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 copied item. ' + '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 copied item. ' + 'The specified mode must be in symbolic form. ' + 'See the manpage for chmod(1) for more information. ' + 'The mode is applied recursively.') + ) + parser.add_option_group(dragdrop_options) + + # Apple package specific options + apple_options = optparse.OptionGroup(parser, '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.') + ) + parser.add_option_group(apple_options) + + # Adobe package specific options + adobe_options = optparse.OptionGroup(parser, 'Adobe-specific Options') + adobe_options.add_option( + '--uninstallerdmg', '--uninstallerpkg', '--uninstallpkg', '-U', + metavar='UNINSTALLERITEM', dest='uninstalleritem', + help=('If the installer item is a disk image containing an Adobe CS4 ' + 'Deployment Toolkit installation package or Adobe CS3 deployment ' + 'package, UNINSTALLERITEM is a path to a disk image containing ' + 'an AdobeUberUninstaller for this item.\n' + 'If the installer item is a Creative Cloud Packager install ' + 'package, UNINSTALLERITEM is a path to the matching Creative ' + 'Cloud Packager uninstall package.') + ) + parser.add_option_group(adobe_options) + + # Forced/Unattended (install) options + forced_unattended_options = optparse.OptionGroup( + parser, '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.') + ) + parser.add_option_group(forced_unattended_options) + + # 'installs' generation options + # (by itself since no installer_item needs to be specified) + gen_installs_options = optparse.OptionGroup( + parser, '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.') + ) + parser.add_option_group(gen_installs_options) + + # Apple update metadata pkg options + # (by itself since no installer_item needs to be specified) + apple_update_metadata_options = optparse.OptionGroup( + parser, 'Generating Apple update metadata items') + apple_update_metadata_options.add_option( + '--apple_update', '--apple-update', + metavar='PRODUCTKEY', + help=('Specify an Apple update \'productKey\' used to manipulate ' + 'the behavior of a pending Apple software update. ' + 'For example, a \'force_install_after_date\' key could be added ' + 'as opposed to importing the update into the munki repo.') + ) + parser.add_option_group(apple_update_metadata_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(parser, '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( + '--OnDemand', + action='store_true', + help=('Indicates this package should be an OnDemand package ' + '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( + '--category', + metavar='CATEGORY', + help='Category for display in Managed Software Center.' + ) + additional_options.add_option( + '--developer', + metavar='DEVELOPER', + help='Developer name for display in Managed Software Center.' + ) + additional_options.add_option( + '--icon', '--iconname', '--icon-name', '--icon_name', + metavar='ICONNAME', + help='Name of icon file for display in Managed Software Center.' + ) + 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.') + ) + additional_options.add_option( + '--nopkg', + action='store_true', + help=('Indicates this pkginfo should have an \'installer_type\' of ' + '\'nopkg\'. Ignored if a package or dmg argument is supplied.') + ) + # secret option! + additional_options.add_option( + '--print-warnings', + action='store_true', default=True, + help=optparse.SUPPRESS_HELP + ) + parser.add_option_group(additional_options)