Files
munki/code/client/munkilib/cliutils.py
2017-04-07 21:56:13 -07:00

231 lines
7.6 KiB
Python

# encoding: utf-8
#
# Copyright 2017 Greg Neagle.
#
# 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.
"""
cliutils
Created by Greg Neagle on 2017-03-12.
Functions supporting the admin command-line tools
"""
import ctypes
from ctypes.util import find_library
import os
import plistlib
import readline
import sys
import tempfile
import thread
import time
import urllib
import urlparse
from xml.parsers.expat import ExpatError
FOUNDATION_SUPPORT = True
try:
# PyLint cannot properly find names inside Cocoa libraries, so issues bogus
# No name 'Foo' in module 'Bar' warnings. Disable them.
# pylint: disable=E0611
from Foundation import CFPreferencesAppSynchronize
from Foundation import CFPreferencesCopyAppValue
from Foundation import CFPreferencesSetAppValue
# pylint: enable=E0611
except ImportError:
# CoreFoundation/Foundation isn't available
FOUNDATION_SUPPORT = False
BUNDLE_ID = 'com.googlecode.munki.munkiimport'
PREFSNAME = BUNDLE_ID + '.plist'
PREFSPATH = os.path.expanduser(os.path.join('~/Library/Preferences', PREFSNAME))
if FOUNDATION_SUPPORT:
def pref(prefname):
"""Return a preference. Since this uses CFPreferencesCopyAppValue,
Preferences can be defined several places. Precedence is:
- MCX/Configuration Profile
- ~/Library/Preferences/ByHost/
com.googlecode.munki.munkiimport.XX.plist
- ~/Library/Preferences/com.googlecode.munki.munkiimport.plist
- /Library/Preferences/com.googlecode.munki.munkiimport.plist
"""
return CFPreferencesCopyAppValue(prefname, BUNDLE_ID)
else:
def pref(prefname):
"""Returns a preference for prefname. This is a fallback mechanism if
CoreFoundation functions are not available -- for example to allow the
possible use of makecatlogs or manifestutil on Linux"""
if not hasattr(pref, 'cache'):
pref.cache = None
if not pref.cache:
try:
pref.cache = plistlib.readPlist(PREFSPATH)
except (IOError, OSError, ExpatError):
pass
if prefname in pref.cache:
return pref.cache[prefname]
else:
return None
def get_version():
"""Returns version of munkitools, reading version.plist"""
# this implementation avoids calling Foundation and will work on
# non Apple OSes.
vers = "UNKNOWN"
build = ""
# find the munkilib directory, and the version file
munkilibdir = os.path.dirname(os.path.abspath(__file__))
versionfile = os.path.join(munkilibdir, "version.plist")
if os.path.exists(versionfile):
try:
vers_plist = plistlib.readPlist(versionfile)
except (IOError, OSError, ExpatError):
pass
else:
try:
vers = vers_plist['CFBundleShortVersionString']
build = vers_plist['BuildNumber']
except KeyError:
pass
if build:
vers = vers + "." + build
return vers
def path2url(path):
'''Converts a path to a file: url'''
return urlparse.urljoin('file:', urllib.pathname2url(path))
def print_utf8(text):
'''Print Unicode text as UTF-8'''
print text.encode('UTF-8')
def print_err_utf8(text):
'''Print Unicode text to stderr as UTF-8'''
print >> sys.stderr, text.encode('UTF-8')
class TempFile(object):
'''A class that creates a temp file that is automatically deleted when
the object goes out of scope.'''
# pylint: disable=too-few-public-methods
def __init__(self):
filedesc, filepath = tempfile.mkstemp()
# we just want the path; close the file descriptor
os.close(filedesc)
self.path = filepath
def __del__(self):
try:
os.unlink(self.path)
except OSError:
pass
# pylint: disable=invalid-name
libedit = None
if 'libedit' in readline.__doc__:
# readline module was compiled against libedit
libedit = ctypes.cdll.LoadLibrary(find_library('libedit'))
# pylint: enable=invalid-name
def raw_input_with_default(prompt, default_text):
'''Get input from user with a prompt and a suggested default value'''
# 10.6's libedit doesn't have the rl_set_prompt function, so we fall back
# to the previous behavior
darwin_vers = int(os.uname()[2].split('.')[0])
if darwin_vers == 10:
if default_text:
prompt = '%s [%s]: ' % (prompt.rstrip(': '), default_text)
return (unicode(raw_input(prompt), encoding=sys.stdin.encoding) or
unicode(default_text))
else:
# no default value, just call raw_input
return unicode(raw_input(prompt), encoding=sys.stdin.encoding)
# A nasty, nasty hack to get around Python readline limitations under
# OS X. Gives us editable default text for configuration and munkiimport
# choices'''
def insert_default_text(prompt, text):
'''Helper function'''
time.sleep(0.01)
libedit.rl_set_prompt(prompt)
readline.insert_text(text)
libedit.rl_forced_update_display()
readline.clear_history()
if not default_text:
return unicode(raw_input(prompt), encoding=sys.stdin.encoding)
elif libedit:
# readline module was compiled against libedit
thread.start_new_thread(insert_default_text, (prompt, default_text))
return unicode(raw_input(), encoding=sys.stdin.encoding)
else:
readline.set_startup_hook(lambda: readline.insert_text(default_text))
try:
return unicode(raw_input(prompt), encoding=sys.stdin.encoding)
finally:
readline.set_startup_hook()
class ConfigurationSaveError(Exception):
'''Error to raise if there's an error saving configuration'''
pass
def configure(prompt_list):
"""Gets configuration options and saves them to preferences store"""
darwin_vers = int(os.uname()[2].split('.')[0])
edited_prefs = {}
for (key, prompt) in prompt_list:
newvalue = raw_input_with_default('%15s: ' % prompt, pref(key))
if darwin_vers == 10:
# old behavior in SL: hitting return gives you an empty string,
# and means accept the default value.
edited_prefs[key] = newvalue or pref(key) or ''
else:
# just use the edited value as-is
edited_prefs[key] = newvalue
if FOUNDATION_SUPPORT:
for key, value in edited_prefs.items():
try:
CFPreferencesSetAppValue(key, value, BUNDLE_ID)
except BaseException:
print >> sys.stderr, 'Could not save configuration!'
raise ConfigurationSaveError
CFPreferencesAppSynchronize(BUNDLE_ID)
else:
try:
existing_prefs = plistlib.readPlist(PREFSPATH)
existing_prefs.update(edited_prefs)
plistlib.writePlist(existing_prefs, PREFSPATH)
except (IOError, OSError, ExpatError):
print >> sys.stderr, (
'Could not save configuration to %s' % PREFSPATH)
raise ConfigurationSaveError
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'