def main():
    usage = """

%prog --product-plist path/to/plist [-p path/to/another] [--munkiimport] [options]
%prog --build-product-plist path/to/Adobe/ESD/volume [--munki-update-for] BaseProductPkginfoName

The first form will check and cache updates for the channels listed in the plist
specified by the --product-plist option.

The second form will generate a product plist containing every channel ID available
for the product whose ESD installer volume is mounted at the path.

See %prog --help for more options and the README for more detail."""

    o = optparse.OptionParser(usage=usage)
    o.add_option("-m", "--munkiimport", action="store_true", default=False,
        help="Process downloaded updates with munkiimport using options defined in %s." % os.path.basename(settings_plist))
    o.add_option("-r", "--include-revoked", action="store_true", default=False,
        help="Include updates that have been marked as revoked in Adobe's feed XML.")
    o.add_option("-f", "--force-import", action="store_true", default=False,
        help="Run munkiimport even if it finds an identical pkginfo and installer_item_hash in the repo.")
    o.add_option("-c", "--make-catalogs", action="store_true", default=False,
        help="Automatically run makecatalogs after importing into Munki.")
    o.add_option("-p", "--product-plist", "--plist", action="append",
        help="Path to an Adobe product plist, for example as generated using the --build-product-plist option. \
Can be specified multiple times.")
    o.add_option("-b", "--build-product-plist", action="store",
        help="Given a path to either a mounted Adobe product ESD installer or a .ccp file from a package built with CCP, \
save a product plist containing every Channel ID found for the product.")
    o.add_option("-u", "--munki-update-for", action="store",
        help="To be used with the --build-product-plist option, specifies the base Munki product.")
    o.add_option("-v", "--verbose", action="count", default=0,
        help="Output verbosity. Can be specified either '-v' or '-vv'.")
    o.add_option("--no-colors", action="store_true", default=False,
        help="Disable colored ANSI output.")

    opts, args = o.parse_args()

    # setup logging
    global L
    L = logging.getLogger('com.github.aamporter')
    log_stdout_handler = logging.StreamHandler(stream=sys.stdout)
    log_stdout_handler.setFormatter(ColorFormatter(
        use_color=not opts.no_colors))
    L.addHandler(log_stdout_handler)
    # INFO is level 30, so each verbose option count lowers level by 10
    L.setLevel(INFO - (10 * opts.verbose))

    # arg/opt processing
    if len(sys.argv) == 1:
        o.print_usage()
        sys.exit(0)
    if opts.munki_update_for and not opts.build_product_plist:
        errorExit("--munki-update-for requires the --build-product-plist option!")
    if not opts.build_product_plist and not opts.product_plist:
        errorExit("One of --product-plist or --build-product-plist must be specified!")

    if opts.build_product_plist:
        esd_path = opts.build_product_plist
        if esd_path.endswith('/'):
            esd_path = esd_path[0:-1]
        plist = buildProductPlist(esd_path, opts.munki_update_for)
        if not plist:
            errorExit("Couldn't build payloads from path %s." % esd_path)
        else:
            if opts.munki_update_for:
                output_plist_name = opts.munki_update_for
            else:
                output_plist_name = os.path.basename(esd_path.replace(' ', ''))
            output_plist_name += '.plist'
            output_plist_file = os.path.join(SCRIPT_DIR, output_plist_name)
            try:
                plistlib.writePlist(plist, output_plist_file)
            except:
                errorExit("Error writing plist to %s" % output_plist_file)
            print "Product plist written to %s" % output_plist_file
            sys.exit(0)

    # munki sanity checks
    if opts.munkiimport:
        if not os.path.exists('/usr/local/munki'):
            errorExit("No Munki installation could be found. Get it at http://code.google.com/p/munki")
        sys.path.insert(0, MUNKI_DIR)
        munkiimport_prefs = os.path.expanduser('~/Library/Preferences/com.googlecode.munki.munkiimport.plist')
        if pref('munki_tool') == 'munkiimport':
            if not os.path.exists(munkiimport_prefs):
                errorExit("Your Munki repo seems to not be configured. Run munkiimport --configure first.")
            try:
                import imp
                # munkiimport doesn't end in .py, so we use imp to make it available to the import system
                imp.load_source('munkiimport', os.path.join(MUNKI_DIR, 'munkiimport'))
                import munkiimport
                munkiimport.REPO_PATH = munkiimport.pref('repo_path')
            except ImportError:
                errorExit("There was an error importing munkilib, which is needed for --munkiimport functionality.")
            if not munkiimport.repoAvailable():
                errorExit("The Munki repo cannot be located. This tool is not interactive; first ensure the repo is mounted.")

    # set up the cache path
    local_cache_path = pref('local_cache_path')
    if os.path.exists(local_cache_path) and not os.path.isdir(local_cache_path):
        errorExit("Local cache path %s was specified and exists, but it is not a directory!" %
            local_cache_path)
    elif not os.path.exists(local_cache_path):
        try:
            os.mkdir(local_cache_path)
        except OSError:
            errorExit("Local cache path %s could not be created. Verify permissions." %
                local_cache_path)
        except:
            errorExit("Unknown error creating local cache path %s." % local_cache_path)
    try:
        os.access(local_cache_path, os.W_OK)
    except:
        errorExit("Cannot write to local cache path!" % local_cache_path)

    # load our product plists
    product_plists = []
    for plist_path in opts.product_plist:
        try:
            plist = plistlib.readPlist(plist_path)
        except:
            errorExit("Couldn't read plist at %s!" % plist_path)
        if 'channels' not in plist.keys():
            errorExit("Plist at %s is missing a 'channels' array, which is required." % plist_path)
        else:
            product_plists.append(plist)

    # sanity-check the settings plist for unknown keys
    if os.path.exists(settings_plist):
        try:
            app_options = plistlib.readPlist(settings_plist)
        except:
            errorExit("There was an error loading the settings plist at %s" % settings_plist)
        for k in app_options.keys():
            if k not in supported_settings_keys:
                print "Warning: Unknown setting in %s: %s" % (os.path.basename(settings_plist), k)

    L.log(INFO, "Starting aamporter run..")
    if opts.munkiimport:
        L.log(INFO, "Will import into Munki (--munkiimport option given).")

    L.log(DEBUG, "aamporter preferences:")
    for key in supported_settings_keys:
        L.log(DEBUG, " - {0}: {1}".format(key, pref(key)))

    # pull feed info and populate channels
    L.log(INFO, "Retrieving feed data..")
    feed = getFeedData()
    parsed = parseFeedData(feed)
    channels = getChannelsFromProductPlists(product_plists)
    L.log(INFO, "Processing the following Channel IDs:")
    [ L.log(INFO, "  - %s" % channel) for channel in sorted(channels) ]

    # begin caching run and build updates dictionary with product/version info
    updates = {}
    for channelid in channels.keys():
        L.log(VERBOSE, "Getting updates for Channel ID %s.." % channelid)
        channel_updates = getUpdatesForChannel(channelid, parsed)
        if not channel_updates:
            L.log(DEBUG, "No updates for channel %s" % channelid)
            continue
        channel_updates = addUpdatesXML(channel_updates)

        for update in channel_updates:
            L.log(VERBOSE, "Considering update %s, %s.." % (update.product, update.version))

            if opts.include_revoked is False:
                highest_version = getHighestVersionOfProduct(channel_updates, update.product)
                if update.version != highest_version:
                    L.log(DEBUG, "%s is not the highest version available (%s) for this update. Skipping.." % (
                        update.version, highest_version))
                    continue

                if updateIsRevoked(update.channel, update.product, update.version, parsed):
                    L.log(DEBUG, "Update is revoked. Skipping update.")
                    continue

                file_element = update.xml.find('InstallFiles/File')
                if file_element is None:
                    L.log(DEBUG, "No File XML element found. Skipping update.")
                else:
                    filename = file_element.find('Name').text
                    bytes = file_element.find('Size').text
                    description = update.xml.find('Description/en_US').text
                    display_name = update.xml.find('DisplayName/en_US').text

                    if not update.product in updates.keys():
                        updates[update.product] = {}
                    if not update.version in updates[update.product].keys():
                        updates[update.product][update.version] = {}
                        updates[update.product][update.version]['channel_ids'] = []
                        updates[update.product][update.version]['update_for'] = []
                    updates[update.product][update.version]['channel_ids'].append(update.channel)
                    for opt in ['munki_repo_destination_path', 'munki_update_for']:
                        if opt in channels[update.channel].keys():
                            updates[update.product][update.version][opt] = channels[update.channel][opt]
                    updates[update.product][update.version]['description'] = description
                    updates[update.product][update.version]['display_name'] = display_name
                    dmg_url = urljoin(getURL('updates'), UPDATE_PATH_PREFIX) + \
                            '/%s/%s/%s' % (update.product, update.version, filename)
                    output_filename = os.path.join(local_cache_path, "%s-%s.dmg" % (
                            update.product, update.version))
                    updates[update.product][update.version]['local_path'] = output_filename
                    need_to_dl = True
                    if os.path.exists(output_filename):
                        we_have_bytes = os.stat(output_filename).st_size
                        if we_have_bytes == int(bytes):
                            L.log(INFO, "Skipping download of %s %s, it is already cached." 
                                % (update.product, update.version))
                            need_to_dl = False
                        else:
                            L.log(VERBOSE, "Incomplete download (%s bytes on disk, should be %s), re-starting." % (
                                we_have_bytes, bytes))
                    if need_to_dl:
                        L.log(INFO, "Downloading update at %s" % dmg_url)
                        urllib.urlretrieve(dmg_url, output_filename)
    L.log(INFO, "Done caching updates.")

    # begin munkiimport run
    if opts.munkiimport:
        L.log(INFO, "Beginning Munki imports..")
        for (update_name, update_meta) in updates.items():
            for (version_name, version_meta) in update_meta.items():
                need_to_import = True
                item_name = "%s%s" % (update_name.replace('-', '_'),
                    pref('munki_pkginfo_name_suffix'))
                # Do 'exists in repo' checks if we're not forcing imports
                if opts.force_import is False and pref("munki_tool") == "munkiimport":
                    pkginfo = munkiimport.makePkgInfo(['--name',
                                            item_name,
                                            version_meta['local_path']],
                                            False)
                    # Cribbed from munkiimport
                    L.log(VERBOSE, "Looking for a matching pkginfo for %s %s.." % (
                        item_name, version_name))
                    matchingpkginfo = munkiimport.findMatchingPkginfo(pkginfo)
                    if matchingpkginfo:
                        L.log(VERBOSE, "Got a matching pkginfo.")
                        if ('installer_item_hash' in matchingpkginfo and
                            matchingpkginfo['installer_item_hash'] ==
                            pkginfo.get('installer_item_hash')):
                            need_to_import = False
                            L.log(INFO,
                                ("We have an exact match for %s %s in the repo. Skipping.." % (
                                    item_name, version_name)))
                    else:
                        need_to_import = True

                if need_to_import:
                    munkiimport_opts = pref('munkiimport_options')[:]
                    if pref("munki_tool") == 'munkiimport':
                        if 'munki_repo_destination_path' in version_meta.keys():
                            subdir = version_meta['munki_repo_destination_path']
                        else:
                            subdir = pref('munki_repo_destination_path')
                        munkiimport_opts.append('--subdirectory')
                        munkiimport_opts.append(subdir)
                    if not version_meta['munki_update_for']:
                        L.log(WARNING,
                            "Warning: {0} does not have an 'update_for' key "
                            "specified in the product plist!".format(item_name))
                        update_catalogs = []
                    else:
                        # handle case of munki_update_for being either a list or a string
                        flatten = lambda *n: (e for a in n
                            for e in (flatten(*a) if isinstance(a, (tuple, list)) else (a,)))
                        update_catalogs = list(flatten(version_meta['munki_update_for']))
                        for base_product in update_catalogs:
                            munkiimport_opts.append('--update_for')
                            munkiimport_opts.append(base_product)
                    munkiimport_opts.append('--name')
                    munkiimport_opts.append(item_name)
                    munkiimport_opts.append('--displayname')
                    munkiimport_opts.append(version_meta['display_name'])
                    munkiimport_opts.append('--description')
                    munkiimport_opts.append(version_meta['description'])
                    if '--catalog' not in munkiimport_opts:
                        munkiimport_opts.append('--catalog')
                        munkiimport_opts.append('testing')
                    if pref('munki_tool') == 'munkiimport':
                        import_cmd = ['/usr/local/munki/munkiimport', '--nointeractive']
                    elif pref('munki_tool') == 'makepkginfo':
                        import_cmd = ['/usr/local/munki/makepkginfo']
                    else:
                        # TODO: validate this pref earlier
                        L.log(ERROR, "Not sure what tool you wanted to use; munki_tool should be 'munkiimport' " + \
                        "or 'makepkginfo' but we got '%s'.  Skipping import." % (pref('munki_tool')))
                        break
                    # Load our app munkiimport options overrides last
                    import_cmd += munkiimport_opts
                    import_cmd.append(version_meta['local_path'])

                    L.log(INFO, "Importing {0} {1} into Munki. Update for: {2}".format(
                        item_name, version_name, ', '.join(update_catalogs)))
                    L.log(VERBOSE, "Calling %s on %s version %s, file %s." % (
                        pref('munki_tool'),
                        update_name,
                        version_name,
                        version_meta['local_path']))
                    munkiprocess = subprocess.Popen(import_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    # wait for the process to terminate
                    stdout, stderr = munkiprocess.communicate()
                    import_retcode = munkiprocess.returncode
                    if import_retcode:
                        L.log(ERROR, "munkiimport returned an error. Skipping update..")
                    else:
                        if pref('munki_tool') == 'makepkginfo':
                            plist_path = os.path.splitext(version_meta['local_path'])[0] + ".plist"
                            with open(plist_path, "w") as plist:
                                plist.write(stdout)
                                L.log(INFO, "pkginfo written to %s" % plist_path)


        L.log(INFO, "Done Munki imports.")
        if opts.make_catalogs:
            munkiimport.makeCatalogs()
Beispiel #2
0
def main():
    usage = """

%prog [options] path/to/plist [path/to/more/plists..]
%prog --build-product-plist [path/to/CCP/pkg/file.ccp] [--munki-update-for BaseProductPkginfoName]

The first form will check and cache updates for the channels listed in the product plists
given as arguments.

The second form will generate a product plist containing all channel IDs contained in the
installer metadata. Accepts either a path to a .cpp file (from Creative Cloud Packager) or
a mounted ESD volume path for CS6-and-earlier installers.

See %prog --help for more options and the README for more detail."""

    o = optparse.OptionParser(usage=usage)
    o.add_option("-l", "--platform", type='choice', choices=['mac', 'win'], default='mac',
        help="Download Adobe updates for Mac or Windows. Available options are 'mac' or 'win', defaults to 'mac'.")
    o.add_option("-m", "--munkiimport", action="store_true", default=False,
        help="Process downloaded updates with munkiimport using options defined in %s." % os.path.basename(settings_plist))
    o.add_option("-r", "--include-revoked", action="store_true", default=False,
        help="Include updates that have been marked as revoked in Adobe's feed XML.")
    o.add_option("--skip-cc", action="store_true", default=False,
        help=("Skip updates for Creative Cloud updates. Useful for certain updates for "
              "CS-era applications that incorporate CC subscription updates."))
    o.add_option("-f", "--force-import", action="store_true", default=False,
        help="Run munkiimport even if it finds an identical pkginfo and installer_item_hash in the repo.")
    o.add_option("-c", "--make-catalogs", action="store_true", default=False,
        help="Automatically run makecatalogs after importing into Munki.")
    o.add_option("-p", "--product-plist", "--plist", action="append", default=[],
        help="Deprecated option for specifying product plists, kept for compatibility. Instead, pass plist paths \
as arguments.")
    o.add_option("-b", "--build-product-plist", action="store",
        help="Given a path to either a mounted Adobe product ESD installer or a .ccp file from a package built with CCP, \
save a product plist containing every Channel ID found for the product. Plist is saved to the current working directory.")
    o.add_option("-u", "--munki-update-for", action="store",
        help="To be used with the --build-product-plist option, specifies the base Munki product.")
    o.add_option("-v", "--verbose", action="count", default=0,
        help="Output verbosity. Can be specified either '-v' or '-vv'.")
    o.add_option("--no-colors", action="store_true", default=False,
        help="Disable colored ANSI output.")
    o.add_option("--no-progressbar", action="store_true", default=False,
        help="Disable the progress indicator.")

    opts, args = o.parse_args()

    # setup logging
    global L
    L = logging.getLogger('com.github.aamporter')
    log_stdout_handler = logging.StreamHandler(stream=sys.stdout)
    log_stdout_handler.setFormatter(ColorFormatter(
        use_color=not opts.no_colors))
    L.addHandler(log_stdout_handler)
    # INFO is level 30, so each verbose option count lowers level by 10
    L.setLevel(INFO - (10 * opts.verbose))

    # arg/opt processing
    if len(sys.argv) == 1:
        o.print_usage()
        sys.exit(0)

    # any args we just pass through to the "legacy" --product-plist/--plist options
    if args:
        opts.product_plist.extend(args)
    if opts.munki_update_for and not opts.build_product_plist:
        errorExit("--munki-update-for requires the --build-product-plist option!")
    if not opts.build_product_plist and not opts.product_plist:
        errorExit("One of --product-plist or --build-product-plist must be specified!")
    if opts.platform == 'win' and opts.munkiimport:
        errorExit("Cannot use the --munkiimport option with --platform win option!")

    if opts.build_product_plist:
        esd_path = opts.build_product_plist
        if esd_path.endswith('/'):
            esd_path = esd_path[0:-1]
        plist = buildProductPlist(esd_path, opts.munki_update_for)
        if not plist:
            errorExit("Couldn't build payloads from path %s." % esd_path)
        else:
            if opts.munki_update_for:
                output_plist_name = opts.munki_update_for
            else:
                output_plist_name = os.path.basename(esd_path.replace(' ', ''))
            output_plist_name += '.plist'
            output_plist_file = os.path.join(os.getcwd(), output_plist_name)
            if os.path.exists(output_plist_file):
                errorExit("A file already exists at %s, not going to overwrite." %
                    output_plist_file)
            try:
                plistlib.writePlist(plist, output_plist_file)
            except:
                errorExit("Error writing plist to %s" % output_plist_file)
            print "Product plist written to %s" % output_plist_file
            sys.exit(0)

    # munki sanity checks
    if opts.munkiimport:
        if not os.path.exists('/usr/local/munki'):
            errorExit("No Munki installation could be found. Get it at http://code.google.com/p/munki")
        sys.path.insert(0, MUNKI_DIR)
        munkiimport_prefs = os.path.expanduser('~/Library/Preferences/com.googlecode.munki.munkiimport.plist')
        if pref('munki_tool') == 'munkiimport':
            if not os.path.exists(munkiimport_prefs):
                errorExit("Your Munki repo seems to not be configured. Run munkiimport --configure first.")
            try:
                import imp
                # munkiimport doesn't end in .py, so we use imp to make it available to the import system
                imp.load_source('munkiimport', os.path.join(MUNKI_DIR, 'munkiimport'))
                import munkiimport
                munkiimport.REPO_PATH = munkiimport.pref('repo_path')
            except ImportError:
                errorExit("There was an error importing munkilib, which is needed for --munkiimport functionality.")

            # rewrite some of munkiimport's function names since they were changed to
            # snake case around 2.6.1:
            # https://github.com/munki/munki/commit/e3948104e869a6a5eb6b440559f4c57144922e71
            try:
                munkiimport.repoAvailable()
            except AttributeError:
                munkiimport.repoAvailable = munkiimport.repo_available
                munkiimport.makePkgInfo = munkiimport.make_pkginfo
                munkiimport.findMatchingPkginfo = munkiimport.find_matching_pkginfo
                munkiimport.makeCatalogs = munkiimport.make_catalogs
            if not munkiimport.repoAvailable():
                errorExit("The Munki repo cannot be located. This tool is not interactive; first ensure the repo is mounted.")

    # set up the cache path
    local_cache_path = pref('local_cache_path')
    if os.path.exists(local_cache_path) and not os.path.isdir(local_cache_path):
        errorExit("Local cache path %s was specified and exists, but it is not a directory!" %
            local_cache_path)
    elif not os.path.exists(local_cache_path):
        try:
            os.mkdir(local_cache_path)
        except OSError:
            errorExit("Local cache path %s could not be created. Verify permissions." %
                local_cache_path)
        except:
            errorExit("Unknown error creating local cache path %s." % local_cache_path)
    try:
        os.access(local_cache_path, os.W_OK)
    except:
        errorExit("Cannot write to local cache path!" % local_cache_path)

    # load our product plists
    product_plists = []
    for plist_path in opts.product_plist:
        try:
            plist = plistlib.readPlist(plist_path)
        except:
            errorExit("Couldn't read plist at %s!" % plist_path)
        if 'channels' not in plist.keys():
            errorExit("Plist at %s is missing a 'channels' array, which is required." % plist_path)
        else:
            product_plists.append(plist)

    # sanity-check the settings plist for unknown keys
    if os.path.exists(settings_plist):
        try:
            app_options = plistlib.readPlist(settings_plist)
        except:
            errorExit("There was an error loading the settings plist at %s" % settings_plist)
        for k in app_options.keys():
            if k not in supported_settings_keys:
                print "Warning: Unknown setting in %s: %s" % (os.path.basename(settings_plist), k)

    L.log(INFO, "Starting aamporter run..")
    if opts.munkiimport:
        L.log(INFO, "Will import into Munki (--munkiimport option given).")

    L.log(DEBUG, "aamporter preferences:")
    for key in supported_settings_keys:
        L.log(DEBUG, " - {0}: {1}".format(key, pref(key)))

    if (sys.version_info.minor, sys.version_info.micro) == (7, 10):
        global NONSSL_ADOBE_URL
        NONSSL_ADOBE_URL = True
        L.log(VERBOSE, ("Python 2.7.10 detected, using HTTP feed URLs to work "
                        "around SSL issues."))

    # pull feed info and populate channels
    L.log(INFO, "Retrieving feed data..")
    feed = getFeedData(opts.platform)
    parsed = parseFeedData(feed)
    channels = getChannelsFromProductPlists(product_plists)
    L.log(INFO, "Processing the following Channel IDs:")
    [ L.log(INFO, "  - %s" % channel) for channel in sorted(channels) ]

    # begin caching run and build updates dictionary with product/version info
    updates = {}
    for channelid in channels.keys():
        L.log(VERBOSE, "Getting updates for Channel ID %s.." % channelid)
        channel_updates = getUpdatesForChannel(channelid, parsed)
        if not channel_updates:
            L.log(DEBUG, "No updates for channel %s" % channelid)
            continue
        channel_updates = addUpdatesXML(channel_updates, opts.platform, skipTargetLicensingCC=opts.skip_cc)

        for update in channel_updates:
            L.log(VERBOSE, "Considering update %s, %s.." % (update.product, update.version))

            if opts.include_revoked is False:
                highest_version = getHighestVersionOfProduct(channel_updates, update.product)
                if update.version != highest_version:
                    L.log(DEBUG, "%s is not the highest version available (%s) for this update. Skipping.." % (
                        update.version, highest_version))
                    continue

                if updateIsRevoked(update.channel, update.product, update.version, parsed):
                    L.log(DEBUG, "Update is revoked. Skipping update.")
                    continue

                file_element = update.xml.find('InstallFiles/File')
                if file_element is None:
                    L.log(DEBUG, "No File XML element found. Skipping update.")
                else:
                    filename = file_element.find('Name').text
                    update_bytes = file_element.find('Size').text
                    description = update.xml.find('Description/en_US').text
                    display_name = update.xml.find('DisplayName/en_US').text

                    if not update.product in updates.keys():
                        updates[update.product] = {}
                    if not update.version in updates[update.product].keys():
                        updates[update.product][update.version] = {}
                        updates[update.product][update.version]['channel_ids'] = []
                        updates[update.product][update.version]['update_for'] = []
                    updates[update.product][update.version]['channel_ids'].append(update.channel)
                    for opt in ['munki_repo_destination_path',
                                'munki_update_for',
                                'makepkginfo_options']:
                        if opt in channels[update.channel].keys():
                            updates[update.product][update.version][opt] = channels[update.channel][opt]
                    updates[update.product][update.version]['description'] = description
                    updates[update.product][update.version]['display_name'] = display_name
                    dmg_url = urljoin(getURL('updates'), UPDATE_PATH_PREFIX + opts.platform) + \
                            '/%s/%s/%s' % (update.product, update.version, filename)
                    output_filename = os.path.join(local_cache_path, "%s-%s.%s" % (
                            update.product, update.version, 'dmg' if opts.platform == 'mac' else 'zip'))
                    updates[update.product][update.version]['local_path'] = output_filename
                    need_to_dl = True
                    if os.path.exists(output_filename):
                        we_have_bytes = os.stat(output_filename).st_size
                        if we_have_bytes == int(update_bytes):
                            L.log(INFO, "Skipping download of %s %s, it is already cached."
                                % (update.product, update.version))
                            need_to_dl = False
                        else:
                            L.log(VERBOSE, "Incomplete download (%s bytes on disk, should be %s), re-starting." % (
                                we_have_bytes, update_bytes))
                    if need_to_dl:
                        L.log(INFO, "Downloading %s %s (%s bytes) to %s" % (update.product, update.version, update_bytes, output_filename))
                        if opts.no_progressbar:
                            urllib.urlretrieve(dmg_url, output_filename)
                        else:
                            urllib.urlretrieve(dmg_url, output_filename, reporthook)

    L.log(INFO, "Done caching updates.")

    # begin munkiimport run
    if opts.munkiimport:
        L.log(INFO, "Beginning Munki imports..")
        for (update_name, update_meta) in updates.items():
            for (version_name, version_meta) in update_meta.items():
                need_to_import = True
                item_name = "%s%s" % (update_name.replace('-', '_'),
                    pref('munki_pkginfo_name_suffix'))
                # Do 'exists in repo' checks if we're not forcing imports
                if opts.force_import is False and pref("munki_tool") == "munkiimport":
                    pkginfo = munkiimport.makePkgInfo(['--name',
                                            item_name,
                                            version_meta['local_path']],
                                            False)
                    # Cribbed from munkiimport
                    L.log(VERBOSE, "Looking for a matching pkginfo for %s %s.." % (
                        item_name, version_name))
                    matchingpkginfo = munkiimport.findMatchingPkginfo(pkginfo)
                    if matchingpkginfo:
                        L.log(VERBOSE, "Got a matching pkginfo.")
                        if ('installer_item_hash' in matchingpkginfo and
                            matchingpkginfo['installer_item_hash'] ==
                            pkginfo.get('installer_item_hash')):
                            need_to_import = False
                            L.log(INFO,
                                ("We have an exact match for %s %s in the repo. Skipping.." % (
                                    item_name, version_name)))
                    else:
                        need_to_import = True

                if need_to_import:
                    munkiimport_opts = pref('munkiimport_options')[:]
                    if pref("munki_tool") == 'munkiimport':
                        if 'munki_repo_destination_path' in version_meta.keys():
                            subdir = version_meta['munki_repo_destination_path']
                        else:
                            subdir = pref('munki_repo_destination_path')
                        munkiimport_opts.append('--subdirectory')
                        munkiimport_opts.append(subdir)
                    if not version_meta['munki_update_for']:
                        L.log(WARNING,
                            "Warning: {0} does not have an 'update_for' key "
                            "specified in the product plist!".format(item_name))
                        update_catalogs = []
                    else:
                        # handle case of munki_update_for being either a list or a string
                        flatten = lambda *n: (e for a in n
                            for e in (flatten(*a) if isinstance(a, (tuple, list)) else (a,)))
                        update_catalogs = list(flatten(version_meta['munki_update_for']))
                        for base_product in update_catalogs:
                            munkiimport_opts.append('--update_for')
                            munkiimport_opts.append(base_product)
                    munkiimport_opts.extend(['--name', item_name,
                                             '--displayname', version_meta['display_name'],
                                             '--description', version_meta['description']])

                    if 'makepkginfo_options' in version_meta:
                        L.log(VERBOSE,
                            "Appending makepkginfo options: %s" %
                            " ".join(version_meta['makepkginfo_options']))
                        munkiimport_opts += version_meta['makepkginfo_options']

                    if pref('munki_tool') == 'munkiimport':
                        import_cmd = ['/usr/local/munki/munkiimport', '--nointeractive']
                    elif pref('munki_tool') == 'makepkginfo':
                        import_cmd = ['/usr/local/munki/makepkginfo']
                    else:
                        # TODO: validate this pref earlier
                        L.log(ERROR, "Not sure what tool you wanted to use; munki_tool should be 'munkiimport' " + \
                        "or 'makepkginfo' but we got '%s'.  Skipping import." % (pref('munki_tool')))
                        break
                    # Load our app munkiimport options overrides last
                    import_cmd += munkiimport_opts
                    import_cmd.append(version_meta['local_path'])

                    L.log(INFO, "Importing {0} {1} into Munki. Update for: {2}".format(
                        item_name, version_name, ', '.join(update_catalogs)))
                    L.log(VERBOSE, "Calling %s on %s version %s, file %s." % (
                        pref('munki_tool'),
                        update_name,
                        version_name,
                        version_meta['local_path']))
                    munkiprocess = subprocess.Popen(import_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    # wait for the process to terminate
                    stdout, stderr = munkiprocess.communicate()
                    import_retcode = munkiprocess.returncode
                    if import_retcode:
                        L.log(ERROR, "munkiimport returned an error. Skipping update..")
                    else:
                        if pref('munki_tool') == 'makepkginfo':
                            plist_path = os.path.splitext(version_meta['local_path'])[0] + ".plist"
                            with open(plist_path, "w") as plist:
                                plist.write(stdout)
                                L.log(INFO, "pkginfo written to %s" % plist_path)


        L.log(INFO, "Done Munki imports.")
        if opts.make_catalogs:
            munkiimport.makeCatalogs()
Beispiel #3
0
def main():
    usage = """

%prog --product-plist path/to/plist [-p path/to/another] [--munkiimport] [options]
%prog --build-product-plist path/to/Adobe/ESD/volume [--munki-update-for] BaseProductPkginfoName

The first form will check and cache updates for the channels listed in the plist
specified by the --product-plist option.

The second form will generate a product plist containing every channel ID available
for the product whose ESD installer volume is mounted at the path.

See %prog --help for more options and the README for more detail."""

    o = optparse.OptionParser(usage=usage)
    o.add_option(
        "-m",
        "--munkiimport",
        action="store_true",
        default=False,
        help="Process downloaded updates with munkiimport using options defined in %s."
        % os.path.basename(settings_plist),
    )
    o.add_option(
        "-r",
        "--include-revoked",
        action="store_true",
        default=False,
        help="Include updates that have been marked as revoked in Adobe's feed XML.",
    )
    o.add_option(
        "-f",
        "--force-import",
        action="store_true",
        default=False,
        help="Run munkiimport even if it finds an identical pkginfo and installer_item_hash in the repo.",
    )
    o.add_option(
        "-c",
        "--make-catalogs",
        action="store_true",
        default=False,
        help="Automatically run makecatalogs after importing into Munki.",
    )
    o.add_option(
        "-p",
        "--product-plist",
        "--plist",
        action="append",
        help="Path to an Adobe product plist, for example as generated using the --build-product-plist option. \
Can be specified multiple times.",
    )
    o.add_option(
        "-b",
        "--build-product-plist",
        action="store",
        help="Given a path to a mounted Adobe product ESD installer, save a containing every Channel ID found for the product.",
    )
    o.add_option(
        "-u",
        "--munki-update-for",
        action="store",
        help="To be used with the --build-product-plist option, specifies the base Munki product.",
    )

    opts, args = o.parse_args()

    if len(sys.argv) == 1:
        o.print_usage()
        sys.exit(0)
    if opts.munki_update_for and not opts.build_product_plist:
        errorExit("--munki-update-for requires the --build-product-plist option!")
    if not opts.build_product_plist and not opts.product_plist:
        errorExit("One of --product-plist or --build-product-plist must be specified!")

    if opts.build_product_plist:
        esd_path = opts.build_product_plist
        if esd_path.endswith("/"):
            esd_path = esd_path[0:-1]
        plist = buildProductPlist(esd_path, opts.munki_update_for)
        if not plist:
            errorExit("Couldn't build payloads from path %s." % esd_path)
        else:
            if opts.munki_update_for:
                output_plist_name = opts.munki_update_for
            else:
                output_plist_name = os.path.basename(esd_path.replace(" ", ""))
            output_plist_name += ".plist"
            output_plist_file = os.path.join(SCRIPT_DIR, output_plist_name)
            try:
                plistlib.writePlist(plist, output_plist_file)
            except:
                errorExit("Error writing plist to %s" % output_plist_file)
            print "Product plist written to %s" % output_plist_file
            sys.exit(0)

    # munki sanity checks
    if opts.munkiimport:
        if not os.path.exists("/usr/local/munki"):
            errorExit("No Munki installation could be found. Get it at http://code.google.com/p/munki")
        sys.path.insert(0, MUNKI_DIR)
        munkiimport_prefs = os.path.expanduser("~/Library/Preferences/com.googlecode.munki.munkiimport.plist")
        if pref("munki_tool") == "munkiimport":
            if not os.path.exists(munkiimport_prefs):
                errorExit("Your Munki repo seems to not be configured. Run munkiimport --configure first.")
            try:
                import imp

                # munkiimport doesn't end in .py, so we use imp to make it available to the import system
                imp.load_source("munkiimport", os.path.join(MUNKI_DIR, "munkiimport"))
                import munkiimport
            except ImportError:
                errorExit("There was an error importing munkilib, which is needed for --munkiimport functionality.")
            if not munkiimport.repoAvailable():
                errorExit(
                    "The Munki repo cannot be located. This tool is not interactive; first ensure the repo is mounted."
                )

    # set up the cache path
    local_cache_path = pref("local_cache_path")
    if os.path.exists(local_cache_path) and not os.path.isdir(local_cache_path):
        errorExit("Local cache path %s was specified and exists, but it is not a directory!" % local_cache_path)
    elif not os.path.exists(local_cache_path):
        try:
            os.mkdir(local_cache_path)
        except OSError:
            errorExit("Local cache path %s could not be created. Verify permissions." % local_cache_path)
        except:
            errorExit("Unknown error creating local cache path %s." % local_cache_path)
    try:
        os.access(local_cache_path, os.W_OK)
    except:
        errorExit("Cannot write to local cache path!" % local_cache_path)

    # load our product plists
    product_plists = []
    for plist_path in opts.product_plist:
        try:
            plist = plistlib.readPlist(plist_path)
        except:
            errorExit("Couldn't read plist at %s!" % plist_path)
        if "channels" not in plist.keys():
            errorExit("Plist at %s is missing a 'channels' array, which is required." % plist_path)
        else:
            product_plists.append(plist)

    # sanity-check the settings plist for unknown keys
    if os.path.exists(settings_plist):
        try:
            app_options = plistlib.readPlist(settings_plist)
        except:
            errorExit("There was an error loading the settings plist at %s" % settings_plist)
        for k in app_options.keys():
            if k not in supported_settings_keys:
                print "Warning: Unknown setting in %s: %s" % (os.path.basename(settings_plist), k)

    # pull feed info and populate channels
    feed = getFeedData()
    parsed = parseFeedData(feed)
    channels = getChannelsFromProductPlists(product_plists)

    # begin caching run and build updates dictionary with product/version info
    updates = {}
    for channelid in channels.keys():
        print "Channel %s" % channelid
        channel_updates = getUpdatesForChannel(channelid, parsed)
        if not channel_updates:
            print "No updates for channel %s" % channelid
            continue

        for update in channel_updates:
            print "Update %s, %s..." % (update.product, update.version)

            if opts.include_revoked is False and updateIsRevoked(
                update.channel, update.product, update.version, parsed
            ):
                print "Update is revoked. Skipping update."
                continue
            details_url = urljoin(getURL("updates"), UPDATE_PATH_PREFIX) + "/%s/%s/%s.xml" % (
                update.product,
                update.version,
                update.version,
            )
            try:
                channel_xml = urllib.urlopen(details_url)
            except:
                print "Couldn't read details XML at %s" % details_url
                break

            try:
                details_xml = ET.fromstring(channel_xml.read())
            except ET.ParseError:
                print "Couldn't parse XML."
                break

            if details_xml is not None:
                licensing_type_elem = details_xml.find("TargetLicensingType")
                if licensing_type_elem is not None:
                    licensing_type_elem = licensing_type_elem.text
                    # TargetLicensingType seems to be 1 for CC updates, 2 for "regular" updates
                    if licensing_type_elem == "1":
                        print "TargetLicensingType of %s found. This seems to be Creative Cloud updates. Skipping update." % licensing_type_elem
                        break

                file_element = details_xml.find("InstallFiles/File")
                if file_element is None:
                    print "No File XML element found. Skipping update."
                else:
                    filename = file_element.find("Name").text
                    bytes = file_element.find("Size").text
                    description = details_xml.find("Description/en_US").text
                    display_name = details_xml.find("DisplayName/en_US").text

                    if not update.product in updates.keys():
                        updates[update.product] = {}
                    if not update.version in updates[update.product].keys():
                        updates[update.product][update.version] = {}
                        updates[update.product][update.version]["channel_ids"] = []
                        updates[update.product][update.version]["update_for"] = []
                    updates[update.product][update.version]["channel_ids"].append(update.channel)
                    for opt in ["munki_repo_destination_path", "munki_update_for"]:
                        if opt in channels[update.channel].keys():
                            updates[update.product][update.version][opt] = channels[update.channel][opt]
                    updates[update.product][update.version]["description"] = description
                    updates[update.product][update.version]["display_name"] = display_name
                    dmg_url = urljoin(getURL("updates"), UPDATE_PATH_PREFIX) + "/%s/%s/%s" % (
                        update.product,
                        update.version,
                        filename,
                    )
                    output_filename = os.path.join(local_cache_path, "%s-%s.dmg" % (update.product, update.version))
                    updates[update.product][update.version]["local_path"] = output_filename
                    need_to_dl = True
                    if os.path.exists(output_filename):
                        we_have_bytes = os.stat(output_filename).st_size
                        if we_have_bytes == int(bytes):
                            print "Skipping download of %s, we already have it." % update.product
                            need_to_dl = False
                        else:
                            print "Incomplete download (%s bytes on disk, should be %s), re-starting." % (
                                we_have_bytes,
                                bytes,
                            )
                    if need_to_dl:
                        print "Downloading update at %s" % dmg_url
                        urllib.urlretrieve(dmg_url, output_filename)
    print "Done caching updates."

    # begin munkiimport run
    if opts.munkiimport:
        for (update_name, update_meta) in updates.items():
            for (version_name, version_meta) in update_meta.items():
                need_to_import = True
                item_name = "%s%s" % (update_name.replace("-", "_"), pref("munki_pkginfo_name_suffix"))
                # Do 'exists in repo' checks if we're not forcing imports
                if opts.force_import is False and pref("munki_tool") == "munkiimport":
                    pkginfo = munkiimport.makePkgInfo(["--name", item_name, version_meta["local_path"]], False)
                    # Cribbed from munkiimport
                    print "Looking for a matching pkginfo for %s %s.." % (item_name, version_name)
                    matchingpkginfo = munkiimport.findMatchingPkginfo(pkginfo)
                    if matchingpkginfo:
                        print "Got a matching pkginfo."
                        if "installer_item_hash" in matchingpkginfo and matchingpkginfo[
                            "installer_item_hash"
                        ] == pkginfo.get("installer_item_hash"):
                            need_to_import = False
                            print "We already have an exact match in the repo. Skipping import."
                    else:
                        need_to_import = True

                if need_to_import:
                    print "Importing %s into munki." % item_name
                    munkiimport_opts = pref("munkiimport_options")[:]
                    if pref("munki_tool") == "munkiimport":
                        if "munki_repo_destination_path" in version_meta.keys():
                            subdir = version_meta["munki_repo_destination_path"]
                        else:
                            subdir = pref("munki_repo_destination_path")
                        munkiimport_opts.append("--subdirectory")
                        munkiimport_opts.append(subdir)
                    if not "munki_update_for" in version_meta.keys():
                        print "Warning: %s does not have an update_for key specified!"
                    else:
                        # handle case of munki_update_for being either a list or a string
                        flatten = lambda *n: (
                            e for a in n for e in (flatten(*a) if isinstance(a, (tuple, list)) else (a,))
                        )
                        update_catalogs = list(flatten(version_meta["munki_update_for"]))
                        print "Applicable base products for Munki: %s" % ", ".join(update_catalogs)
                        for base_product in update_catalogs:
                            munkiimport_opts.append("--update_for")
                            munkiimport_opts.append(base_product)
                    munkiimport_opts.append("--name")
                    munkiimport_opts.append(item_name)
                    munkiimport_opts.append("--displayname")
                    munkiimport_opts.append(version_meta["display_name"])
                    munkiimport_opts.append("--description")
                    munkiimport_opts.append(version_meta["description"])
                    if "--catalog" not in munkiimport_opts:
                        munkiimport_opts.append("--catalog")
                        munkiimport_opts.append("testing")
                    if pref("munki_tool") == "munkiimport":
                        import_cmd = ["/usr/local/munki/munkiimport", "--nointeractive"]
                    elif pref("munki_tool") == "makepkginfo":
                        import_cmd = ["/usr/local/munki/makepkginfo"]
                    else:
                        print "Not sure what tool you wanted to use; munki_tool should be 'munkiimport' " + "or 'makepkginfo' but we got '%s'.  Skipping import." % (
                            pref("munki_tool")
                        )
                        break
                    # Load our app munkiimport options overrides last
                    import_cmd += munkiimport_opts
                    import_cmd.append(version_meta["local_path"])
                    print "Calling %s on %s version %s, file %s." % (
                        pref("munki_tool"),
                        update_name,
                        version_name,
                        version_meta["local_path"],
                    )
                    munkiprocess = subprocess.Popen(import_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    # wait for the process to terminate
                    stdout, stderr = munkiprocess.communicate()
                    import_retcode = munkiprocess.returncode
                    if import_retcode:
                        print "munkiimport returned an error. Skipping update.."
                    else:
                        if pref("munki_tool") == "makepkginfo":
                            plist_path = os.path.splitext(version_meta["local_path"])[0] + ".plist"
                            with open(plist_path, "w") as plist:
                                plist.write(stdout)
                                print "pkginfo written to %s" % plist_path

        print "Done importing into Munki."
        if opts.make_catalogs:
            munkiimport.makeCatalogs()
def main():
    if len(sys.argv) < 2:
        sys.exit("""Usage:
  ./munkiimport_logic_audio.py path/to/LogicProContent/
  See script comments and the README for more detail.""")

    PKGS_DIR = sys.argv[1]
    PKGS_DIR = os.path.abspath(PKGS_DIR)

    # setup logging
    global L
    L = logging.getLogger('com.github.munkiimport_logic_audio')
    log_stdout_handler = logging.StreamHandler(stream=sys.stdout)
    L.addHandler(log_stdout_handler)
    # Hardcode the verbosity as we're not using optparse
    L.setLevel(INFO)

    # Simple munki sanity check
    if not os.path.exists('/usr/local/munki'):
        errorExit("No Munki installation could be found. Get it at https://github.com/munki/munki.")

    # Import munki
    sys.path.append(MUNKI_DIR)
    munkiimport_prefs = os.path.expanduser('~/Library/Preferences/com.googlecode.munki.munkiimport.plist')
    if munki_tool:
        if not os.path.exists(munkiimport_prefs):
            errorExit("Your Munki repo seems to not be configured. Run munkiimport --configure first.")
        try:
            import imp
            # munkiimport doesn't end in .py, so we use imp to make it available to the import system
            imp.load_source('munkiimport', os.path.join(MUNKI_DIR, 'munkiimport'))
            import munkiimport
            munkiimport.REPO_PATH = munkiimport.pref('repo_path')
        except ImportError:
            errorExit("There was an error importing munkilib, which is needed for --munkiimport functionality.")
        # rewrite some of munkiimport's function names since they were changed to
        # snake case around 2.6.1:
        # https://github.com/munki/munki/commit/e3948104e869a6a5eb6b440559f4c57144922e71
        try:
            munkiimport.repoAvailable()
        except AttributeError:
            munkiimport.repoAvailable = munkiimport.repo_available
            munkiimport.makePkgInfo = munkiimport.make_pkginfo
            munkiimport.findMatchingPkginfo = munkiimport.find_matching_pkginfo
            munkiimport.makeCatalogs = munkiimport.make_catalogs
        if not munkiimport.repoAvailable():
            errorExit("The Munki repo cannot be located. This tool is not interactive; first ensure the repo is mounted.")

    # Check for '__Downloaded Items'
    valid_loc = os.path.join(PKGS_DIR, '__Downloaded Items')
    if not os.path.isdir(valid_loc):
        errorExit('"__Downloaded Items" not found! Please re-download audio content or ' +
                  'select a valid directory.')

    # Start searching and importing packages
    for root, dirs, files in os.walk(valid_loc):
        for name in files:
            need_to_import = True
            if name.endswith((".pkg")):
                pkg_path = os.path.join(root, name)
                # Do 'exists in repo' checks
                pkginfo = munkiimport.makePkgInfo([pkg_path], False)
                # Check if package has already been imported, lifted from munkiimport
                matchingpkginfo = munkiimport.findMatchingPkginfo(pkginfo)
                if matchingpkginfo:
                    L.log(VERBOSE, "Got a matching pkginfo.")
                    if ('installer_item_hash' in matchingpkginfo and
                        matchingpkginfo['installer_item_hash'] ==
                        pkginfo.get('installer_item_hash')):
                        need_to_import = False
                        L.log(INFO,
                            ("We have an exact match for %s in the repo. Skipping.." % (
                                name)))
                else:
                    need_to_import = True

                if need_to_import:
                    if name in ESSENTIAL_PKGS:
                        UPDATE4 = LOGICNAME
                    elif 'Alchemy' in name:
                        UPDATE4 = 'LogicProX-Alchemy'
                    else:
                        UPDATE4 = 'LogicProX-BaseLoops'
                    L.log(INFO, ("%s is an update_for %s." % (name, UPDATE4)))
                    # Import into Munki Repo
                    cmd = [
                        "/usr/local/munki/munkiimport",
                        "--nointeractive",
                        "--unattended-install",
                        "--subdirectory", MUNKIIMPORT_OPTIONS[0],
                        "--update-for", UPDATE4,
                        ]
                    cmd += MUNKIIMPORT_OPTIONS
                    cmd.append(pkg_path)
                    subprocess.call(cmd)
    munkiimport.makeCatalogs()