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()
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()
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()