diff --git a/code/client/iconimporter b/code/client/iconimporter index 46a2c449..b1f47327 100755 --- a/code/client/iconimporter +++ b/code/client/iconimporter @@ -29,6 +29,7 @@ from optparse import OptionParser from munkilib import munkicommon from munkilib import FoundationPlist from munkilib import iconutils +from munkilib import Repo # PyLint cannot properly find names inside Cocoa libraries, so issues bogus # No name 'Foo' in module 'Bar' warnings. Disable them. @@ -36,10 +37,9 @@ from munkilib import iconutils from Foundation import CFPreferencesCopyAppValue # pylint: enable=E0611 -def generate_png_from_copy_from_dmg_item(install_item, repo_path): +def generate_png_from_copy_from_dmg_item(install_item, repo): '''Generate a PNG from a disk image containing an application''' - dmgpath = os.path.join( - repo_path, 'pkgs', install_item['installer_item_location']) + dmgpath = repo.join('pkgs', install_item['installer_item_location']) mountpoints = munkicommon.mountdmg(dmgpath) if mountpoints: mountpoint = mountpoints[0] @@ -49,9 +49,9 @@ def generate_png_from_copy_from_dmg_item(install_item, repo_path): app_path = os.path.join(mountpoint, apps[0]['source_item']) icon_path = iconutils.findIconForApp(app_path) if icon_path: - png_path = os.path.join( - repo_path, u'icons', install_item['name'] + u'.png') - result = iconutils.convertIconToPNG(icon_path, png_path) + png_path = repo.join(u'icons', install_item['name'] + u'.png') + png_handle = repo.open(png_path, 'w') + result = iconutils.convertIconToPNG(icon_path, png_handle.local_path) if result: print_utf8(u'\tWrote: %s' % png_path) else: @@ -63,13 +63,13 @@ def generate_png_from_copy_from_dmg_item(install_item, repo_path): munkicommon.unmountdmg(mountpoint) -def generate_pngs_from_installer_pkg(install_item, repo_path): +def generate_pngs_from_installer_pkg(install_item, repo): '''Generate PNGS from applications inside a pkg''' icon_paths = [] mountpoint = None pkg_path = None - item_path = os.path.join( - repo_path, u'pkgs', install_item['installer_item_location']) + pkg_repo = None + item_path = repo.join( u'pkgs', install_item['installer_item_location']) if munkicommon.hasValidDiskImageExt(item_path): dmg_path = item_path mountpoints = munkicommon.mountdmg(dmg_path) @@ -86,28 +86,33 @@ def generate_pngs_from_installer_pkg(install_item, repo_path): break elif munkicommon.hasValidPackageExt(item_path): pkg_path = item_path + pkg_repo = repo if pkg_path: - if os.path.isdir(pkg_path): - icon_paths = iconutils.extractAppIconsFromBundlePkg(pkg_path) + if (pkg_repo and pkg_repo.isdir(pkg_path)) or (not repo and os.path.isdir(pkg_path)): + icon_paths = iconutils.extractAppIconsFromBundlePkg(pkg_path, pkg_repo) else: + handle = None + if pkg_repo: + handle = pkg_repo.open(pkg_path, 'r') + pkg_path = handle.local_path icon_paths = iconutils.extractAppIconsFromFlatPkg(pkg_path) if mountpoint: munkicommon.unmountdmg(mountpoint) if len(icon_paths) == 1: - png_path = os.path.join( - repo_path, u'icons', install_item['name'] + u'.png') - result = iconutils.convertIconToPNG(icon_paths[0], png_path) + png_path = repo.join(u'icons', install_item['name'] + u'.png') + handle = repo.open(png_path, 'w') + result = iconutils.convertIconToPNG(icon_paths[0], handle.local_path) if result: print_utf8(u'\tWrote: %s' % png_path) elif len(icon_paths) > 1: index = 1 for icon_path in icon_paths: - png_path = os.path.join( - repo_path, u'icons', - install_item['name'] + '_' + str(index) + u'.png') - result = iconutils.convertIconToPNG(icon_path, png_path) + png_path = repo.join( + u'icons', install_item['name'] + '_' + str(index) + u'.png') + handle = repo.open(png_path, 'w') + result = iconutils.convertIconToPNG(icon_path, handle.local_path) if result: print_utf8(u'\tWrote: %s' % png_path) index += 1 @@ -115,12 +120,13 @@ def generate_pngs_from_installer_pkg(install_item, repo_path): print_utf8(u'\tNo application icons found.') -def find_items_to_check(repo_path, itemlist=None): +def findItemsToCheck(repo, itemlist=None): '''Builds a list of items to check; only the latest version of an item is retained. If itemlist is given, include items only on that list.''' - all_catalog_path = os.path.join(repo_path, 'catalogs/all') - catalogitems = FoundationPlist.readPlist(all_catalog_path) + all_catalog_path = repo.join('catalogs', 'all') + handle = repo.open(all_catalog_path) + catalogitems = FoundationPlist.readPlist(handle.local_path) itemdb = {} for catalogitem in catalogitems: if itemlist and catalogitem['name'] not in itemlist: @@ -137,27 +143,26 @@ def find_items_to_check(repo_path, itemlist=None): return pkg_list -def generate_pngs_from_munki_items(repo_path, force=False, itemlist=None): +def generate_pngs_from_munki_items(repo, force=False, itemlist=None): '''Generate PNGs from either pkgs or disk images containing applications''' - itemlist = find_items_to_check(repo_path, itemlist=itemlist) - icons_dir = os.path.join(repo_path, u'icons') - if not os.path.exists(icons_dir): - os.mkdir(icons_dir) + itemlist = findItemsToCheck(repo, itemlist=itemlist) + icons_dir = repo.join(u'icons') + if not repo.exists(icons_dir): + repo.mkdir(icons_dir) for item in itemlist: print_utf8(u'Processing %s...' % item['name']) icon_name = item.get('icon_name') or item['name'] if not os.path.splitext(icon_name)[1]: icon_name += u'.png' - icon_path = os.path.join( - repo_path, u'icons', icon_name) - if os.path.exists(icon_path) and not force: + icon_path = repo.join(u'icons', icon_name) + if repo.exists(icon_path) and not force: print_utf8(u'Found existing icon at %s' % icon_name) continue installer_type = item.get('installer_type') if installer_type == 'copy_from_dmg': - generate_png_from_copy_from_dmg_item(item, repo_path) + generate_png_from_copy_from_dmg_item(item, repo) elif installer_type in [None, '']: - generate_pngs_from_installer_pkg(item, repo_path) + generate_pngs_from_installer_pkg(item, repo) else: print_utf8(u'\tCan\'t process installer_type: %s' % installer_type) @@ -185,10 +190,9 @@ def pref(prefname): def main(): '''Main''' - usage = "usage: %prog [options] [/path/to/repo_root]" + usage = "usage: %prog [options] [/path/to/repo_root] [repo_url] [plugin]" parser = OptionParser(usage=usage) - parser.add_option( - '--force', '-f', action='store_true', dest='force', + parser.add_option('--force', '-f', action='store_true', dest='force', help='Create pngs even if there is an existing icon in the repo.') parser.add_option( '--item', '-i', action='append', type='string', dest='items', @@ -198,6 +202,8 @@ def main(): # Make sure we have a path to work with repo_path = None + repo_url = None + plugin = None if len(arguments) == 0: repo_path = pref('repo_path') if not repo_path: @@ -209,13 +215,30 @@ def main(): repo_path = arguments[0].rstrip("/") # Make sure the repo path exists - if not os.path.exists(repo_path): + if len(arguments) > 1: + repo_url = arguments[1].rstrip("/") + else: + repo_url = pref('repo_url') + if not repo_url: + print_err_utf8("Need to specify a URL for the repo!") + exit(-1) + else: + print_utf8("Using repo url: %s" % repo_url) + + if len(arguments) > 2: + plugin = arguments[2] + else: + plugin = pref('plugin') + + # Make sure the repo exists + repo = Repo.Open(repo_path, repo_url, plugin) + if not repo.available(): print_err_utf8("Repo root path %s doesn't exist!" % repo_path) exit(-1) # generate icons! - generate_pngs_from_munki_items(repo_path, force=options.force, - itemlist=options.items) + generate_pngs_from_munki_items(repo, force=options.force, + itemlist=options.items) # clean up munkicommon.cleanUpTmpDir() diff --git a/code/client/makecatalogs b/code/client/makecatalogs index c6c082b1..407eefd3 100755 --- a/code/client/makecatalogs +++ b/code/client/makecatalogs @@ -31,6 +31,7 @@ import sys import os import optparse import hashlib +from munkilib import Repo try: from munkilib import FoundationPlist as plistlib @@ -90,7 +91,7 @@ def print_err_utf8(text): print >> sys.stderr, text.encode('UTF-8') -def makecatalogs(repopath, options): +def makecatalogs(repo, options): '''Assembles all pkginfo files into catalogs. Assumes a pkgsinfo directory under repopath. User calling this needs to be able to write to the repo/catalogs @@ -101,15 +102,15 @@ def makecatalogs(repopath, options): exit_code = 0 # Make sure the icons directory exists - iconspath = os.path.join(repopath, 'icons') - # make sure iconspath is Unicode so that os.walk later gives us + iconspath = repo.join('icons') + # make sure iconspath is Unicode so that repo.walk later gives us # Unicode names back. if type(iconspath) is str: iconspath = unicode(iconspath, 'utf-8') elif type(iconspath) is not unicode: iconspath = unicode(iconspath) - if not os.path.exists(iconspath): + if not repo.exists(iconspath): print_err_utf8("icons path %s doesn't exist, skipping hashing!" % iconspath) iconhashing = False @@ -119,7 +120,7 @@ def makecatalogs(repopath, options): iconhashing = True # Walk through the icon files - for dirpath, dirnames, filenames in os.walk(iconspath): + for dirpath, dirnames, filenames in repo.walk(iconspath): for dirname in dirnames: # don't recurse into directories that start # with a period. @@ -130,7 +131,7 @@ def makecatalogs(repopath, options): # skip files that start with a period as well continue - filepath = os.path.join(dirpath, filename) + filepath = repo.join(dirpath, filename) iconpath = filepath.rsplit(iconspath + os.path.sep, 1)[1] @@ -138,7 +139,7 @@ def makecatalogs(repopath, options): try: print_utf8("Hashing %s..." % (iconpath)) icons[iconpath] = ( - hashlib.sha256(open(filepath, 'rb').read()).hexdigest()) + hashlib.sha256(repo.open(filepath, 'rb').read()).hexdigest()) except IOError, inst: errors.append("IO error for %s: %s" % (filepath, inst)) exit_code = -1 @@ -150,15 +151,15 @@ def makecatalogs(repopath, options): continue # Make sure the pkgsinfo directory exists - pkgsinfopath = os.path.join(repopath, 'pkgsinfo') - # make sure pkgsinfopath is Unicode so that os.walk later gives us + pkgsinfopath = repo.join('pkgsinfo') + # make sure pkgsinfopath is Unicode so that repo.walk later gives us # Unicode names back. if type(pkgsinfopath) is str: pkgsinfopath = unicode(pkgsinfopath, 'utf-8') elif type(pkgsinfopath) is not unicode: pkgsinfopath = unicode(pkgsinfopath) - if not os.path.exists(pkgsinfopath): + if not repo.exists(pkgsinfopath): print_err_utf8("pkgsinfo path %s doesn't exist!" % pkgsinfopath) exit(-1) @@ -167,7 +168,7 @@ def makecatalogs(repopath, options): catalogs['all'] = [] # Walk through the pkginfo files - for dirpath, dirnames, filenames in os.walk(pkgsinfopath, followlinks=True): + for dirpath, dirnames, filenames in repo.walk(pkgsinfopath, followlinks=True): for dirname in dirnames: # don't recurse into directories that start # with a period. @@ -178,11 +179,13 @@ def makecatalogs(repopath, options): # skip files that start with a period as well continue - filepath = os.path.join(dirpath, filename) + filepath = repo.join(dirpath, filename) # Try to read the pkginfo file try: - pkginfo = plistlib.readPlist(filepath) + handle = repo.open(filepath, 'r') + if handle: + pkginfo = plistlib.readPlist(handle.local_path) except IOError, inst: errors.append("IO error for %s: %s" % (filepath, inst)) exit_code = -1 @@ -249,8 +252,8 @@ def makecatalogs(repopath, options): # Try to form a path and fail if the # installer_item_location is not a valid type try: - installeritempath = os.path.join( - repopath, "pkgs", pkginfo['installer_item_location']) + installeritempath = repo.join( + "pkgs", pkginfo['installer_item_location']) except TypeError: errors.append("WARNING: invalid installer_item_location " "in info file %s" @@ -259,7 +262,7 @@ def makecatalogs(repopath, options): continue # Check if the installer item actually exists - if not os.path.exists(installeritempath): + if not repo.exists(installeritempath): errors.append("WARNING: Info file %s refers to " "missing installer item: %s" % (filepath[len(pkgsinfopath)+1:], @@ -285,8 +288,8 @@ def makecatalogs(repopath, options): # if an uninstaller_item_location is specified, sanity-check it if 'uninstaller_item_location' in pkginfo: try: - uninstalleritempath = os.path.join( - repopath, "pkgs", pkginfo['uninstaller_item_location']) + uninstalleritempath = repo.join( + "pkgs", pkginfo['uninstaller_item_location']) except TypeError: errors.append("WARNING: invalid uninstaller_item_location " "in info file %s" @@ -295,7 +298,7 @@ def makecatalogs(repopath, options): continue # Check if the uninstaller item actually exists - if not os.path.exists(uninstalleritempath): + if not repo.exists(uninstalleritempath): errors.append("WARNING: Info file %s refers to " "missing uninstaller item: %s" % (filepath[len(pkgsinfopath)+1:], @@ -325,20 +328,20 @@ def makecatalogs(repopath, options): print_err_utf8(error) # clear out old catalogs - catalogpath = os.path.join(repopath, "catalogs") - if not os.path.exists(catalogpath): - os.mkdir(catalogpath) + catalogpath = repo.join("catalogs") + if not repo.exists(catalogpath): + repo.mkdir(catalogpath) else: - for item in listdir(catalogpath): - itempath = os.path.join(catalogpath, item) - if os.path.isfile(itempath): - os.remove(itempath) + for item in repo.listdir(catalogpath): + itempath = repo.join(catalogpath, item) + if repo.isfile(itempath): + repo.remove(itempath) # write the new catalogs print for key in catalogs.keys(): - catalogpath = os.path.join(repopath, "catalogs", key) - if os.path.exists(catalogpath): + catalogpath = repo.join("catalogs", key) + if repo.exists(catalogpath): print_err_utf8( "WARNING: catalog %s already exists at %s. " "Perhaps this is a non-case sensitive filesystem and you " @@ -346,8 +349,10 @@ def makecatalogs(repopath, options): % (key, catalogpath)) exit_code = -1 elif len(catalogs[key]) != 0: - plistlib.writePlist(catalogs[key], catalogpath) - print "Created catalog %s..." % (catalogpath) + handle = repo.open(catalogpath, 'w') + if handle: + plistlib.writePlist(catalogs[key], handle.local_path) + print "Created catalog %s..." % (catalogpath) else: print_err_utf8( "WARNING: Did not create catalog %s " @@ -386,6 +391,12 @@ def main(): help='Print the version of the munki tools and exit.') parser.add_option('--force', '-f', action='store_true', dest='force', help='Disable sanity checks.') + parser.add_option('--repo_url', '--repo-url', default=pref('repo_url'), + help='Optional repo fileshare URL that takes precedence ' + 'over the default repo_url specified via ' + '--configure.') + parser.add_option('--plugin', '--plugin', default=pref('plugin'), + help='Specify a custom plugin to run for munkiimport Repo.') parser.set_defaults(force=False) options, arguments = parser.parse_args() @@ -406,13 +417,13 @@ def main(): repopath = arguments[0].rstrip("/") # Make sure the repo path exists - if not os.path.exists(repopath): + repo = Repo.Open(repopath, options.repo_url, options.plugin) + if not repo.exists(): print_err_utf8("Repo root path %s doesn't exist!" % repopath) exit(-1) # Make the catalogs - makecatalogs(repopath, options) + makecatalogs(repo, options) if __name__ == '__main__': main() - diff --git a/code/client/manifestutil b/code/client/manifestutil index b98743b5..3a822f05 100755 --- a/code/client/manifestutil +++ b/code/client/manifestutil @@ -35,6 +35,9 @@ import time from ctypes.util import find_library from xml.parsers.expat import ExpatError +from munkilib import munkicommon +from munkilib import Repo + try: from munkilib.munkicommon import get_version except ImportError: @@ -269,12 +272,12 @@ def get_installer_item_names(cataloglist): '''Returns a list of unique installer item (pkg) names from the given list of catalogs''' item_list = [] - catalogs_path = os.path.join(pref('repo_path'), 'catalogs') - for filename in os.listdir(catalogs_path): + catalogs_path = repo.join('catalogs') + for filename in repo.listdir(catalogs_path): if filename in cataloglist: try: - catalog = plistlib.readPlist( - os.path.join(catalogs_path, filename)) + handle = repo.open(repo.join(catalogs_path, filename), 'r') + catalog = plistlib.readPlist(handle.local_path) except (IOError, OSError, ExpatError): # skip items that aren't valid plists # or that we can't read @@ -290,9 +293,9 @@ def get_installer_item_names(cataloglist): def get_manifest_names(): '''Returns a list of available manifests''' - manifests_path = os.path.join(pref('repo_path'), 'manifests') + manifests_path = repo.join('manifests') manifests = [] - for dirpath, dirnames, filenames in os.walk(manifests_path): + for dirpath, dirnames, filenames in repo.walk(manifests_path): for dirname in dirnames: # don't recurse into directories that start # with a period. @@ -303,21 +306,22 @@ def get_manifest_names(): if name.startswith("."): # don't process these continue - manifests.append(os.path.join(subdir, name).lstrip('/')) + manifests.append(repo.join(subdir, name).lstrip('/')) manifests.sort() return manifests def get_catalogs(): '''Returns a list of available catalogs''' - catalogs_path = os.path.join(pref('repo_path'), 'catalogs') + catalogs_path = repo.join('catalogs') catalogs = [] - for name in os.listdir(catalogs_path): + for name in repo.listdir(catalogs_path): if name.startswith(".") or name == 'all': # don't process these continue try: - _ = plistlib.readPlist(os.path.join(catalogs_path, name)) + handle = repo.open(repo.join(catalogs_path, name), 'r') + _ = plistlib.readPlist(handle.local_path) except (IOError, OSError, ExpatError): # skip items that aren't valid plists pass @@ -369,11 +373,11 @@ def printplist(plistdict): def get_manifest(manifest_name): '''Gets the contents of a manifest''' - manifest_path = os.path.join( - pref('repo_path'), 'manifests', manifest_name) - if os.path.exists(manifest_path): + manifest_path = repo.join('manifests', manifest_name) + if repo.exists(manifest_path): try: - return plistlib.readPlist(manifest_path) + handle = repo.open(manifest_path, 'r') + return plistlib.readPlist(handle.local_path) except (IOError, OSError, ExpatError): print >> sys.stderr, ( 'Could not read manifest %s' % manifest_name) @@ -385,14 +389,14 @@ def get_manifest(manifest_name): def save_manifest(manifest_dict, manifest_name, overwrite_existing=False): '''Saves a manifest to disk''' - manifest_path = os.path.join( - pref('repo_path'), 'manifests', manifest_name) + manifest_path = repo.join('manifests', manifest_name) if not overwrite_existing: - if os.path.exists(manifest_path): + if repo.exists(manifest_path): print >> sys.stderr, '%s already exists!' % manifest_name return False try: - plistlib.writePlist(manifest_dict, manifest_path) + handle = repo.open(manifest_path, 'w') + plistlib.writePlist(manifest_dict, handle.local_path) return True except (IOError, OSError, ExpatError), err: print >> sys.stderr, 'Saving %s failed: %s' % (manifest_name, err) @@ -417,77 +421,14 @@ def manifest_rename(source_manifest_name, dest_manifest_name, source_manifest_name, dest_manifest_name, err) return False -def repo_available(): - """Checks the repo path for proper directory structure. - If the directories look wrong we probably don't have a - valid repo path. Returns True if things look OK.""" - repo_path = pref('repo_path') - if not repo_path: - print >> sys.stderr, 'No repo path specified.' - return False - if not os.path.exists(repo_path) and pref('repo_url'): - mount_repo_cli() - if not os.path.exists(repo_path): - return False - for subdir in ['catalogs', 'manifests', 'pkgs', 'pkgsinfo']: - if not os.path.exists(os.path.join(repo_path, subdir)): - print >> sys.stderr, "%s is missing %s" % (repo_path, subdir) - return False - # if we get this far, the repo path looks OK - return True - - -def mount_repo_cli(): - """Attempts to connect to the repo fileshare""" - global WE_MOUNTED_THE_REPO - repo_path = pref('repo_path') - repo_url = pref('repo_url') - if os.path.exists(repo_path): - return - print 'Attempting to mount fileshare %s:' % repo_path - if NETFSMOUNTURLSYNC_AVAILABLE: - # mount the share using the NetFS API - try: - mount_share_url(repo_url) - except ShareMountException, err: - print >> sys.stderr, err - else: - WE_MOUNTED_THE_REPO = True - else: - # do it the old way - os.mkdir(repo_path) - if repo_url.startswith('afp:'): - cmd = ['/sbin/mount_afp', '-i', repo_url, repo_path] - elif repo_url.startswith('smb:'): - cmd = ['/sbin/mount_smbfs', repo_url[4:], repo_path] - elif repo_url.startswith('nfs://'): - cmd = ['/sbin/mount_nfs', repo_url[6:], repo_path] - else: - print >> sys.stderr, 'Unsupported filesystem URL!' - return - retcode = subprocess.call(cmd) - if retcode: - os.rmdir(repo_path) - else: - WE_MOUNTED_THE_REPO = True - - -def unmount_repo_cli(): - """Attempts to unmount the repo fileshare""" - repo_path = pref('repo_path') - if not os.path.exists(repo_path): - return - cmd = ['/sbin/umount', repo_path] - return subprocess.call(cmd) - - def cleanup_and_exit(exitcode): """Give the user the chance to unmount the repo when we exit""" result = 0 - if WE_MOUNTED_THE_REPO: + if repo and repo.mounted and repo.WE_MOUNTED_THE_REPO: answer = raw_input('Unmount the repo fileshare? [y/n] ') if answer.lower().startswith('y'): - result = unmount_repo_cli() + result = repo.unmount() + munkicommon.cleanUpTmpDir() exit(exitcode or result) @@ -629,12 +570,13 @@ def find(args): findtext = arguments[0] keyname = options.section - manifests_path = os.path.join(pref('repo_path'), 'manifests') + manifests_path = repo.join('manifests') count = 0 for name in get_manifest_names(): - pathname = os.path.join(manifests_path, name) + pathname = repo.join(manifests_path, name) try: - manifest = plistlib.readPlist(pathname) + handle = repo.open(pathname, 'r') + manifest = plistlib.readPlist(handle.local_path) except (IOError, OSError, ExpatError): print >> sys.stderr, 'Error reading %s' % pathname continue @@ -1184,6 +1126,8 @@ def set_up_tab_completer(): def handle_subcommand(args): + global repo + '''Does all our subcommands''' # check if any arguments are passed. # if not, list subcommands. @@ -1203,8 +1147,10 @@ def handle_subcommand(args): if (subcommand not in ['version', 'configure', 'help'] and '-h' not in args and '--help' not in args): - if not repo_available(): - exit(-1) + if not repo: + repo = Repo.Open(pref('repo_path'), pref('repo_url'), pref('plugin')) + if not repo.available(): + exit(-1) try: # find function to call by looking in the global name table @@ -1220,13 +1166,14 @@ def handle_subcommand(args): return subcommand_function(args[1:]) -WE_MOUNTED_THE_REPO = False INTERACTIVE_MODE = False CMD_ARG_DICT = {} +repo = None def main(): '''Our main routine''' global INTERACTIVE_MODE + global repo cmds = {'add-pkg': 'pkgs', 'add-catalog': 'catalogs', @@ -1261,7 +1208,8 @@ def main(): # so let's enter interactive mode INTERACTIVE_MODE = True # must have an available repo for interfactive mode - if not repo_available(): + repo = Repo.Open(pref('repo_path'), pref('repo_url'), pref('plugin')) + if not repo.available(): exit(-1) # build the rest of our dict to enable tab completion CMD_ARG_DICT['options'] = {'--manifest': 'manifests', diff --git a/code/client/munkiimport b/code/client/munkiimport index f167fb34..50e843ca 100755 --- a/code/client/munkiimport +++ b/code/client/munkiimport @@ -31,6 +31,7 @@ import subprocess import sys import time import thread +import re from ctypes.util import find_library from optparse import OptionParser, BadOptionError, AmbiguousOptionError @@ -40,6 +41,7 @@ import objc from munkilib import iconutils from munkilib import munkicommon from munkilib import FoundationPlist +from munkilib import Repo # PyLint cannot properly find names inside Cocoa libraries, so issues bogus # No name 'Foo' in module 'Bar' warnings. Disable them. @@ -144,7 +146,6 @@ def mount_share_url(share_url): password = getpass.getpass() mount_share_with_credentials(share_url, username, password) - if 'libedit' in readline.__doc__: # readline module was compiled against libedit LIBEDIT = ctypes.cdll.LoadLibrary(find_library('libedit')) @@ -193,10 +194,8 @@ def raw_input_with_default(prompt, default_text): class PassThroughOptionParser(OptionParser): """ An unknown option pass-through implementation of OptionParser. - When unknown arguments are encountered, bundle with largs and try again, until rargs is depleted. - sys.exit(status) will still be called if a known argument is passed incorrectly (e.g. missing arguments or bad argument types, etc.) """ @@ -216,10 +215,10 @@ def make_dmg(pkgpath): """Wraps a non-flat package into a disk image. Returns path to newly-created disk image.""" - pkgname = os.path.basename(pkgpath) + pkgname = repo.basename(pkgpath) print 'Making disk image containing %s...' % pkgname - diskimagename = os.path.splitext(pkgname)[0] + '.dmg' - diskimagepath = os.path.join(munkicommon.tmpdir(), diskimagename) + diskimagename = repo.splitext(pkgname)[0] + '.dmg' + diskimagepath = repo.join(munkicommon.tmpdir(), diskimagename) cmd = ['/usr/bin/hdiutil', 'create', '-srcfolder', pkgpath, diskimagepath] proc = subprocess.Popen(cmd, shell=False, bufsize=-1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -239,66 +238,6 @@ def make_dmg(pkgpath): return diskimagepath -def repo_available(): - """Checks the repo path for proper directory structure. - If the directories look wrong we probably don't have a - valid repo path. Returns True if things look OK.""" - if not REPO_PATH: - print >> sys.stderr, 'No repo path specified.' - return False - if not os.path.exists(REPO_PATH) and REPO_URL: - mount_repo_cli() - if not os.path.exists(REPO_PATH): - return False - for subdir in ['catalogs', 'manifests', 'pkgs', 'pkgsinfo']: - if not os.path.exists(os.path.join(REPO_PATH, subdir)): - print >> sys.stderr, "%s is missing %s" % (REPO_PATH, subdir) - return False - # if we get this far, the repo path looks OK - return True - - -def mount_repo_cli(): - """Attempts to connect to the repo fileshare""" - global WE_MOUNTED_THE_REPO - if os.path.exists(REPO_PATH): - return - print 'Attempting to mount fileshare %s:' % REPO_URL - if NETFSMOUNTURLSYNC_AVAILABLE: - # mount the share using the NetFS API - try: - mount_share_url(REPO_URL) - except ShareMountException, err: - print >> sys.stderr, err - else: - WE_MOUNTED_THE_REPO = True - else: - # do it the old way - os.mkdir(REPO_PATH) - if REPO_URL.startswith('afp:'): - cmd = ['/sbin/mount_afp', '-i', REPO_URL, REPO_PATH] - elif REPO_URL.startswith('smb:'): - cmd = ['/sbin/mount_smbfs', REPO_URL[4:], REPO_PATH] - elif REPO_URL.startswith('nfs://'): - cmd = ['/sbin/mount_nfs', REPO_URL[6:], REPO_PATH] - else: - print >> sys.stderr, 'Unsupported filesystem URL!' - return - retcode = subprocess.call(cmd) - if retcode: - os.rmdir(REPO_PATH) - else: - WE_MOUNTED_THE_REPO = True - - -def unmount_repo_cli(): - """Attempts to unmount the repo fileshare""" - if not os.path.exists(REPO_PATH): - return - cmd = ['/sbin/umount', REPO_PATH] - return subprocess.call(cmd) - - class RepoCopyError(Exception): """Error copying installer item to repo""" pass @@ -313,62 +252,61 @@ def copy_item_to_repo(itempath, vers, subdirectory=''): if not os.path.exists(REPO_PATH): raise RepoCopyError('Could not connect to munki repo.') - destination_path = os.path.join(REPO_PATH, 'pkgs', subdirectory) - if not os.path.exists(destination_path): + destination_path = repo.join('pkgs', subdirectory) + if not repo.exists(destination_path): try: - os.makedirs(destination_path) + repo.makedirs(destination_path) except OSError, errmsg: raise RepoCopyError('Could not create %s: %s' % (destination_path, errmsg)) - item_name = os.path.basename(itempath) - destination_path_name = os.path.join(destination_path, item_name) + item_name = repo.basename(itempath) + destination_path_name = repo.join(destination_path, item_name) if itempath == destination_path_name: # we've been asked to 'import' a repo item. # just return the relative path - return os.path.join(subdirectory, item_name) + return repo.join(subdirectory, item_name) if vers: - name, ext = os.path.splitext(item_name) + name, ext = repo.splitext(item_name) if not name.endswith(vers): # add the version number to the end of the filename item_name = '%s-%s%s' % (name, vers, ext) - destination_path_name = os.path.join(destination_path, item_name) + destination_path_name = repo.join(destination_path, item_name) index = 0 - name, ext = os.path.splitext(item_name) - while os.path.exists(destination_path_name): + name, ext = repo.splitext(item_name) + while repo.exists(destination_path_name): print 'File %s already exists...' % destination_path_name # try appending numbers until we have a unique name index += 1 item_name = '%s__%s%s' % (name, index, ext) - destination_path_name = os.path.join(destination_path, item_name) + destination_path_name = repo.join(destination_path, item_name) - print 'Copying %s to %s...' % (os.path.basename(itempath), + print 'Copying %s to %s...' % (repo.basename(itempath), destination_path_name) - cmd = ['/bin/cp', itempath, destination_path_name] - retcode = subprocess.call(cmd) + retcode = repo.put(itempath, destination_path_name) if retcode: raise RepoCopyError('Unable to copy %s to %s' % (itempath, destination_path_name)) else: - return os.path.join(subdirectory, item_name) + return repo.join(subdirectory, item_name) def get_icon_path(pkginfo): """Return path for icon""" icon_name = pkginfo.get('icon_name') or pkginfo['name'] - if not os.path.splitext(icon_name)[1]: + if not repo.splitext(icon_name)[1]: icon_name += u'.png' - return os.path.join(REPO_PATH, u'icons', icon_name) + return repo.join(u'icons', icon_name) def icon_exists_in_repo(pkginfo): """Returns True if there is an icon for this item in the repo""" icon_path = get_icon_path(pkginfo) - if os.path.exists(icon_path): + if repo.exists(icon_path): return True return False @@ -376,7 +314,7 @@ def icon_exists_in_repo(pkginfo): def add_icon_hash_to_pkginfo(pkginfo): """Adds the icon hash tp pkginfo if the icon exists in repo""" icon_path = get_icon_path(pkginfo) - if os.path.isfile(icon_path): + if repo.isfile(icon_path): pkginfo['icon_hash'] = munkicommon.getsha256hash(icon_path) @@ -443,10 +381,10 @@ def generate_pngs_from_installer_pkg(item_path, pkginfo): def convert_and_install_icon(pkginfo, icon_path, index=None): '''Convert icon file to png and save to repo icon path''' - destination_path = os.path.join(REPO_PATH, 'icons') - if not os.path.exists(destination_path): + destination_path = 'icons' + if not repo.exists(destination_path): try: - os.makedirs(destination_path) + repo.makedirs(destination_path) except OSError, errmsg: print >> sys.stderr, ('Could not create %s: %s' % (destination_path, errmsg)) @@ -456,37 +394,41 @@ def convert_and_install_icon(pkginfo, icon_path, index=None): else: destination_name = pkginfo['name'] - png_path = os.path.join( - destination_path, destination_name + u'.png') - result = iconutils.convertIconToPNG(icon_path, png_path) + png_name = destination_name + u'.png' + png_path = repo.join(destination_path, png_name) + png_tmp = repo.join(munkicommon.tmpdir(), png_name) + result = iconutils.convertIconToPNG(icon_path, png_tmp) if result: - print 'Created icon: %s' % png_path + result = repo.put(png_tmp, png_path) + if result == 0: + print 'Created icon: %s' % png_path + else: + print >> sys.stderr, u'Error uploading icon to %s.' % png_path else: print >> sys.stderr, u'Error converting %s to png.' % icon_path def copy_icon_to_repo(iconpath): """Saves a product icon to the repo""" - destination_path = os.path.join(REPO_PATH, 'icons') - if not os.path.exists(destination_path): + destination_path = 'icons' + if not repo.exists(destination_path): try: - os.makedirs(destination_path) + repo.makedirs(destination_path) except OSError, errmsg: raise RepoCopyError('Could not create %s: %s' % (destination_path, errmsg)) - icon_name = os.path.basename(iconpath) - destination_path_name = os.path.join(destination_path, icon_name) + icon_name = repo.basename(iconpath) + destination_path_name = repo.join(destination_path, icon_name) - if os.path.exists(destination_path_name): + if repo.exists(destination_path_name): # remove any existing icon in the repo try: - os.unlink(destination_path_name) + repo.unlink(destination_path_name) except OSError, errmsg: raise RepoCopyError('Could not remove existing %s' % (destination_path_name)) print 'Copying %s to %s...' % (icon_name, destination_path_name) - cmd = ['/bin/cp', iconpath, destination_path_name] - retcode = subprocess.call(cmd) + retcode = repo.put(iconpath, destination_path_name) if retcode: raise RepoCopyError('Unable to copy %s to %s' % (iconpath, destination_path_name)) @@ -496,10 +438,10 @@ def copy_pkginfo_to_repo(pkginfo, subdirectory=''): """Saves pkginfo to munki_repo_path/pkgsinfo/subdirectory""" # less error checking because we copy the installer_item # first and bail if it fails... - destination_path = os.path.join(REPO_PATH, 'pkgsinfo', subdirectory) - if not os.path.exists(destination_path): + destination_path = repo.join(repo.path, 'pkgsinfo', subdirectory) + if not repo.exists(destination_path): try: - os.makedirs(destination_path) + repo.makedirs(destination_path) except OSError, errmsg: raise RepoCopyError('Could not create %s: %s' % (destination_path, errmsg)) @@ -508,17 +450,18 @@ def copy_pkginfo_to_repo(pkginfo, subdirectory=''): pkginfo_ext = '.' + pkginfo_ext pkginfo_name = '%s-%s%s' % (pkginfo['name'], pkginfo['version'], pkginfo_ext) - pkginfo_path = os.path.join(destination_path, pkginfo_name) + pkginfo_path = repo.join(destination_path, pkginfo_name) index = 0 - while os.path.exists(pkginfo_path): + while repo.exists(pkginfo_path): index += 1 pkginfo_name = '%s-%s__%s%s' % (pkginfo['name'], pkginfo['version'], index, pkginfo_ext) - pkginfo_path = os.path.join(destination_path, pkginfo_name) + pkginfo_path = repo.join(destination_path, pkginfo_name) print 'Saving pkginfo to %s...' % pkginfo_path try: - FoundationPlist.writePlist(pkginfo, pkginfo_path) + handle = repo.open(pkginfo_path, 'w') + FoundationPlist.writePlist(pkginfo, handle.local_path) except FoundationPlist.NSPropertyListWriteException, errmsg: raise RepoCopyError(errmsg) return pkginfo_path @@ -545,13 +488,13 @@ def prompt_for_subdirectory(subdirectory): newdir = raw_input( 'Upload item to subdirectory path [%s]: ' % subdirectory) if newdir: - if not repo_available(): + if not repo.available(): raise RepoCopyError('Could not connect to munki repo.') if APPLEMETADATA: - destination_path = os.path.join(REPO_PATH, 'pkgsinfo', newdir) + destination_path = repo.join('pkgsinfo', newdir) else: - destination_path = os.path.join(REPO_PATH, 'pkgs', newdir) - if not os.path.exists(destination_path): + destination_path = repo.join('pkgs', newdir) + if not repo.exists(destination_path): answer = raw_input('Path %s doesn\'t exist. Create it? [y/n] ' % destination_path) if answer.lower().startswith('y'): @@ -571,11 +514,16 @@ class CatalogDBException(Exception): def make_catalog_db(): """Returns a dict we can use like a database""" - all_items_path = os.path.join(REPO_PATH, 'catalogs', 'all') - if not os.path.exists(all_items_path): - raise CatalogDBException + all_items_path = repo.join('catalogs', 'all') + handle = None + try: - catalogitems = FoundationPlist.readPlist(all_items_path) + handle = repo.open(all_items_path, 'r') + except IOError: + raise CatalogDBException + + try: + catalogitems = FoundationPlist.readPlist(handle.local_path) except FoundationPlist.NSPropertyListSerializationException: raise CatalogDBException @@ -602,9 +550,9 @@ def make_catalog_db(): # add to installer item table if 'installer_item_location' in item: - installer_item_name = os.path.basename( + installer_item_name = repo.basename( item['installer_item_location']) - (name, ext) = os.path.splitext(installer_item_name) + (name, ext) = repo.splitext(installer_item_name) if '-' in name: (name, vers) = munkicommon.nameAndVersion(name) installer_item_name = name + ext @@ -728,7 +676,7 @@ def find_matching_pkginfo(pkginfo): # no matches by receipts or installed applications, # let's try to match based on installer_item_name - installer_item_name = os.path.basename( + installer_item_name = repo.basename( pkginfo.get('installer_item_location', '')) possiblematches = catdb['installer_items'].get(installer_item_name) if possiblematches: @@ -789,17 +737,21 @@ def make_pkginfo(options=None, test_mode=False): def make_catalogs(): """Calls makecatalogs to rebuild our catalogs""" # first look for a makecatalogs in the same dir as us - mydir = os.path.dirname(os.path.abspath(__file__)) + mydir = repo.dirname(os.path.abspath(__file__)) makecatalogs_path = os.path.join(mydir, 'makecatalogs') - if not os.path.exists(makecatalogs_path): + if not repo.exists(makecatalogs_path): # didn't find it; assume the default install path makecatalogs_path = '/usr/local/munki/makecatalogs' - if not repo_available(): + if not repo.available(): raise RepoCopyError('Could not connect to munki repo.') if not VERBOSE: print 'Rebuilding catalogs at %s...' % REPO_PATH - proc = subprocess.Popen([makecatalogs_path, REPO_PATH], - bufsize=-1, stdout=subprocess.PIPE, + cmd = [makecatalogs_path] + if REPO_URL: + cmd.append('--repo-url') + cmd.append(REPO_URL) + cmd.append(REPO_PATH) + proc = subprocess.Popen(cmd, bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) while True: output = proc.stdout.readline() @@ -817,13 +769,13 @@ def make_catalogs(): def cleanup_and_exit(exitcode): """Unmounts the repo if we mounted it, then exits""" result = 0 - if WE_MOUNTED_THE_REPO: + if repo and repo.mounted and repo.WE_MOUNTED_THE_REPO: if not NOINTERACTIVE: answer = raw_input('Unmount the repo fileshare? [y/n] ') if answer.lower().startswith('y'): - result = unmount_repo_cli() + result = repo.unmount() else: - result = unmount_repo_cli() + result = repo.unmount() # clean up tmpdir munkicommon.cleanUpTmpDir() @@ -853,8 +805,27 @@ def configure(): ('editor', 'pkginfo editor (examples: /usr/bin/vi or TextMate.app; ' 'leave empty to not open an editor after import)'), - ('default_catalog', 'Default catalog to use (example: testing)')]: - + ('default_catalog', 'Default catalog to use (example: testing)'), + ('plugin', 'Use a plugin to write to a custom munki repository') + ]: + if key == 'plugin': + # first look for a plugins folder in the same dir as us + plugin_path = os.path.dirname(os.path.abspath(__file__)) + plugin_path = os.path.join(plugin_path, 'munkilib/plugins') + if not os.path.exists(plugin_path): + # didn't find it; assume the default install path + plugin_path = '/usr/local/munki/munkilib/plugins' + hasPlugin = False + included_extensions = ['py'] + plugins = [fn for fn in os.listdir(plugin_path) + if any(fn.endswith(ext) for ext in included_extensions)] + for plugin in plugins: + if 'FileRepo' not in plugin: + hasPlugin = True + if not os.path.isdir(plugin_path): + continue + if not hasPlugin: + continue _prefs[key] = raw_input_with_default('%15s: ' % prompt, pref(key)) for key, value in _prefs.items(): @@ -871,10 +842,10 @@ PREFSPATH = os.path.expanduser(os.path.join('~/Library/Preferences', PREFSNAME)) APPLEMETADATA = False NOINTERACTIVE = False -WE_MOUNTED_THE_REPO = False VERBOSE = False REPO_PATH = "" REPO_URL = "" +repo = None def main(): """Main routine""" @@ -883,12 +854,12 @@ def main(): global VERBOSE global REPO_PATH global REPO_URL + global repo usage = """usage: %prog [options] /path/to/installer_item Imports an installer item into a munki repo. Installer item can be a pkg, mpkg, dmg, mobileconfig, or app. Bundle-style pkgs and apps are wrapped in a dmg file before upload. - Example: munkiimport --subdirectory apps /path/to/installer_item """ @@ -897,7 +868,6 @@ def main(): In addition to the options described above, options used with 'makepkginfo' may also be specified to customize the resulting pkginfo file. - Example: munkiimport --subdirectory apps -c production --minimum_os_vers 10.6.8 /path/to/installer_item\n""" @@ -924,6 +894,8 @@ def main(): help='Optional repo fileshare URL that takes precedence ' 'over the default repo_url specified via ' '--configure.') + parser.add_option('--plugin', '--plugin', default='', + help='Optional custom plugin to run for munkiimport Repo.') parser.add_option('--icon_path', '--icon-path', default='', type='string', help='Path to an icon file for the package. ' 'Will overwrite an existing icon.') @@ -945,8 +917,10 @@ def main(): NOINTERACTIVE = options.nointeractive VERBOSE = options.verbose + #default is what user put in munkiimport --configure REPO_PATH = pref('repo_path') REPO_URL = pref('repo_url') + REPO_PLUGIN = pref('plugin') if options.repo_path: if not os.path.exists(options.repo_path) and not options.repo_url: @@ -957,10 +931,13 @@ def main(): exit(-1) REPO_PATH = options.repo_path - + #if specified options, override defaults if options.repo_url: REPO_URL = options.repo_url + if options.plugin: + REPO_PLUGIN = options.plugin + if options.icon_path and not os.path.isfile(options.icon_path): print >> sys.stderr, ('The specified icon file does not exist.') exit(-1) @@ -1015,13 +992,14 @@ def main(): 'tool, or provide with --repo-path') exit(-1) - if not repo_available(): + repo = Repo.Open(REPO_PATH, REPO_URL, REPO_PLUGIN) + if not repo.available(): print >> sys.stderr, ('Could not connect to munki repo. Check the ' 'configuration and try again.') exit(-1) if not APPLEMETADATA: - if os.path.isdir(installer_item): # Start of indent + if repo.isdir(installer_item): # Start of indent if munkicommon.hasValidDiskImageExt(installer_item): # a directory named foo.dmg or foo.iso! print >> sys.stderr, '%s is an unknown type.' % installer_item @@ -1042,7 +1020,7 @@ def main(): arguments.append(installer_item) # End of indent if uninstaller_item: - if os.path.isdir(uninstaller_item): + if repo.isdir(uninstaller_item): if munkicommon.hasValidDiskImageExt(uninstaller_item): # a directory named foo.dmg or foo.iso! print >> sys.stderr, ( @@ -1142,8 +1120,10 @@ def main(): pkginfo[key] = raw_input_with_default(prompt, default) if kind == 'bool': value = pkginfo[key].lower().strip() - # set key to True/False - pkginfo[key] = value.startswith(('y', 't')) + if value.startswith(('y', 't')): + pkginfo[key] = True + else: + pkginfo[key] = False # special handling for catalogs array prompt = '%20s: ' % 'Catalogs' @@ -1173,11 +1153,11 @@ def main(): cleanup_and_exit(0) if options.subdirectory == '': - pkgs_path = os.path.join(REPO_PATH, 'pkgs') + pkgs_path = repo.join('pkgs') if not APPLEMETADATA and installer_item.startswith(pkgs_path): # the installer item is already in the repo. # use its relative path as the subdirectory - installer_item_dirpath = os.path.dirname(installer_item) + installer_item_dirpath = repo.dirname(installer_item) options.subdirectory = \ installer_item_dirpath[len(pkgs_path)+1:] options.subdirectory = prompt_for_subdirectory( @@ -1230,7 +1210,7 @@ def main(): # adjust the uninstaller_item_location to match # the actual location and name; update size and hash pkginfo['uninstaller_item_location'] = uploaded_pkgpath - itemsize = int(os.path.getsize(uninstaller_item)) + itemsize = int(repo.getsize(uninstaller_item)) itemhash = munkicommon.getsha256hash(uninstaller_item) pkginfo['uninstaller_item_size'] = int(itemsize/1024) pkginfo['uninstaller_item_hash'] = itemhash diff --git a/code/client/munkilib/Repo.py b/code/client/munkilib/Repo.py new file mode 100644 index 00000000..4bfa3ca2 --- /dev/null +++ b/code/client/munkilib/Repo.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# encoding: utf-8 +# +# Copyright 2016 Centrify Corporation. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Repo +Created by Centrify Corporation 2016-06-02. +Interface for accessing a repo. +""" + +import re +import sys +import imp +import os + +def Open(path, url, plugin): + #looks for installtion path for munki + # first look for a plugin in the same dir as us in munkilib/plugins + munkilib_path = os.path.dirname(os.path.abspath(__file__)) + munkilib_path = os.path.join(munkilib_path, 'plugins') + if not os.path.exists(munkilib_path): + # didn't find it; assume the default install path + command = "munkiimport" + commandPath = os.popen("/usr/bin/which %s" % command).read().strip() + commandPath = os.path.split(commandPath) + munkilib_path = commandPath[0] + #use default munki location if munki installation path is not found + if munkilib_path == None or munkilib_path == "": + munkilib_path = '/usr/local/munki/munkilib/plugins' + else: + munkilib_path = munkilib_path + '/munkilib/plugins' + #looks for plugin in /usr/local/munki/munkilib/plugins (installation of munki) + if plugin == None or plugin == "": + #default is FileRepo + plugin = 'FileRepo' + module = imp.load_source(plugin, munkilib_path + "/" + plugin + ".py") + import_class = getattr(module, plugin) + parent = import_class + + class Repo(parent): + mounted = False + + def available(self): + #if path does not exist, mount to local filesystem + if not self.exists(): + retcode = self.mount() + if retcode == 0: + self.mounted = True + #if path still doesn't exist, then cannot find munki_repo + if not self.exists(): + print >> sys.stderr, "repo is missing" + return False + #checks if all subdirectories are there + for subdir in ['catalogs', 'manifests', 'pkgs', 'pkgsinfo']: + if not self.exists(subdir): + print >> sys.stderr, "repo is missing %s" % subdir + return False + # if we get this far, the repo path looks OK + return True + + return Repo(path, url) diff --git a/code/client/munkilib/iconutils.py b/code/client/munkilib/iconutils.py index b5bde0cf..c6349808 100644 --- a/code/client/munkilib/iconutils.py +++ b/code/client/munkilib/iconutils.py @@ -165,42 +165,58 @@ def extractAppIconsFromFlatPkg(pkg_path): return icon_paths -def findInfoPlistPathsInBundlePkg(pkg_path): +def findInfoPlistPathsInBundlePkg(pkg_path, repo=None): '''Returns a dict with pkg paths as keys and filename lists as values''' pkg_dict = {} - bomfile = os.path.join(pkg_path, u'Contents/Archive.bom') + if repo: + repo_bomfile = repo.join(pkg_path, u'Contents/Archive.bom') + handle = repo.open(repo_bomfile, 'r') + bomfile = handle.local_path + else: + bomfile = os.path.join(pkg_path, u'Contents/Archive.bom') if os.path.exists(bomfile): - info_paths = getAppInfoPathsFromBundleComponentPkg(pkg_path) + info_paths = getAppInfoPathsFromBOM(bomfile) if info_paths: pkg_dict[pkg_path] = info_paths else: # mpkg or dist pkg; look for component pkgs within pkg_dict = {} - original_dir = os.getcwd() - pkg_contents_dir = os.path.join(pkg_path, u'Contents') - if os.path.isdir(pkg_contents_dir): - os.chdir(pkg_contents_dir) - pkgs = (glob.glob('*.pkg') + glob.glob('*/*.pkg') - + glob.glob('*/*/*.pkg') + glob.glob('*.mpkg') + - glob.glob('*/*.mpkg') + glob.glob('*/*/*.mpkg')) - os.chdir(original_dir) + pkgs = [] + if repo: + pkg_contents_dir = repo.join(pkg_path, u'Contents') + if repo.isdir(pkg_contents_dir): + pkgs = repo.glob(pkg_contents_dir, '*.pkg', '*/*.pkg', + '*/*/*.pkg', '*.mpkg', '*/*.mpkg', '*/*/*.mpkg') else: - pkgs = [] + pkg_contents_dir = os.path.join(pkg_path, u'Contents') + if os.path.isdir(pkg_contents_dir): + original_dir = os.getcwd() + os.chdir(pkg_contents_dir) + pkgs = (glob.glob('*.pkg') + glob.glob('*/*.pkg') + + glob.glob('*/*/*.pkg') + glob.glob('*.mpkg') + + glob.glob('*/*.mpkg') + glob.glob('*/*/*.mpkg')) + os.chdir(original_dir) for pkg in pkgs: full_path = os.path.join(pkg_contents_dir, pkg) pkg_dict.update(findInfoPlistPathsInBundlePkg(full_path)) return pkg_dict -def extractAppIconsFromBundlePkg(pkg_path): +def extractAppIconsFromBundlePkg(pkg_path, repo=None): '''Returns a list of paths for application icons found inside the bundle pkg at pkg_path''' - pkg_dict = findInfoPlistPathsInBundlePkg(pkg_path) + pkg_dict = findInfoPlistPathsInBundlePkg(pkg_path, repo) icon_paths = [] exporttmp = tempfile.mkdtemp(dir='/tmp') for pkg in pkg_dict: - archive_path = os.path.join(pkg, u'Contents/Archive.pax.gz') + handle = None + if repo: + repo_archive_path = repo.join(pkg, u'Contents/Archive.pax.gz') + handle = repo.open(repo_archive_path, 'r') + archive_path = handle.local_path + else: + archive_path = os.path.join(pkg, u'Contents/Archive.pax.gz') err = extractAppBitsFromPkgArchive(archive_path, exporttmp) if err == 0: for info_path in pkg_dict[pkg]: @@ -212,9 +228,8 @@ def extractAppIconsFromBundlePkg(pkg_path): return icon_paths -def getAppInfoPathsFromBundleComponentPkg(pkg_path): +def getAppInfoPathsFromBOM(bomfile): '''Returns a list of paths to application Info.plists''' - bomfile = os.path.join(pkg_path, u'Contents/Archive.bom') if os.path.exists(bomfile): cmd = ['/usr/bin/lsbom', '-s', bomfile] proc = subprocess.Popen(cmd, shell=False, bufsize=-1, diff --git a/code/client/munkilib/plugins/FileRepo.py b/code/client/munkilib/plugins/FileRepo.py new file mode 100644 index 00000000..eaabd4c7 --- /dev/null +++ b/code/client/munkilib/plugins/FileRepo.py @@ -0,0 +1,280 @@ +#!/usr/bin/python +# encoding: utf-8 +# +# Copyright 2016 Centrify Corporation. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +FileRepo +Created by Centrify Corporation 2016-06-02. +Implementation for accessing a repo via direct file access, including +a remote repo mounted via AFP, SMB, or NFS. +""" + +from munkilib.munkicommon import listdir +import os +import sys +import subprocess +import objc +import glob + +# NetFS share mounting code borrowed and liberally adapted from Michael Lynn's +# work here: https://gist.github.com/pudquick/1362a8908be01e23041d +try: + import errno + import getpass + from CoreFoundation import CFURLCreateWithString + + class Attrdict(dict): + '''Custom dict class''' + __getattr__ = dict.__getitem__ + __setattr__ = dict.__setitem__ + + NetFS = Attrdict() + # Can cheat and provide 'None' for the identifier, it'll just use + # frameworkPath instead + # scan_classes=False means only add the contents of this Framework + NetFS_bundle = objc.initFrameworkWrapper( + 'NetFS', frameworkIdentifier=None, + frameworkPath=objc.pathForFramework('NetFS.framework'), + globals=NetFS, scan_classes=False) + + # https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ + # ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html + # Fix NetFSMountURLSync signature + del NetFS['NetFSMountURLSync'] + objc.loadBundleFunctions( + NetFS_bundle, NetFS, [('NetFSMountURLSync', 'i@@@@@@o^@')]) + NETFSMOUNTURLSYNC_AVAILABLE = True +except (ImportError, KeyError): + NETFSMOUNTURLSYNC_AVAILABLE = False + +if NETFSMOUNTURLSYNC_AVAILABLE: + class ShareMountException(Exception): + '''An exception raised if share mounting failed''' + pass + + + class ShareAuthenticationNeededException(ShareMountException): + '''An exception raised if authentication is needed''' + pass + + + def mount_share(share_url): + '''Mounts a share at /Volumes, returns the mount point or raises an + error''' + sh_url = CFURLCreateWithString(None, share_url, None) + # Set UI to reduced interaction + open_options = {NetFS.kNAUIOptionKey: NetFS.kNAUIOptionNoUI} + # Allow mounting sub-directories of root shares + mount_options = {NetFS.kNetFSAllowSubMountsKey: True} + # Mount! + result, output = NetFS.NetFSMountURLSync( + sh_url, None, None, None, open_options, mount_options, None) + # Check if it worked + if result != 0: + if result in (errno.ENOTSUP, errno.EAUTH): + # errno.ENOTSUP is returned if an afp share needs a login + # errno.EAUTH is returned if authentication fails (SMB for sure) + raise ShareAuthenticationNeededException() + raise ShareMountException( + 'Error mounting url "%s": %s, error %s' + % (share_url, os.strerror(result), result)) + # Return the mountpath + return str(output[0]) + + + def mount_share_with_credentials(share_url, username, password): + '''Mounts a share at /Volumes, returns the mount point or raises an + error. Include username and password as parameters, not in the + share_path URL''' + sh_url = CFURLCreateWithString(None, share_url, None) + # Set UI to reduced interaction + open_options = {NetFS.kNAUIOptionKey: NetFS.kNAUIOptionNoUI} + # Allow mounting sub-directories of root shares + mount_options = {NetFS.kNetFSAllowSubMountsKey: True} + # Mount! + result, output = NetFS.NetFSMountURLSync( + sh_url, None, username, password, open_options, mount_options, None) + # Check if it worked + if result != 0: + raise ShareMountException( + 'Error mounting url "%s": %s, error %s' + % (share_url, os.strerror(result), result)) + # Return the mountpath + return str(output[0]) + + + def mount_share_url(share_url): + '''Mount a share url under /Volumes, prompting for password if needed + Raises ShareMountException if there's an error''' + try: + mount_share(share_url) + except ShareAuthenticationNeededException: + username = raw_input('Username: ') + password = getpass.getpass() + mount_share_with_credentials(share_url, username, password) + +class FileRepo(object): + WE_MOUNTED_THE_REPO = False + '''Repo implementation that access a local or locally-mounted repo.''' + def __init__(self, path, url): + self.path = path + self.url = url + + def exists(self, subdir = None): + '''Returns true if the specified path exists in the repo''' + full_path = self.path + if subdir: + full_path = os.path.join(full_path, subdir) + return os.path.exists(full_path) + + def isdir(self, path): + '''Returns true if the specified path exists in the repo + and is a directory.''' + return os.path.isdir(os.path.join(self.path, path)) + + def isfile(self, path): + '''Returns true if the specified path exists in the repo + and is a regular file.''' + return os.path.isfile(os.path.join(self.path, path)) + + def join(self, *args): + '''Combines path elements within the repo.''' + return os.path.join(*args) + + def dirname(self, path): + '''Returns the directory portion of a path.''' + return os.path.dirname(path) + + def basename(self, path): + '''Returns the filename portion of a path.''' + return os.path.basename(path) + + def splitext(self, path): + '''Splits the base and extention parts of a path.''' + return os.path.splitext(path) + + def mkdir(self, path, mode=0777): + '''Creates a directory within the repo.''' + return os.mkdir(os.path.join(self.path, path), mode) + + def makedirs(self, path, mode=0777): + '''Creates a directory within the repo, including parent directories.''' + return os.makedirs(os.path.join(self.path, path), mode) + + def listdir(self, path): + '''Lists the contents of a repo directory.''' + return listdir(os.path.join(self.path, path)) + + def remove(self, path): + '''Removes a file from the repo.''' + return os.remove(os.path.join(self.path, path)) + + def unlink(self, path): + '''Removes a file from the repo.''' + return os.unlink(os.path.join(self.path, path)) + + def get(self, src, dest): + '''Copies a file from the repo to a local file.''' + cmd = ['/bin/cp', os.path.join(self.path, src), dest] + return subprocess.call(cmd) + + def put(self, src, dest): + '''Copies a local file to the repo.''' + cmd = ['/bin/cp', src, os.path.join(self.path, dest)] + return subprocess.call(cmd) + + # + # Some callers open a file, but then use the local_path field + # to access it rather than reading or writing through the returned + # handle. For local repos those callers could just use the + # file name directly rather than opening it through this method, + # but for the CommandRepo implementation the local_path field + # will be a local temporary file that was copied from the remote + # repo and/or will be copied to the remote repo on close. + # + def open(self, path, mode='r'): + '''Opens a file in the repo.''' + class RepoFile(object): + def __init__(self, repo, repo_path, mode): + self.repo = repo + self.repo_path = repo_path + self.repo_mode = mode + self.file = open(self.repo_path, mode) + self.local_path = self.repo_path + + def read(self): + return self.file.read() + + return RepoFile(self, os.path.join(self.path, path), mode) + + def mount(self): + '''Mounts the repo locally.''' + if os.path.exists(self.path): + return + print 'Attempting to mount fileshare %s:' % self.url + if NETFSMOUNTURLSYNC_AVAILABLE: + try: + mount_share_url(self.url) + except ShareMountException, err: + print sys.stderr, err + return + else: + self.WE_MOUNTED_THE_REPO = True + return 0 + else: + os.mkdir(self.path) + if self.url.startswith('afp:'): + cmd = ['/sbin/mount_afp', '-i', self.url, self.path] + elif self.url.startswith('smb:'): + cmd = ['/sbin/mount_smbfs', self.url[4:], self.path] + elif self.url.startswith('nfs://'): + cmd = ['/sbin/mount_nfs', self.url[6:], self.path] + else: + print >> sys.stderr, 'Unsupported filesystem URL!' + return + retcode = subprocess.call(cmd) + if retcode: + os.rmdir(self.path) + else: + self.WE_MOUNTED_THE_REPO = True + return retcode + + def unmount(self): + '''Unmounts the repo.''' + if not os.path.exists(self.path): + return + retcode = 0 + if os.path.exists(self.path): + cmd = ['/sbin/umount', self.path] + retcode = subprocess.call(cmd) + return retcode + + def walk(self, path, **kwargs): + '''Walks a path in the repo, returning all files and subdirectories. + Only a subset of the features of os.walk() are supported.''' + for (dirpath, dirnames, filenames) in os.walk(os.path.join(self.path, path), **kwargs): + dirpath = dirpath[len(self.path) + 1:] + yield (dirpath, dirnames, filenames) + + def glob(self, path, *args): + '''Expands a set of glob patterns within a repo path.''' + matches = [] + original_dir = os.getcwd() + os.chdir(path) + for arg in args: + matches += glob.glob(arg) + os.chdir(original_dir) + return matches