Merge branch 'cloudrepo' into Munki3

This commit is contained in:
Greg Neagle
2017-02-28 13:24:16 -08:00
7 changed files with 650 additions and 319 deletions
+60 -37
View File
@@ -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()
+44 -33
View File
@@ -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()
+39 -91
View File
@@ -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',
+121 -141
View File
@@ -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
+74
View File
@@ -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)
+32 -17
View File
@@ -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,
+280
View File
@@ -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