First commits of new-style repo plugins

This commit is contained in:
Greg Neagle
2017-03-08 09:23:58 -08:00
parent f47cd6ac1a
commit d4ecc08a14
2 changed files with 391 additions and 0 deletions
@@ -0,0 +1,154 @@
# encoding: utf-8
import base64
import getpass
import os
import plistlib
import subprocess
import tempfile
from munkilib.munkirepo import Repo
from munkilib import display
CURL_CMD = '/usr/bin/curl'
class CurlError(Exception):
pass
class MWA2APIRepo(Repo):
def __init__(self, baseurl):
'''Constructor'''
self.baseurl = baseurl
self.authtoken = None
def connect(self):
'''For a fileshare repo, we'd mount the share, prompting for
credentials if needed. For the API repo, well look for a stored
authtoken; if we don't find one, we'll prompt for credentials
and make an authtoken.'''
print 'Please provide credentials for %s:' % self.baseurl
username = raw_input('Username: ')
password = getpass.getpass()
user_and_pass = '%s:%s' % (username, password)
self.authtoken = 'Basic %s' % base64.b64encode(user_and_pass)
def _curl(self, relative_url, headers=None, method='GET',
filename=None, content=None):
'''Use curl to talk to MWA2 API'''
# we use a config/directive file to avoid having the auth header show
# up in a process listing
fileref, directivepath = tempfile.mkstemp()
fileobj = os.fdopen(fileref, 'w')
print >> fileobj, 'silent' # no progress meter
print >> fileobj, 'show-error' # print error msg to stderr
print >> fileobj, 'fail' # throw error if download fails
print >> fileobj, 'location' # follow redirects
print >> fileobj, 'request = %s' % method
if headers:
for key in headers:
print >> fileobj, 'header = "%s: %s"' % (key, headers[key])
print >> fileobj, 'header = "Authorization: %s"' % self.authtoken
if method == 'GET':
print >> fileobj, 'header = "Accept: application/xml"'
else:
print >> fileobj, 'header = "Content-type: application/xml"'
url = os.path.join(self.baseurl, relative_url)
print >> fileobj, 'url = "%s"' % url
fileobj.close()
cmd = [CURL_CMD, '-q', '--config', directivepath]
if filename and method == 'GET':
cmd.extend(['-o', filename])
if filename and method == 'PUT':
cmd.extend(['-d', '@%s' % filename])
elif content and method == 'PUT':
cmd.extend(['-d', content])
#display.display_debug1('Curl command is %s', cmd)
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()
try:
os.unlink(directivepath)
except OSError:
pass
if proc.returncode:
raise CurlError((proc.returncode, err))
return output
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.'''
url = kind + '?api_fields=filename'
try:
data = self._curl(url)
except CurlError, err:
raise
plist = plistlib.readPlistFromString(data)
if kind in ['catalogs', 'manifests', 'pkgsinfo']:
# it's a list of dicts containing 'filename' key/values
return [item['filename'] for item in plist]
else:
# it's a list of filenames
return plist
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.'''
try:
return self._curl(resource_identifier)
except CurlError, 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.'''
try:
result = self._curl(resource_identifier, filename=local_file_path)
except CurlError, 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.'''
try:
result = self._curl(
resource_identifier, method='PUT', content=content)
except CurlError, 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.'''
try:
result = self._curl(
resource_identifier, method='PUT', filename=local_file_path)
except CurlError, 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.'''
try:
result = self._curl(resource_identifier, method='DELETE')
except CurlError, err:
raise
@@ -0,0 +1,237 @@
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