mirror of
https://github.com/munki/munki.git
synced 2026-05-08 05:19:31 -05:00
munkiimport working with new-style repo plugins
This commit is contained in:
+160
-263
@@ -23,8 +23,7 @@ Created by Greg Neagle on 2010-09-29.
|
||||
Assists with importing installer items into the munki repo
|
||||
"""
|
||||
import ctypes
|
||||
import errno
|
||||
import getpass
|
||||
from ctypes.util import find_library
|
||||
import os
|
||||
import readline
|
||||
import subprocess
|
||||
@@ -32,11 +31,8 @@ import sys
|
||||
import time
|
||||
import thread
|
||||
|
||||
from ctypes.util import find_library
|
||||
from optparse import OptionParser, BadOptionError, AmbiguousOptionError
|
||||
|
||||
import objc
|
||||
|
||||
from munkilib import iconutils
|
||||
from munkilib import info
|
||||
from munkilib import display
|
||||
@@ -51,106 +47,12 @@ from munkilib import FoundationPlist
|
||||
# PyLint cannot properly find names inside Cocoa libraries, so issues bogus
|
||||
# No name 'Foo' in module 'Bar' warnings. Disable them.
|
||||
# pylint: disable=E0611
|
||||
from CoreFoundation import CFURLCreateWithString
|
||||
from Foundation import CFPreferencesAppSynchronize
|
||||
from Foundation import CFPreferencesCopyAppValue
|
||||
from Foundation import CFPreferencesSetAppValue
|
||||
# pylint: enable=E0611
|
||||
|
||||
|
||||
# NetFS share mounting code borrowed and liberally adapted from Michael Lynn's
|
||||
# work here: https://gist.github.com/pudquick/1362a8908be01e23041d
|
||||
try:
|
||||
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
|
||||
|
||||
|
||||
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 (-6600, errno.EINVAL, errno.ENOTSUP, errno.EAUTH):
|
||||
# -6600 is kNetAuthErrorInternal in NetFS.h 10.9+
|
||||
# errno.EINVAL is returned if an afp share needs a login in some
|
||||
# versions of OS X
|
||||
# errno.ENOTSUP is returned if an afp share needs a login in some
|
||||
# versions of OS X
|
||||
# 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)
|
||||
|
||||
if 'libedit' in readline.__doc__:
|
||||
# readline module was compiled against libedit
|
||||
LIBEDIT = ctypes.cdll.LoadLibrary(find_library('libedit'))
|
||||
@@ -220,10 +122,10 @@ def make_dmg(pkgpath):
|
||||
"""Wraps a non-flat package into a disk image.
|
||||
Returns path to newly-created disk image."""
|
||||
|
||||
pkgname = repo.basename(pkgpath)
|
||||
pkgname = os.path.basename(pkgpath)
|
||||
print 'Making disk image containing %s...' % pkgname
|
||||
diskimagename = repo.splitext(pkgname)[0] + '.dmg'
|
||||
diskimagepath = repo.join(osutils.tmpdir(), diskimagename)
|
||||
diskimagename = os.path.splitext(pkgname)[0] + '.dmg'
|
||||
diskimagepath = os.path.join(osutils.tmpdir(), diskimagename)
|
||||
cmd = ['/usr/bin/hdiutil', 'create', '-srcfolder', pkgpath, diskimagepath]
|
||||
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
@@ -244,74 +146,74 @@ def make_dmg(pkgpath):
|
||||
|
||||
|
||||
class RepoCopyError(Exception):
|
||||
"""Error copying installer item to repo"""
|
||||
'''Exception raised when copying a file to the repo fails'''
|
||||
pass
|
||||
|
||||
def list_items_of_kind(repo, kind):
|
||||
'''Returns a list of items of kind. Relative pathnames are prepended
|
||||
with kind. (example: ['icons/Bar.png', 'icons/Foo.png'])'''
|
||||
return [os.path.join(kind, item) for item in repo.itemlist(kind)]
|
||||
|
||||
def copy_item_to_repo(itempath, vers, subdirectory=''):
|
||||
|
||||
def copy_item_to_repo(repo, itempath, vers, subdirectory=''):
|
||||
"""Copies an item to the appropriate place in the repo.
|
||||
If itempath is a path within the repo/pkgs directory, copies nothing.
|
||||
Renames the item if an item already exists with that name.
|
||||
Returns the relative path to the item."""
|
||||
|
||||
if not os.path.exists(REPO_PATH):
|
||||
raise RepoCopyError('Could not connect to munki repo.')
|
||||
|
||||
destination_path = repo.join('pkgs', subdirectory)
|
||||
if not repo.exists(destination_path):
|
||||
try:
|
||||
repo.makedirs(destination_path)
|
||||
except OSError, errmsg:
|
||||
raise RepoCopyError('Could not create %s: %s'
|
||||
% (destination_path, errmsg))
|
||||
|
||||
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 repo.join(subdirectory, item_name)
|
||||
destination_path = os.path.join('pkgs', subdirectory)
|
||||
item_name = os.path.basename(itempath)
|
||||
destination_path_name = os.path.join(destination_path, item_name)
|
||||
|
||||
name, ext = os.path.splitext(item_name)
|
||||
if vers:
|
||||
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 = repo.join(destination_path, item_name)
|
||||
destination_path_name = os.path.join(destination_path, item_name)
|
||||
|
||||
index = 0
|
||||
name, ext = repo.splitext(item_name)
|
||||
while repo.exists(destination_path_name):
|
||||
try:
|
||||
pkgs_list = list_items_of_kind(repo, 'pkgs')
|
||||
except munkirepo.RepoError, err:
|
||||
raise RepoCopyError(u'Unable to get list of current pkgs: %s'
|
||||
% unicode(err))
|
||||
while destination_path_name in pkgs_list:
|
||||
print 'File %s already exists...' % destination_path_name
|
||||
# try appending numbers until we have a unique name
|
||||
index += 1
|
||||
item_name = '%s__%s%s' % (name, index, ext)
|
||||
destination_path_name = repo.join(destination_path, item_name)
|
||||
destination_path_name = os.path.join(destination_path, item_name)
|
||||
|
||||
print 'Copying %s to %s...' % (repo.basename(itempath),
|
||||
print 'Copying %s to %s...' % (os.path.basename(itempath),
|
||||
destination_path_name)
|
||||
|
||||
retcode = repo.put(itempath, destination_path_name)
|
||||
if retcode:
|
||||
raise RepoCopyError('Unable to copy %s to %s'
|
||||
% (itempath, destination_path_name))
|
||||
try:
|
||||
repo.put_from_local_file(destination_path_name, itempath)
|
||||
except munkirepo.RepoError, err:
|
||||
raise RepoCopyError(u'Unable to copy %s to %s: %s'
|
||||
% (itempath, destination_path_name, unicode(err)))
|
||||
else:
|
||||
return repo.join(subdirectory, item_name)
|
||||
return os.path.join(subdirectory, item_name)
|
||||
|
||||
|
||||
def get_icon_path(pkginfo):
|
||||
"""Return path for icon"""
|
||||
icon_name = pkginfo.get('icon_name') or pkginfo['name']
|
||||
if not repo.splitext(icon_name)[1]:
|
||||
if not os.path.splitext(icon_name)[1]:
|
||||
icon_name += u'.png'
|
||||
return repo.join(u'icons', icon_name)
|
||||
return os.path.join(u'icons', icon_name)
|
||||
|
||||
|
||||
def icon_exists_in_repo(pkginfo):
|
||||
def icon_exists_in_repo(repo, pkginfo):
|
||||
"""Returns True if there is an icon for this item in the repo"""
|
||||
icon_path = get_icon_path(pkginfo)
|
||||
if repo.exists(icon_path):
|
||||
try:
|
||||
icon_list = list_items_of_kind(repo, 'icons')
|
||||
except munkirepo.RepoError, err:
|
||||
raise RepoCopyError(u'Unable to get list of current icons: %s'
|
||||
% unicode(err))
|
||||
if icon_path in icon_list:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -319,11 +221,11 @@ 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 repo.isfile(icon_path):
|
||||
if os.path.isfile(icon_path):
|
||||
pkginfo['icon_hash'] = munkihash.getsha256hash(icon_path)
|
||||
|
||||
|
||||
def generate_png_from_copy_from_dmg_item(dmg_path, pkginfo):
|
||||
def generate_png_from_copy_from_dmg_item(repo, dmg_path, pkginfo):
|
||||
'''Generates a product icon from a copy_from_dmg item
|
||||
and uploads to the repo'''
|
||||
mountpoints = dmgutils.mountdmg(dmg_path)
|
||||
@@ -335,7 +237,7 @@ def generate_png_from_copy_from_dmg_item(dmg_path, pkginfo):
|
||||
app_path = os.path.join(mountpoint, apps[0]['source_item'])
|
||||
icon_path = iconutils.findIconForApp(app_path)
|
||||
if icon_path:
|
||||
convert_and_install_icon(pkginfo, icon_path)
|
||||
convert_and_install_icon(repo, pkginfo, icon_path)
|
||||
else:
|
||||
print 'No application icons found.'
|
||||
else:
|
||||
@@ -343,7 +245,7 @@ def generate_png_from_copy_from_dmg_item(dmg_path, pkginfo):
|
||||
dmgutils.unmountdmg(mountpoint)
|
||||
|
||||
|
||||
def generate_pngs_from_installer_pkg(item_path, pkginfo):
|
||||
def generate_pngs_from_installer_pkg(repo, item_path, pkginfo):
|
||||
'''Generates a product icon (or candidate icons) from
|
||||
an installer pkg and uploads to the repo'''
|
||||
icon_paths = []
|
||||
@@ -374,101 +276,98 @@ def generate_pngs_from_installer_pkg(item_path, pkginfo):
|
||||
dmgutils.unmountdmg(mountpoint)
|
||||
|
||||
if len(icon_paths) == 1:
|
||||
convert_and_install_icon(pkginfo, icon_paths[0])
|
||||
convert_and_install_icon(repo, pkginfo, icon_paths[0])
|
||||
elif len(icon_paths) > 1:
|
||||
index = 1
|
||||
for icon_path in icon_paths:
|
||||
convert_and_install_icon(pkginfo, icon_path, index=index)
|
||||
convert_and_install_icon(repo, pkginfo, icon_path, index=index)
|
||||
index += 1
|
||||
else:
|
||||
print 'No application icons found.'
|
||||
|
||||
|
||||
def convert_and_install_icon(pkginfo, icon_path, index=None):
|
||||
def convert_and_install_icon(repo, pkginfo, icon_path, index=None):
|
||||
'''Convert icon file to png and save to repo icon path'''
|
||||
destination_path = 'icons'
|
||||
if not repo.exists(destination_path):
|
||||
try:
|
||||
repo.makedirs(destination_path)
|
||||
except OSError, errmsg:
|
||||
print >> sys.stderr, ('Could not create %s: %s' %
|
||||
(destination_path, errmsg))
|
||||
|
||||
if index is not None:
|
||||
destination_name = pkginfo['name'] + '_' + str(index)
|
||||
else:
|
||||
destination_name = pkginfo['name']
|
||||
|
||||
png_name = destination_name + u'.png'
|
||||
png_path = repo.join(destination_path, png_name)
|
||||
png_tmp = repo.join(osutils.tmpdir(), png_name)
|
||||
result = iconutils.convertIconToPNG(icon_path, png_tmp)
|
||||
repo_png_path = os.path.join(destination_path, png_name)
|
||||
local_png_tmp = os.path.join(osutils.tmpdir(), png_name)
|
||||
result = iconutils.convertIconToPNG(icon_path, local_png_tmp)
|
||||
if result:
|
||||
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
|
||||
try:
|
||||
repo.put_from_local_file(repo_png_path, local_png_tmp)
|
||||
print 'Created icon: %s' % repo_png_path
|
||||
except munkirepo.RepoError, err:
|
||||
print >> sys.stderr, (u'Error uploading icon to %s: %s'
|
||||
% (repo_png_path, unicode(err)))
|
||||
else:
|
||||
print >> sys.stderr, u'Error converting %s to png.' % icon_path
|
||||
|
||||
|
||||
def copy_icon_to_repo(iconpath):
|
||||
def copy_icon_to_repo(repo, iconpath):
|
||||
"""Saves a product icon to the repo"""
|
||||
destination_path = 'icons'
|
||||
if not repo.exists(destination_path):
|
||||
try:
|
||||
repo.makedirs(destination_path)
|
||||
except OSError, errmsg:
|
||||
raise RepoCopyError('Could not create %s: %s'
|
||||
% (destination_path, errmsg))
|
||||
icon_name = repo.basename(iconpath)
|
||||
destination_path_name = repo.join(destination_path, icon_name)
|
||||
icon_name = os.path.basename(iconpath)
|
||||
destination_path_name = os.path.join(destination_path, icon_name)
|
||||
|
||||
if repo.exists(destination_path_name):
|
||||
try:
|
||||
icon_list = list_items_of_kind(repo, 'icons')
|
||||
except munkirepo.RepoError, err:
|
||||
raise RepoCopyError(u'Unable to get list of current icons: %s'
|
||||
% unicode(err))
|
||||
if destination_path_name in icon_list:
|
||||
# remove any existing icon in the repo
|
||||
try:
|
||||
repo.unlink(destination_path_name)
|
||||
except OSError, errmsg:
|
||||
raise RepoCopyError('Could not remove existing %s'
|
||||
% (destination_path_name))
|
||||
repo.delete(destination_path_name)
|
||||
except munkirepo.RepoError, err:
|
||||
raise RepoCopyError('Could not remove existing %s: %s'
|
||||
% (destination_path_name, unicode(err)))
|
||||
print 'Copying %s to %s...' % (icon_name, destination_path_name)
|
||||
retcode = repo.put(iconpath, destination_path_name)
|
||||
if retcode:
|
||||
raise RepoCopyError('Unable to copy %s to %s'
|
||||
% (iconpath, destination_path_name))
|
||||
try:
|
||||
repo.put_from_local_file(destination_path_name, iconpath)
|
||||
except munkirepo.RepoError, err:
|
||||
raise RepoCopyError('Unable to copy %s to %s: %s'
|
||||
% (iconpath, destination_path_name, unicode(err)))
|
||||
|
||||
|
||||
def copy_pkginfo_to_repo(pkginfo, subdirectory=''):
|
||||
"""Saves pkginfo to munki_repo_path/pkgsinfo/subdirectory"""
|
||||
def copy_pkginfo_to_repo(repo, pkginfo, subdirectory=''):
|
||||
"""Saves pkginfo to <munki_repo>/pkgsinfo/subdirectory"""
|
||||
# less error checking because we copy the installer_item
|
||||
# first and bail if it fails...
|
||||
destination_path = repo.join(repo.root, 'pkgsinfo', subdirectory)
|
||||
if not repo.exists(destination_path):
|
||||
try:
|
||||
repo.makedirs(destination_path)
|
||||
except OSError, errmsg:
|
||||
raise RepoCopyError('Could not create %s: %s'
|
||||
% (destination_path, errmsg))
|
||||
destination_path = os.path.join('pkgsinfo', subdirectory)
|
||||
pkginfo_ext = pref('pkginfo_extension') or ''
|
||||
if pkginfo_ext and not pkginfo_ext.startswith('.'):
|
||||
pkginfo_ext = '.' + pkginfo_ext
|
||||
pkginfo_name = '%s-%s%s' % (pkginfo['name'], pkginfo['version'],
|
||||
pkginfo_ext)
|
||||
pkginfo_path = repo.join(destination_path, pkginfo_name)
|
||||
pkginfo_path = os.path.join(destination_path, pkginfo_name)
|
||||
index = 0
|
||||
while repo.exists(pkginfo_path):
|
||||
try:
|
||||
pkgsinfo_list = list_items_of_kind(repo, 'pkgsinfo')
|
||||
except munkirepo.RepoError, err:
|
||||
raise RepoCopyError(u'Unable to get list of current pkgsinfo: %s'
|
||||
% unicode(err))
|
||||
while pkginfo_path in pkgsinfo_list:
|
||||
index += 1
|
||||
pkginfo_name = '%s-%s__%s%s' % (pkginfo['name'], pkginfo['version'],
|
||||
index, pkginfo_ext)
|
||||
pkginfo_path = repo.join(destination_path, pkginfo_name)
|
||||
pkginfo_path = os.path.join(destination_path, pkginfo_name)
|
||||
|
||||
print 'Saving pkginfo to %s...' % pkginfo_path
|
||||
try:
|
||||
handle = repo.open(pkginfo_path, 'w')
|
||||
FoundationPlist.writePlist(pkginfo, handle.local_path)
|
||||
pkginfo_str = FoundationPlist.writePlistToString(pkginfo)
|
||||
except FoundationPlist.NSPropertyListWriteException, errmsg:
|
||||
raise RepoCopyError(errmsg)
|
||||
try:
|
||||
repo.put(pkginfo_path, pkginfo_str)
|
||||
except munkirepo.RepoError, err:
|
||||
raise RepoCopyError('Unable to save pkginfo to %s: %s'
|
||||
% (pkginfo_path, unicode(err)))
|
||||
return pkginfo_path
|
||||
|
||||
|
||||
@@ -487,19 +386,24 @@ def open_pkginfo_in_editor(pkginfo_path):
|
||||
'Problem running editor %s: %s.' % (editor, err))
|
||||
|
||||
|
||||
def prompt_for_subdirectory(subdirectory):
|
||||
def prompt_for_subdirectory(repo, subdirectory):
|
||||
"""Prompts the user for a subdirectory for the pkg and pkginfo"""
|
||||
try:
|
||||
pkgsinfo_list = list_items_of_kind(repo, 'pkgsinfo')
|
||||
except munkirepo.RepoError, err:
|
||||
raise RepoCopyError(u'Unable to get list of current pkgsinfo: %s'
|
||||
% unicode(err))
|
||||
# filter the list of pkgsinfo to a list of subdirectories
|
||||
subdir_set = set()
|
||||
for item in pkgsinfo_list:
|
||||
subdir_set.add(os.path.dirname(item))
|
||||
|
||||
while True:
|
||||
newdir = raw_input(
|
||||
'Upload item to subdirectory path [%s]: ' % subdirectory)
|
||||
if newdir:
|
||||
if not repo.available():
|
||||
raise RepoCopyError('Could not connect to munki repo.')
|
||||
if APPLEMETADATA:
|
||||
destination_path = repo.join('pkgsinfo', newdir)
|
||||
else:
|
||||
destination_path = repo.join('pkgs', newdir)
|
||||
if not repo.exists(destination_path):
|
||||
destination_path = os.path.join('pkgsinfo', newdir)
|
||||
if destination_path not in subdir_set:
|
||||
answer = raw_input('Path %s doesn\'t exist. Create it? [y/n] '
|
||||
% destination_path)
|
||||
if answer.lower().startswith('y'):
|
||||
@@ -516,21 +420,18 @@ class CatalogDBException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def make_catalog_db():
|
||||
def make_catalog_db(repo):
|
||||
"""Returns a dict we can use like a database"""
|
||||
|
||||
all_items_path = repo.join('catalogs', 'all')
|
||||
handle = None
|
||||
try:
|
||||
plist = repo.get('catalogs/all')
|
||||
except munkirepo.RepoError, err:
|
||||
raise CatalogDBException(err)
|
||||
|
||||
try:
|
||||
handle = repo.open(all_items_path, 'r')
|
||||
except IOError:
|
||||
raise CatalogDBException
|
||||
|
||||
try:
|
||||
catalogitems = FoundationPlist.readPlist(handle.local_path)
|
||||
except FoundationPlist.NSPropertyListSerializationException:
|
||||
raise CatalogDBException
|
||||
catalogitems = FoundationPlist.readPlistFromString(plist)
|
||||
except FoundationPlist.NSPropertyListSerializationException, err:
|
||||
raise CatalogDBException(err)
|
||||
|
||||
pkgid_table = {}
|
||||
app_table = {}
|
||||
@@ -555,9 +456,9 @@ def make_catalog_db():
|
||||
|
||||
# add to installer item table
|
||||
if 'installer_item_location' in item:
|
||||
installer_item_name = repo.basename(
|
||||
installer_item_name = os.path.basename(
|
||||
item['installer_item_location'])
|
||||
(name, ext) = repo.splitext(installer_item_name)
|
||||
(name, ext) = os.path.splitext(installer_item_name)
|
||||
if '-' in name:
|
||||
(name, vers) = pkgutils.nameAndVersion(name)
|
||||
installer_item_name = name + ext
|
||||
@@ -617,7 +518,7 @@ def make_catalog_db():
|
||||
return pkgdb
|
||||
|
||||
|
||||
def find_matching_pkginfo(pkginfo):
|
||||
def find_matching_pkginfo(repo, pkginfo):
|
||||
"""Looks through repo catalogs looking for matching pkginfo
|
||||
Returns a pkginfo dictionary, or an empty dict"""
|
||||
|
||||
@@ -627,8 +528,10 @@ def find_matching_pkginfo(pkginfo):
|
||||
pkgutils.MunkiLooseVersion(value_a))
|
||||
|
||||
try:
|
||||
catdb = make_catalog_db()
|
||||
except CatalogDBException:
|
||||
catdb = make_catalog_db(repo)
|
||||
except CatalogDBException, err:
|
||||
print (u'Could not get a list of existing items from the repo: %s'
|
||||
% unicode(err))
|
||||
return {}
|
||||
|
||||
if 'installer_item_hash' in pkginfo:
|
||||
@@ -681,7 +584,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 = repo.basename(
|
||||
installer_item_name = os.path.basename(
|
||||
pkginfo.get('installer_item_location', ''))
|
||||
possiblematches = catdb['installer_items'].get(installer_item_name)
|
||||
if possiblematches:
|
||||
@@ -742,13 +645,11 @@ 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 = repo.dirname(os.path.abspath(__file__))
|
||||
mydir = os.path.dirname(os.path.abspath(__file__))
|
||||
makecatalogs_path = os.path.join(mydir, 'makecatalogs')
|
||||
if not repo.exists(makecatalogs_path):
|
||||
if not os.path.exists(makecatalogs_path):
|
||||
# didn't find it; assume the default install path
|
||||
makecatalogs_path = '/usr/local/munki/makecatalogs'
|
||||
if not repo.available():
|
||||
raise RepoCopyError('Could not connect to munki repo.')
|
||||
if not VERBOSE:
|
||||
print 'Rebuilding catalogs at %s...' % REPO_PATH
|
||||
cmd = [makecatalogs_path]
|
||||
@@ -777,13 +678,13 @@ def make_catalogs():
|
||||
def cleanup_and_exit(exitcode):
|
||||
"""Unmounts the repo if we mounted it, then exits"""
|
||||
result = 0
|
||||
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 = repo.unmount()
|
||||
else:
|
||||
result = repo.unmount()
|
||||
#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 = repo.unmount()
|
||||
# else:
|
||||
# result = repo.unmount()
|
||||
# clean up tmpdir
|
||||
osutils.cleanUpTmpDir()
|
||||
|
||||
@@ -852,7 +753,7 @@ NOINTERACTIVE = False
|
||||
VERBOSE = False
|
||||
REPO_PATH = ""
|
||||
REPO_URL = ""
|
||||
repo = None
|
||||
REPO_PLUGIN = ""
|
||||
|
||||
def main():
|
||||
"""Main routine"""
|
||||
@@ -862,7 +763,6 @@ def main():
|
||||
global REPO_PATH
|
||||
global REPO_URL
|
||||
global REPO_PLUGIN
|
||||
global repo
|
||||
|
||||
usage = """usage: %prog [options] /path/to/installer_item
|
||||
Imports an installer item into a munki repo.
|
||||
@@ -956,8 +856,7 @@ def main():
|
||||
|
||||
# Verify that arguments, presumed to be for
|
||||
# 'makepkginfo' are valid and return installer_item
|
||||
return_dict = make_pkginfo(
|
||||
options=arguments, test_mode=True)
|
||||
return_dict = make_pkginfo(options=arguments, test_mode=True)
|
||||
try:
|
||||
return_dict = FoundationPlist.readPlistFromString(return_dict)
|
||||
except FoundationPlist.FoundationPlistException, err:
|
||||
@@ -994,20 +893,15 @@ def main():
|
||||
print >> sys.stderr, '%s does not exist!' % installer_item
|
||||
exit(-1)
|
||||
|
||||
if not REPO_PATH:
|
||||
print >> sys.stderr, ('Path to munki repo has not been defined. '
|
||||
'Run with --configure option to configure this '
|
||||
'tool, or provide with --repo-path')
|
||||
exit(-1)
|
||||
|
||||
repo = munkirepo.connect(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.')
|
||||
try:
|
||||
repo = munkirepo.connect(REPO_URL, REPO_PLUGIN)
|
||||
except munkirepo.RepoError, err:
|
||||
print >> sys.stderr, (u'Could not connect to munki repo: %s'
|
||||
% unicode(err))
|
||||
exit(-1)
|
||||
|
||||
if not APPLEMETADATA:
|
||||
if repo.isdir(installer_item): # Start of indent
|
||||
if os.path.isdir(installer_item): # Start of indent
|
||||
if pkgutils.hasValidDiskImageExt(installer_item):
|
||||
# a directory named foo.dmg or foo.iso!
|
||||
print >> sys.stderr, '%s is an unknown type.' % installer_item
|
||||
@@ -1028,7 +922,7 @@ def main():
|
||||
arguments.append(installer_item) # End of indent
|
||||
|
||||
if uninstaller_item:
|
||||
if repo.isdir(uninstaller_item):
|
||||
if os.path.isdir(uninstaller_item):
|
||||
if pkgutils.hasValidDiskImageExt(uninstaller_item):
|
||||
# a directory named foo.dmg or foo.iso!
|
||||
print >> sys.stderr, (
|
||||
@@ -1057,7 +951,7 @@ def main():
|
||||
cleanup_and_exit(-1)
|
||||
if not options.nointeractive:
|
||||
# try to find existing pkginfo items that match this one
|
||||
matchingpkginfo = find_matching_pkginfo(pkginfo)
|
||||
matchingpkginfo = find_matching_pkginfo(repo, pkginfo)
|
||||
exactmatch = False
|
||||
if matchingpkginfo:
|
||||
if ('installer_item_hash' in matchingpkginfo and
|
||||
@@ -1161,17 +1055,16 @@ def main():
|
||||
cleanup_and_exit(0)
|
||||
|
||||
if options.subdirectory == '':
|
||||
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 = repo.dirname(installer_item)
|
||||
options.subdirectory = \
|
||||
installer_item_dirpath[len(pkgs_path)+1:]
|
||||
#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)
|
||||
# options.subdirectory = installer_item_dirpath[len(pkgs_path)+1:]
|
||||
options.subdirectory = prompt_for_subdirectory(
|
||||
options.subdirectory)
|
||||
repo, options.subdirectory)
|
||||
|
||||
if (not icon_exists_in_repo(pkginfo) and not options.icon_path
|
||||
if (not icon_exists_in_repo(repo, pkginfo) and not options.icon_path
|
||||
and not APPLEMETADATA
|
||||
and not pkginfo.get('installer_type') == 'profile'):
|
||||
print 'No existing product icon found.'
|
||||
@@ -1181,9 +1074,10 @@ def main():
|
||||
installer_type = pkginfo.get('installer_type')
|
||||
if installer_type == 'copy_from_dmg':
|
||||
generate_png_from_copy_from_dmg_item(
|
||||
installer_item, pkginfo)
|
||||
repo, installer_item, pkginfo)
|
||||
elif installer_type in [None, '']:
|
||||
generate_pngs_from_installer_pkg(installer_item, pkginfo)
|
||||
generate_pngs_from_installer_pkg(
|
||||
repo, installer_item, pkginfo)
|
||||
else:
|
||||
print >> sys.stderr, (
|
||||
'Can\'t generate icons from installer_type: %s.'
|
||||
@@ -1195,7 +1089,7 @@ def main():
|
||||
|
||||
if not APPLEMETADATA:
|
||||
try:
|
||||
uploaded_pkgpath = copy_item_to_repo(installer_item,
|
||||
uploaded_pkgpath = copy_item_to_repo(repo, installer_item,
|
||||
pkginfo.get('version'),
|
||||
options.subdirectory)
|
||||
except RepoCopyError, errmsg:
|
||||
@@ -1208,7 +1102,7 @@ def main():
|
||||
|
||||
if uninstaller_item:
|
||||
try:
|
||||
uploaded_pkgpath = copy_item_to_repo(uninstaller_item,
|
||||
uploaded_pkgpath = copy_item_to_repo(repo, uninstaller_item,
|
||||
pkginfo.get('version'),
|
||||
options.subdirectory)
|
||||
except RepoCopyError, errmsg:
|
||||
@@ -1218,7 +1112,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(repo.getsize(uninstaller_item))
|
||||
itemsize = int(os.path.getsize(uninstaller_item))
|
||||
itemhash = munkihash.getsha256hash(uninstaller_item)
|
||||
pkginfo['uninstaller_item_size'] = int(itemsize/1024)
|
||||
pkginfo['uninstaller_item_hash'] = itemhash
|
||||
@@ -1226,23 +1120,26 @@ def main():
|
||||
# if we have an icon, upload it
|
||||
if options.icon_path:
|
||||
try:
|
||||
convert_and_install_icon(pkginfo, options.icon_path)
|
||||
convert_and_install_icon(repo, pkginfo, options.icon_path)
|
||||
except RepoCopyError, errmsg:
|
||||
print >> sys.stderr, errmsg
|
||||
|
||||
# add icon to pkginfo if in repository
|
||||
add_icon_hash_to_pkginfo(pkginfo)
|
||||
|
||||
# installer_item upload was successful, so upload pkginfo to repo
|
||||
# installer_item upload was successful. so upload pkginfo to repo
|
||||
if not options.nointeractive:
|
||||
# open the pkginfo file in the user's editor
|
||||
#open_pkginfo_in_editor(pkginfo)
|
||||
#answer = raw_input('Type <return> when done editing...')
|
||||
pass
|
||||
try:
|
||||
pkginfo_path = copy_pkginfo_to_repo(pkginfo, options.subdirectory)
|
||||
pkginfo_path = copy_pkginfo_to_repo(repo, pkginfo, options.subdirectory)
|
||||
except RepoCopyError, errmsg:
|
||||
print >> sys.stderr, errmsg
|
||||
cleanup_and_exit(-1)
|
||||
|
||||
if not options.nointeractive:
|
||||
# open the pkginfo file in the user's editor
|
||||
open_pkginfo_in_editor(pkginfo_path)
|
||||
answer = raw_input('Rebuild catalogs? [y/n] ')
|
||||
if answer.lower().startswith('y'):
|
||||
try:
|
||||
|
||||
Executable → Regular
+205
-228
@@ -1,41 +1,22 @@
|
||||
#!/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.
|
||||
"""
|
||||
'''Defines FileRepo plugin. See docstring for FileRepo class'''
|
||||
|
||||
import errno
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import objc
|
||||
import glob
|
||||
import sys
|
||||
|
||||
from urlparse import urlparse
|
||||
|
||||
from munkilib.munkirepo import Repo, RepoError
|
||||
|
||||
from munkilib.munkicommon import listdir
|
||||
from munkilib.munkirepo import Repo
|
||||
|
||||
# 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
|
||||
import objc
|
||||
from CoreFoundation import CFURLCreateWithString
|
||||
|
||||
class Attrdict(dict):
|
||||
@@ -62,218 +43,214 @@ try:
|
||||
except (ImportError, KeyError):
|
||||
NETFSMOUNTURLSYNC_AVAILABLE = False
|
||||
|
||||
if NETFSMOUNTURLSYNC_AVAILABLE:
|
||||
class ShareMountException(Exception):
|
||||
'''An exception raised if share mounting failed'''
|
||||
pass
|
||||
|
||||
class ShareMountException(Exception):
|
||||
'''An exception raised if share mounting failed'''
|
||||
pass
|
||||
|
||||
|
||||
class ShareAuthenticationNeededException(ShareMountException):
|
||||
'''An exception raised if authentication is needed'''
|
||||
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(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, mountpoints = NetFS.NetFSMountURLSync(
|
||||
sh_url, None, None, None, open_options, mount_options, None)
|
||||
# Check if it worked
|
||||
if result != 0:
|
||||
if result in (-6600, errno.EINVAL, errno.ENOTSUP, errno.EAUTH):
|
||||
# -6600 is kNetAuthErrorInternal in NetFS.h 10.9+
|
||||
# errno.EINVAL is returned if an afp share needs a login in some
|
||||
# versions of OS X
|
||||
# errno.ENOTSUP is returned if an afp share needs a login in some
|
||||
# versions of OS X
|
||||
# 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(mountpoints[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_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, mountpoints = 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(mountpoints[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)
|
||||
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:
|
||||
mountpoint = mount_share(share_url)
|
||||
except ShareAuthenticationNeededException:
|
||||
username = raw_input('Username: ')
|
||||
password = getpass.getpass()
|
||||
mountpoint = mount_share_with_credentials(share_url, username, password)
|
||||
return mountpoint
|
||||
|
||||
|
||||
class FileRepo(Repo):
|
||||
WE_MOUNTED_THE_REPO = False
|
||||
'''Repo implementation that access a local or locally-mounted repo.'''
|
||||
'''Handles local filesystem repo and repos mounted via filesharing'''
|
||||
|
||||
def exists(self, subdir=None):
|
||||
'''Returns true if the specified path exists in the repo'''
|
||||
full_path = self.root
|
||||
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.root, 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.root, 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.root, path), mode)
|
||||
|
||||
def makedirs(self, path, mode=0777):
|
||||
'''Creates a directory within the repo, including parent directories.'''
|
||||
return os.makedirs(os.path.join(self.root, path), mode)
|
||||
|
||||
def listdir(self, path):
|
||||
'''Lists the contents of a repo directory.'''
|
||||
return listdir(os.path.join(self.root, path))
|
||||
|
||||
def remove(self, path):
|
||||
'''Removes a file from the repo.'''
|
||||
return os.remove(os.path.join(self.root, path))
|
||||
|
||||
def unlink(self, path):
|
||||
'''Removes a file from the repo.'''
|
||||
return os.unlink(os.path.join(self.root, path))
|
||||
|
||||
def get(self, src, dest):
|
||||
'''Copies a file from the repo to a local file.'''
|
||||
cmd = ['/bin/cp', os.path.join(self.root, 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.root, 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.
|
||||
#
|
||||
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()
|
||||
|
||||
def open(self, path, mode='r'):
|
||||
'''Opens a file in the repo.'''
|
||||
return self.RepoFile(self, os.path.join(self.root, path), mode)
|
||||
|
||||
def mount(self):
|
||||
'''Mounts the repo locally.'''
|
||||
if os.path.exists(self.root):
|
||||
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
|
||||
def __init__(self, baseurl):
|
||||
'''Constructor'''
|
||||
self.baseurl = baseurl
|
||||
url_parts = urlparse(baseurl)
|
||||
self.url_scheme = url_parts.scheme
|
||||
if self.url_scheme == 'file':
|
||||
self.root = url_parts.path
|
||||
else:
|
||||
os.mkdir(self.root)
|
||||
if self.url.startswith('afp:'):
|
||||
cmd = ['/sbin/mount_afp', '-i', self.url, self.root]
|
||||
elif self.url.startswith('smb:'):
|
||||
cmd = ['/sbin/mount_smbfs', self.url[4:], self.root]
|
||||
elif self.url.startswith('nfs://'):
|
||||
cmd = ['/sbin/mount_nfs', self.url[6:], self.root]
|
||||
else:
|
||||
print >> sys.stderr, 'Unsupported filesystem URL!'
|
||||
return
|
||||
retcode = subprocess.call(cmd)
|
||||
if retcode:
|
||||
os.rmdir(self.root)
|
||||
else:
|
||||
self.WE_MOUNTED_THE_REPO = True
|
||||
return retcode
|
||||
self.root = os.path.join('/Volumes', url_parts.path)
|
||||
self.we_mounted_repo = False
|
||||
self._connect()
|
||||
|
||||
def unmount(self):
|
||||
'''Unmounts the repo.'''
|
||||
if not os.path.exists(self.root):
|
||||
return
|
||||
retcode = 0
|
||||
if os.path.exists(self.root):
|
||||
def __del__(self):
|
||||
'''Destructor -- unmount the fileshare if we mounted it'''
|
||||
if self.we_mounted_repo and os.path.exists(self.root):
|
||||
cmd = ['/sbin/umount', self.root]
|
||||
retcode = subprocess.call(cmd)
|
||||
return retcode
|
||||
subprocess.call(cmd)
|
||||
|
||||
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.root, path), **kwargs):
|
||||
dirpath = dirpath[len(self.root) + 1:]
|
||||
yield (dirpath, dirnames, filenames)
|
||||
def _connect(self):
|
||||
'''If self.root is present, return. Otherwise try to mount the share
|
||||
url.'''
|
||||
if not os.path.exists(self.root) and self.url_scheme != 'file':
|
||||
print 'Attempting to mount fileshare %s:' % self.baseurl
|
||||
if NETFSMOUNTURLSYNC_AVAILABLE:
|
||||
try:
|
||||
self.root = mount_share_url(self.baseurl)
|
||||
except ShareMountException, err:
|
||||
print sys.stderr, err
|
||||
return
|
||||
else:
|
||||
self.we_mounted_repo = True
|
||||
else:
|
||||
os.mkdir(self.root)
|
||||
if self.baseurl.startswith('afp:'):
|
||||
cmd = ['/sbin/mount_afp', '-i', self.baseurl, self.root]
|
||||
elif self.baseurl.startswith('smb:'):
|
||||
cmd = ['/sbin/mount_smbfs', self.baseurl[4:], self.root]
|
||||
elif self.baseurl.startswith('nfs://'):
|
||||
cmd = ['/sbin/mount_nfs', self.baseurl[6:], self.root]
|
||||
else:
|
||||
print >> sys.stderr, 'Unsupported filesystem URL!'
|
||||
return
|
||||
retcode = subprocess.call(cmd)
|
||||
if retcode:
|
||||
os.rmdir(self.root)
|
||||
else:
|
||||
self.we_mounted_repo = True
|
||||
# mount attempt complete; check again for existence of self.root
|
||||
if not os.path.exists(self.root):
|
||||
raise RepoError('%s does not exist' % self.root)
|
||||
|
||||
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
|
||||
def itemlist(self, kind):
|
||||
'''Returns a list of identifiers for each item of kind.
|
||||
Kind might be 'catalogs', 'manifests', 'pkgsinfo', 'pkgs', or 'icons'.
|
||||
For a file-backed repo this would be a list of pathnames.'''
|
||||
search_dir = os.path.join(self.root, kind)
|
||||
file_list = []
|
||||
try:
|
||||
for (dirpath, dummy_dirnames, filenames) in os.walk(search_dir):
|
||||
for name in filenames:
|
||||
abs_path = os.path.join(dirpath, name)
|
||||
rel_path = abs_path[len(search_dir):].lstrip("/")
|
||||
file_list.append(rel_path)
|
||||
return file_list
|
||||
except (OSError, IOError), err:
|
||||
raise RepoError(err)
|
||||
|
||||
def get(self, resource_identifier):
|
||||
'''Returns the content of item with given resource_identifier.
|
||||
For a file-backed repo, a resource_identifier of
|
||||
'pkgsinfo/apps/Firefox-52.0.plist' would return the contents of
|
||||
<repo_root>/pkgsinfo/apps/Firefox-52.0.plist.
|
||||
Avoid using this method with the 'pkgs' kind as it might return a
|
||||
really large blob of data.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
try:
|
||||
fileref = open(repo_filepath)
|
||||
data = fileref.read()
|
||||
fileref.close()
|
||||
return data
|
||||
except OSError, err:
|
||||
raise RepoError(err)
|
||||
|
||||
def get_to_local_file(self, resource_identifier, local_file_path):
|
||||
'''Gets the contents of item with given resource_identifier and saves
|
||||
it to local_file_path.
|
||||
For a file-backed repo, a resource_identifier
|
||||
of 'pkgsinfo/apps/Firefox-52.0.plist' would copy the contents of
|
||||
<repo_root>/pkgsinfo/apps/Firefox-52.0.plist to a local file given by
|
||||
local_file_path.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
try:
|
||||
shutil.copyfile(repo_filepath, local_file_path)
|
||||
except (OSError, IOError), err:
|
||||
raise RepoError(err)
|
||||
|
||||
def put(self, resource_identifier, content):
|
||||
'''Stores content on the repo based on resource_identifier.
|
||||
For a file-backed repo, a resource_identifier of
|
||||
'pkgsinfo/apps/Firefox-52.0.plist' would result in the content being
|
||||
saved to <repo_root>/pkgsinfo/apps/Firefox-52.0.plist.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
dir_path = os.path.dirname(repo_filepath)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path, 0755)
|
||||
try:
|
||||
fileref = open(repo_filepath, 'w')
|
||||
fileref.write(content)
|
||||
fileref.close()
|
||||
except OSError, err:
|
||||
raise RepoError(err)
|
||||
|
||||
def put_from_local_file(self, resource_identifier, local_file_path):
|
||||
'''Copies the content of local_file_path to the repo based on
|
||||
resource_identifier. For a file-backed repo, a resource_identifier
|
||||
of 'pkgsinfo/apps/Firefox-52.0.plist' would result in the content
|
||||
being saved to <repo_root>/pkgsinfo/apps/Firefox-52.0.plist.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
if local_file_path == repo_filepath:
|
||||
# nothing to do!
|
||||
return
|
||||
dir_path = os.path.dirname(repo_filepath)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path, 0755)
|
||||
try:
|
||||
shutil.copyfile(local_file_path, repo_filepath)
|
||||
except (OSError, IOError), err:
|
||||
raise RepoError(err)
|
||||
|
||||
def delete(self, resource_identifier):
|
||||
'''Deletes a repo object located by resource_identifier.
|
||||
For a file-backed repo, a resource_identifier of
|
||||
'pkgsinfo/apps/Firefox-52.0.plist' would result in the deletion of
|
||||
<repo_root>/pkgsinfo/apps/Firefox-52.0.plist.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
try:
|
||||
os.remove(repo_filepath)
|
||||
except OSError, err:
|
||||
raise RepoError(err)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# encoding: utf-8
|
||||
'''Defines MWA2APIRepo plugin. See docstring for MWA2APIRepo class'''
|
||||
|
||||
import base64
|
||||
import getpass
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from urlparse import urlparse
|
||||
|
||||
from munkilib.munkirepo import Repo
|
||||
|
||||
|
||||
# NetFS share mounting code borrowed and liberally adapted from Michael Lynn's
|
||||
# work here: https://gist.github.com/pudquick/1362a8908be01e23041d
|
||||
try:
|
||||
import objc
|
||||
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
|
||||
|
||||
|
||||
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, mountpoints = NetFS.NetFSMountURLSync(
|
||||
sh_url, None, None, None, open_options, mount_options, None)
|
||||
# Check if it worked
|
||||
if result != 0:
|
||||
if result in (-6600, errno.EINVAL, errno.ENOTSUP, errno.EAUTH):
|
||||
# -6600 is kNetAuthErrorInternal in NetFS.h 10.9+
|
||||
# errno.EINVAL is returned if an afp share needs a login in some
|
||||
# versions of OS X
|
||||
# errno.ENOTSUP is returned if an afp share needs a login in some
|
||||
# versions of OS X
|
||||
# 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(mountpoints[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, mountpoints = 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(mountpoints[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:
|
||||
mountpoint = mount_share(share_url)
|
||||
except ShareAuthenticationNeededException:
|
||||
username = raw_input('Username: ')
|
||||
password = getpass.getpass()
|
||||
mountpoint = mount_share_with_credentials(share_url, username, password)
|
||||
return mountpoint
|
||||
|
||||
|
||||
class NewFileRepo(Repo):
|
||||
'''Handles local filesystem repo and repos mounted via filesharing'''
|
||||
|
||||
def __init__(self, baseurl):
|
||||
'''Constructor'''
|
||||
self.baseurl = baseurl
|
||||
url_parts = urlparse(baseurl)
|
||||
self.url_scheme = url_parts.scheme
|
||||
if self.url_scheme == 'file':
|
||||
self.root = url_parts.path
|
||||
else:
|
||||
self.root = os.path.join('/Volumes', url_parts.path)
|
||||
self.we_mounted_repo = False
|
||||
|
||||
def connect(self):
|
||||
'''If self.root is present, return. Otherwise try to mount the share
|
||||
url.'''
|
||||
if not os.path.exists(self.root) and self.url_scheme != 'file':
|
||||
print 'Attempting to mount fileshare %s:' % self.baseurl
|
||||
if NETFSMOUNTURLSYNC_AVAILABLE:
|
||||
try:
|
||||
self.root = mount_share_url(self.baseurl)
|
||||
except ShareMountException, err:
|
||||
print sys.stderr, err
|
||||
return
|
||||
else:
|
||||
self.we_mounted_repo = True
|
||||
else:
|
||||
os.mkdir(self.root)
|
||||
if self.baseurl.startswith('afp:'):
|
||||
cmd = ['/sbin/mount_afp', '-i', self.baseurl, self.root]
|
||||
elif self.baseurl.startswith('smb:'):
|
||||
cmd = ['/sbin/mount_smbfs', self.baseurl[4:], self.root]
|
||||
elif self.baseurl.startswith('nfs://'):
|
||||
cmd = ['/sbin/mount_nfs', self.baseurl[6:], self.root]
|
||||
else:
|
||||
print >> sys.stderr, 'Unsupported filesystem URL!'
|
||||
return
|
||||
retcode = subprocess.call(cmd)
|
||||
if retcode:
|
||||
os.rmdir(self.root)
|
||||
else:
|
||||
self.we_mounted_repo = True
|
||||
# mount attempt complete; check again for existence of self.root
|
||||
if not os.path.exists(self.root):
|
||||
raise SomeSortOfError
|
||||
|
||||
def itemlist(self, kind):
|
||||
'''Returns a list of identifiers for each item of kind.
|
||||
Kind might be 'catalogs', 'manifests', 'pkgsinfo', 'pkgs', or 'icons'.
|
||||
For a file-backed repo this would be a list of pathnames.'''
|
||||
search_dir = os.path.join(self.root, kind)
|
||||
file_list = []
|
||||
for (dirpath, dummy_dirnames, filenames) in os.walk(search_dir):
|
||||
for name in filenames:
|
||||
abs_path = os.path.join(dirpath, name)
|
||||
rel_path = abs_path[len(search_dir):].lstrip("/")
|
||||
file_list.append(rel_path)
|
||||
return file_list
|
||||
|
||||
def get(self, resource_identifier):
|
||||
'''Returns the content of item with given resource_identifier.
|
||||
For a file-backed repo, a resource_identifier of
|
||||
'pkgsinfo/apps/Firefox-52.0.plist' would return the contents of
|
||||
<repo_root>/pkgsinfo/apps/Firefox-52.0.plist.
|
||||
Avoid using this method with the 'pkgs' kind as it might return a
|
||||
really large blob of data.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
try:
|
||||
fileref = open(repo_filepath)
|
||||
data = fileref.read()
|
||||
fileref.close()
|
||||
return data
|
||||
except OSError, err:
|
||||
raise
|
||||
|
||||
def get_to_local_file(self, resource_identifier, local_file_path):
|
||||
'''Gets the contents of item with given resource_identifier and saves
|
||||
it to local_file_path.
|
||||
For a file-backed repo, a resource_identifier
|
||||
of 'pkgsinfo/apps/Firefox-52.0.plist' would copy the contents of
|
||||
<repo_root>/pkgsinfo/apps/Firefox-52.0.plist to a local file given by
|
||||
local_file_path.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
try:
|
||||
shutil.copyfile(repo_filepath, local_file_path)
|
||||
except (OSError, IOError), err:
|
||||
raise
|
||||
|
||||
def put(self, resource_identifier, content):
|
||||
'''Stores content on the repo based on resource_identifier.
|
||||
For a file-backed repo, a resource_identifier of
|
||||
'pkgsinfo/apps/Firefox-52.0.plist' would result in the content being
|
||||
saved to <repo_root>/pkgsinfo/apps/Firefox-52.0.plist.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
dir_path = os.path.dirname(repo_filepath)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path, 0755)
|
||||
try:
|
||||
fileref = open(repo_filepath, 'w')
|
||||
fileref.write(content)
|
||||
fileref.close()
|
||||
except OSError, err:
|
||||
raise
|
||||
|
||||
def put_from_local_file(self, resource_identifier, local_file_path):
|
||||
'''Copies the content of local_file_path to the repo based on
|
||||
resource_identifier. For a file-backed repo, a resource_identifier
|
||||
of 'pkgsinfo/apps/Firefox-52.0.plist' would result in the content
|
||||
being saved to <repo_root>/pkgsinfo/apps/Firefox-52.0.plist.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
dir_path = os.path.dirname(repo_filepath)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path, 0755)
|
||||
try:
|
||||
shutil.copyfile(local_file_path, repo_filepath)
|
||||
except (OSError, IOError), err:
|
||||
raise
|
||||
|
||||
def delete(self, resource_identifier):
|
||||
'''Deletes a repo object located by resource_identifier.
|
||||
For a file-backed repo, a resource_identifier of
|
||||
'pkgsinfo/apps/Firefox-52.0.plist' would result in the deletion of
|
||||
<repo_root>/pkgsinfo/apps/Firefox-52.0.plist.'''
|
||||
repo_filepath = os.path.join(self.root, resource_identifier)
|
||||
try:
|
||||
os.remove(repo_filepath)
|
||||
except OSError, err:
|
||||
raise
|
||||
|
||||
@@ -3,45 +3,16 @@ import os
|
||||
import sys
|
||||
|
||||
|
||||
class RepoError(Exception):
|
||||
'''Base exception for repo errors'''
|
||||
pass
|
||||
|
||||
|
||||
class Repo(object):
|
||||
'''Abstract base class for repo'''
|
||||
mounted = False
|
||||
|
||||
def __init__(self, path, url):
|
||||
self.root = path
|
||||
self.url = url
|
||||
|
||||
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
|
||||
#check 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
|
||||
|
||||
def exists(self):
|
||||
'''Must be overriden in subclass'''
|
||||
return False
|
||||
|
||||
def mount(self):
|
||||
'''Must be overridden in subclasses'''
|
||||
return -1
|
||||
|
||||
|
||||
class MissingRepo(Repo):
|
||||
'''Stub object to return when we can't find the one requsted'''
|
||||
def available(self):
|
||||
return False
|
||||
def __init__(self, url):
|
||||
'''Override in subclasses'''
|
||||
pass
|
||||
|
||||
|
||||
def plugin_named(name):
|
||||
@@ -55,15 +26,15 @@ def plugin_named(name):
|
||||
return None
|
||||
|
||||
|
||||
def connect(repo_path, repo_url, plugin_name):
|
||||
def connect(repo_url, plugin_name):
|
||||
'''Return a repo object for operations on our Munki repo'''
|
||||
if plugin_name is None:
|
||||
plugin_name = 'FileRepo'
|
||||
plugin = plugin_named(plugin_name)
|
||||
if plugin:
|
||||
return plugin(repo_path, repo_url)
|
||||
return plugin(repo_url)
|
||||
else:
|
||||
return MissingRepo(repo_path, repo_url)
|
||||
raise RepoError('Could not find repo plugin named %s' % plugin_name)
|
||||
|
||||
|
||||
# yes, having this at the end is weird. But it allows us to dynamically import
|
||||
|
||||
Reference in New Issue
Block a user