munkicommon __init.py__ almost free of functions...

This commit is contained in:
Greg Neagle
2016-12-14 15:07:38 -08:00
parent 705e4d5239
commit b63531bd8a
15 changed files with 995 additions and 930 deletions

View File

@@ -22,55 +22,27 @@ Created by Greg Neagle on 2008-11-18.
Common functions used by the munki tools.
"""
import ctypes
import ctypes.util
import fcntl
import hashlib
import os
import logging
import logging.handlers
import platform
import re
import select
import shutil
import signal
import struct
import subprocess
import sys
import tempfile
import time
import urllib2
import warnings
from distutils import version
from types import StringType
from xml.dom import minidom
from .. import munkistatus
from .. import FoundationPlist
# We wildcard-import from submodules for backwards compatibility; functions
# that were previously available from this module
# pylint: disable=wildcard-import
from .authrestart import *
from .constants import *
from .display import *
from .dmgutils import *
from .hash import *
from .info import *
from .munkilog import *
from .osutils import *
from .output import *
from .pkgutils import *
from .prefs import *
from .processes import *
from .reports import *
from .scriptutils import *
# pylint: enable=wildcard-import
import LaunchServices
# 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 NSDate, NSMetadataQuery, NSPredicate, NSRunLoop
# pylint: enable=E0611
# we use lots of camelCase-style names. Deal with it.
# we use camelCase-style names. Deal with it.
# pylint: disable=C0103
@@ -82,249 +54,22 @@ def stopRequested():
global _stop_requested
if _stop_requested:
return True
STOP_REQUEST_FLAG = (
stop_request_flag = (
'/private/tmp/'
'com.googlecode.munki.managedsoftwareupdate.stop_requested')
if munkistatusoutput:
if os.path.exists(STOP_REQUEST_FLAG):
if os.path.exists(stop_request_flag):
# store this so it's persistent until this session is over
_stop_requested = True
log('### User stopped session ###')
try:
os.unlink(STOP_REQUEST_FLAG)
os.unlink(stop_request_flag)
except OSError, err:
display_error(
'Could not remove %s: %s', STOP_REQUEST_FLAG, err)
'Could not remove %s: %s', stop_request_flag, err)
return True
return False
def gethash(filename, hash_function):
"""
Calculates the hashvalue of the given file with the given hash_function.
Args:
filename: The file name to calculate the hash value of.
hash_function: The hash function object to use, which was instanciated
before calling this function, e.g. hashlib.md5().
Returns:
The hashvalue of the given file as hex string.
"""
if not os.path.isfile(filename):
return 'NOT A FILE'
f = open(filename, 'rb')
while 1:
chunk = f.read(2**16)
if not chunk:
break
hash_function.update(chunk)
f.close()
return hash_function.hexdigest()
def getmd5hash(filename):
"""
Returns hex of MD5 checksum of a file
"""
hash_function = hashlib.md5()
return gethash(filename, hash_function)
def getsha256hash(filename):
"""
Returns the SHA-256 hash value of a file as a hex string.
"""
hash_function = hashlib.sha256()
return gethash(filename, hash_function)
def isApplication(pathname):
"""Returns true if path appears to be an OS X application"""
# No symlinks, please
if os.path.islink(pathname):
return False
if pathname.endswith('.app'):
return True
if os.path.isdir(pathname):
# look for app bundle structure
# use Info.plist to determine the name of the executable
infoplist = os.path.join(pathname, 'Contents', 'Info.plist')
if os.path.exists(infoplist):
plist = FoundationPlist.readPlist(infoplist)
if 'CFBundlePackageType' in plist:
if plist['CFBundlePackageType'] != 'APPL':
return False
# get CFBundleExecutable,
# falling back to bundle name if it's missing
bundleexecutable = plist.get(
'CFBundleExecutable', os.path.basename(pathname))
bundleexecutablepath = os.path.join(
pathname, 'Contents', 'MacOS', bundleexecutable)
if os.path.exists(bundleexecutablepath):
return True
return False
# utility functions for running scripts from pkginfo files
# used by updatecheck.py and installer.py
def writefile(stringdata, path):
'''Writes string data to path.
Returns the path on success, empty string on failure.'''
try:
fileobject = open(path, mode='w', buffering=1)
# write line-by-line to ensure proper UNIX line-endings
for line in stringdata.splitlines():
print >> fileobject, line.encode('UTF-8')
fileobject.close()
return path
except (OSError, IOError):
display_error("Couldn't write %s" % stringdata)
return ""
def runEmbeddedScript(scriptname, pkginfo_item, suppress_error=False):
'''Runs a script embedded in the pkginfo.
Returns the result code.'''
# get the script text from the pkginfo
script_text = pkginfo_item.get(scriptname)
itemname = pkginfo_item.get('name')
if not script_text:
display_error(
'Missing script %s for %s' % (scriptname, itemname))
return -1
# write the script to a temp file
scriptpath = os.path.join(tmpdir(), scriptname)
if writefile(script_text, scriptpath):
cmd = ['/bin/chmod', '-R', 'o+x', scriptpath]
retcode = subprocess.call(cmd)
if retcode:
display_error(
'Error setting script mode in %s for %s'
% (scriptname, itemname))
return -1
else:
display_error(
'Cannot write script %s for %s' % (scriptname, itemname))
return -1
# now run the script
return runScript(
itemname, scriptpath, scriptname, suppress_error=suppress_error)
def runScript(itemname, path, scriptname, suppress_error=False):
'''Runs a script, Returns return code.'''
if suppress_error:
display_detail(
'Running %s for %s ' % (scriptname, itemname))
else:
display_status_minor(
'Running %s for %s ' % (scriptname, itemname))
if munkistatusoutput:
# set indeterminate progress bar
munkistatus.percent(-1)
scriptoutput = []
try:
proc = subprocess.Popen(path, shell=False, bufsize=1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
except OSError, e:
display_error(
'Error executing script %s: %s' % (scriptname, str(e)))
return -1
while True:
msg = proc.stdout.readline().decode('UTF-8')
if not msg and (proc.poll() != None):
break
# save all script output in case there is
# an error so we can dump it to the log
scriptoutput.append(msg)
msg = msg.rstrip("\n")
display_info(msg)
retcode = proc.poll()
if retcode and not suppress_error:
display_error(
'Running %s for %s failed.' % (scriptname, itemname))
display_error("-"*78)
for line in scriptoutput:
display_error("\t%s" % line.rstrip("\n"))
display_error("-"*78)
elif not suppress_error:
log('Running %s for %s was successful.' % (scriptname, itemname))
if munkistatusoutput:
# clear indeterminate progress bar
munkistatus.percent(0)
return retcode
def forceLogoutNow():
"""Force the logout of interactive GUI users and spawn MSU."""
try:
procs = findProcesses(exe=LOGINWINDOW)
users = {}
for pid in procs:
users[procs[pid]['user']] = pid
if 'root' in users:
del users['root']
# force MSU GUI to raise
f = open('/private/tmp/com.googlecode.munki.installatlogout', 'w')
f.close()
# kill loginwindows to cause logout of current users, whether
# active or switched away via fast user switching.
for user in users:
try:
os.kill(users[user], signal.SIGKILL)
except OSError:
pass
except BaseException, err:
display_error('Exception in forceLogoutNow(): %s' % str(err))
def blockingApplicationsRunning(pkginfoitem):
"""Returns true if any application in the blocking_applications list
is running or, if there is no blocking_applications list, if any
application in the installs list is running."""
if 'blocking_applications' in pkginfoitem:
appnames = pkginfoitem['blocking_applications']
else:
# if no blocking_applications specified, get appnames
# from 'installs' list if it exists
appnames = [os.path.basename(item.get('path'))
for item in pkginfoitem.get('installs', [])
if item['type'] == 'application']
display_debug1("Checking for %s" % appnames)
running_apps = [appname for appname in appnames
if isAppRunning(appname)]
if running_apps:
display_detail(
"Blocking apps for %s are running:" % pkginfoitem['name'])
display_detail(" %s" % running_apps)
return True
return False
def main():
"""Placeholder"""
print 'This is a library of support tools for the Munki Suite.'
if __name__ == '__main__':
main()
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -25,9 +25,9 @@ Functions supporting FileVault authrestart.
import subprocess
from .osutils import getOsVersion
from .output import display_debug1, display_error, display_warning, log
from .prefs import pref
from . import display
from . import osutils
from . import prefs
from .. import FoundationPlist
@@ -36,22 +36,22 @@ def supports_auth_restart():
if an Authorized Restart is supported, returns True
or False accordingly.
"""
display_debug1('Checking if FileVault is Enabled...')
display.display_debug1('Checking if FileVault is Enabled...')
active_cmd = ['/usr/bin/fdesetup', 'isactive']
try:
is_active = subprocess.check_output(
active_cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
if exc.output and 'false' in exc.output:
display_warning('FileVault appears to be Disabled...')
display.display_warning('FileVault appears to be Disabled...')
return False
if not exc.output:
display_warning(
display.display_warning(
'Encountered problem determining FileVault Status...')
return False
display_warning(exc.output)
display.display_warning(exc.output)
return False
display_debug1(
display.display_debug1(
'Checking if FileVault can perform an AuthRestart...')
support_cmd = ['/usr/bin/fdesetup', 'supportsauthrestart']
try:
@@ -59,17 +59,17 @@ def supports_auth_restart():
support_cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
if not exc.output:
display_warning(
display.display_warning(
'Encountered problem determining AuthRestart Status...')
return False
display_warning(exc.output)
display.display_warning(exc.output)
return False
if 'true' in is_active and 'true' in is_supported:
display_debug1(
display.display_debug1(
'FileVault is On and Supports an AuthRestart...')
return True
else:
display_warning(
display.display_warning(
'FileVault is Disabled or does not support an AuthRestart...')
return False
@@ -78,12 +78,12 @@ def get_auth_restart_key():
"""Returns recovery key as a string... If we failed
to get the proper information, returns an empty string"""
# checks to see if recovery key preference is set
recoverykeyplist = pref('RecoveryKeyFile')
recoverykeyplist = prefs.pref('RecoveryKeyFile')
if not recoverykeyplist:
display_warning(
display.display_warning(
"RecoveryKeyFile preference is not set")
return ''
display_debug1(
display.display_debug1(
'RecoveryKeyFile preference is set to {0}...'.format(recoverykeyplist))
# try to get the recovery key from the defined location
try:
@@ -91,11 +91,11 @@ def get_auth_restart_key():
recovery_key = keyplist['RecoveryKey'].strip()
return recovery_key
except FoundationPlist.NSPropertyListSerializationException:
display_error(
display.display_error(
'We had trouble getting info from {0}...'.format(recoverykeyplist))
return ''
except KeyError:
display_error(
display.display_error(
'Problem with Key: RecoveryKey in {0}...'.format(recoverykeyplist))
return ''
@@ -105,38 +105,33 @@ def perform_auth_restart():
to perform an authorized restart it checks to see if the machine supports
the feature. If supported it will then look for the defined plist containing
a key called RecoveryKey. It will use that value to perform the restart"""
display_debug1(
display.display_debug1(
'Checking if performing an Auth Restart is fully supported...')
if not supports_auth_restart():
display_warning("Machine doesn't support Authorized Restarts...")
display.display_warning("Machine doesn't support Authorized Restarts...")
return False
display_debug1('Machine Supports Authorized Restarts...')
display.display_debug1('Machine supports Authorized Restarts...')
recovery_key = get_auth_restart_key()
if not recovery_key:
return False
key = {'Password': recovery_key}
inputplist = FoundationPlist.writePlistToString(key)
log('Attempting an Authorized Restart Now...')
display.display_info('Attempting an Authorized Restart now...')
cmd = subprocess.Popen(
['/usr/bin/fdesetup', 'authrestart', '-inputplist'],
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE)
(dummy_out, err) = cmd.communicate(input=inputplist)
os_version_tuple = getOsVersion(as_tuple=True)
os_version_tuple = osutils.getOsVersion(as_tuple=True)
if os_version_tuple >= (10, 12) and 'System is being restarted' in err:
return True
if err:
display_error(err)
display.display_error(err)
return False
else:
return True
def main():
"""Placeholder"""
print 'This is a library of support tools for the Munki Suite.'
if __name__ == '__main__':
main()
print 'This is a library of support tools for the Munki Suite.'

3
code/client/munkilib/munkicommon/constants.py Executable file → Normal file
View File

@@ -46,3 +46,6 @@ ADDITIONAL_HTTP_HEADERS_KEY = 'AdditionalHttpHeaders'
LOGINWINDOW = (
"/System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow")
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -0,0 +1,251 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2016 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.
"""
display.py
Created by Greg Neagle on 2016-12-13.
Common output functions
"""
import sys
import warnings
from . import munkilog
from . import prefs
from . import reports
from .. import munkistatus
def getsteps(num_of_steps, limit):
"""
Helper function for display_percent_done
"""
steps = []
current = 0.0
for i in range(0, num_of_steps):
if i == num_of_steps-1:
steps.append(int(round(limit)))
else:
steps.append(int(round(current)))
current += float(limit)/float(num_of_steps-1)
return steps
def display_percent_done(current, maximum):
"""
Mimics the command-line progress meter seen in some
of Apple's tools (like softwareupdate), or tells
MunkiStatus to display percent done via progress bar.
"""
if munkistatusoutput:
step = getsteps(21, maximum)
if current in step:
if current == maximum:
percentdone = 100
else:
percentdone = int(float(current)/float(maximum)*100)
munkistatus.percent(str(percentdone))
elif verbose > 0:
step = getsteps(16, maximum)
output = ''
indicator = ['\t0', '.', '.', '20', '.', '.', '40', '.', '.',
'60', '.', '.', '80', '.', '.', '100\n']
for i in range(0, 16):
if current >= step[i]:
output += indicator[i]
if output:
sys.stdout.write('\r' + output)
sys.stdout.flush()
def str_to_ascii(a_string):
"""Given str (unicode, latin-1, or not) return ascii.
Args:
s: str, likely in Unicode-16BE, UTF-8, or Latin-1 charset
Returns:
str, ascii form, no >7bit chars
"""
try:
return unicode(a_string).encode('ascii', 'ignore')
except UnicodeDecodeError:
return a_string.decode('ascii', 'ignore')
def to_unicode(obj, encoding='UTF-8'):
"""Coerces basestring obj to unicode"""
if isinstance(obj, basestring):
if not isinstance(obj, unicode):
obj = unicode(obj, encoding)
return obj
def concat_message(msg, *args):
"""Concatenates a string with any additional arguments,
making sure everything is unicode"""
# coerce msg to unicode if it's not already
msg = to_unicode(msg)
if args:
# coerce all args to unicode as well
args = [to_unicode(arg) for arg in args]
try:
msg = msg % tuple(args)
except TypeError, dummy_err:
warnings.warn(
'String format does not match concat args: %s'
% (str(sys.exc_info())))
return msg.rstrip()
def display_status_major(msg, *args):
"""
Displays major status messages, formatting as needed
for verbose/non-verbose and munkistatus-style output.
"""
msg = concat_message(msg, *args)
munkilog.log(msg)
if munkistatusoutput:
munkistatus.message(msg)
munkistatus.detail('')
munkistatus.percent(-1)
elif verbose > 0:
if msg.endswith('.') or msg.endswith(u''):
print '%s' % msg.encode('UTF-8')
else:
print '%s...' % msg.encode('UTF-8')
sys.stdout.flush()
def display_status_minor(msg, *args):
"""
Displays minor status messages, formatting as needed
for verbose/non-verbose and munkistatus-style output.
"""
msg = concat_message(msg, *args)
munkilog.log(u' ' + msg)
if munkistatusoutput:
munkistatus.detail(msg)
elif verbose > 0:
if msg.endswith('.') or msg.endswith(u''):
print ' %s' % msg.encode('UTF-8')
else:
print ' %s...' % msg.encode('UTF-8')
sys.stdout.flush()
def display_info(msg, *args):
"""
Displays info messages.
Not displayed in MunkiStatus.
"""
msg = concat_message(msg, *args)
munkilog.log(u' ' + msg)
if munkistatusoutput:
pass
elif verbose > 0:
print ' %s' % msg.encode('UTF-8')
sys.stdout.flush()
def display_detail(msg, *args):
"""
Displays minor info messages.
Not displayed in MunkiStatus.
These are usually logged only, but can be printed to
stdout if verbose is set greater than 1
"""
msg = concat_message(msg, *args)
if munkistatusoutput:
pass
elif verbose > 1:
print ' %s' % msg.encode('UTF-8')
sys.stdout.flush()
if prefs.pref('LoggingLevel') > 0:
munkilog.log(u' ' + msg)
def display_debug1(msg, *args):
"""
Displays debug messages, formatting as needed
for verbose/non-verbose and munkistatus-style output.
"""
msg = concat_message(msg, *args)
if munkistatusoutput:
pass
elif verbose > 2:
print ' %s' % msg.encode('UTF-8')
sys.stdout.flush()
if prefs.pref('LoggingLevel') > 1:
munkilog.log('DEBUG1: %s' % msg)
def display_debug2(msg, *args):
"""
Displays debug messages, formatting as needed
for verbose/non-verbose and munkistatus-style output.
"""
msg = concat_message(msg, *args)
if munkistatusoutput:
pass
elif verbose > 3:
print ' %s' % msg.encode('UTF-8')
if prefs.pref('LoggingLevel') > 2:
munkilog.log('DEBUG2: %s' % msg)
def display_warning(msg, *args):
"""
Prints warning msgs to stderr and the log
"""
msg = concat_message(msg, *args)
warning = 'WARNING: %s' % msg
if verbose > 0:
print >> sys.stderr, warning.encode('UTF-8')
munkilog.log(warning)
# append this warning to our warnings log
munkilog.log(warning, 'warnings.log')
# collect the warning for later reporting
if 'Warnings' not in reports.report:
reports.report['Warnings'] = []
reports.report['Warnings'].append('%s' % msg)
def display_error(msg, *args):
"""
Prints msg to stderr and the log
"""
msg = concat_message(msg, *args)
errmsg = 'ERROR: %s' % msg
if verbose > 0:
print >> sys.stderr, errmsg.encode('UTF-8')
munkilog.log(errmsg)
# append this error to our errors log
munkilog.log(errmsg, 'errors.log')
# collect the errors for later reporting
if 'Errors' not in reports.report:
reports.report['Errors'] = []
reports.report['Errors'].append('%s' % msg)
# module globals
verbose = 1
munkistatusoutput = False
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -25,8 +25,9 @@ Utilities for working with disk images.
import os
import subprocess
from . import display
from .. import FoundationPlist
from .output import display_detail, display_error, display_warning
# we use lots of camelCase-style names. Deal with it.
# pylint: disable=C0103
@@ -66,7 +67,7 @@ def DMGisWritable(dmgpath):
bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = proc.communicate()
if err:
display_error(
display.display_error(
u'hdiutil error %s with image %s.', err, dmgpath)
(pliststr, out) = getFirstPlist(out)
if pliststr:
@@ -89,7 +90,7 @@ def DMGhasSLA(dmgpath):
bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = proc.communicate()
if err:
display_error(
display.display_error(
u'hdiutil error %s with image %s.', err, dmgpath)
(pliststr, out) = getFirstPlist(out)
if pliststr:
@@ -115,7 +116,7 @@ def hdiutilInfo():
bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = proc.communicate()
if err:
display_error(u'hdiutil info error: %s', err)
display.display_error(u'hdiutil info error: %s', err)
(pliststr, out) = getFirstPlist(out)
if pliststr:
try:
@@ -217,7 +218,7 @@ def mountdmg(dmgpath, use_shadow=False, use_existing_mounts=False):
stdin = ''
if DMGhasSLA(dmgpath):
stdin = 'Y\n'
display_detail(
display.display_detail(
'NOTE: %s has embedded Software License Agreement' % dmgname)
cmd = ['/usr/bin/hdiutil', 'attach', dmgpath,
'-mountRandom', '/tmp', '-nobrowse', '-plist']
@@ -228,7 +229,7 @@ def mountdmg(dmgpath, use_shadow=False, use_existing_mounts=False):
stderr=subprocess.PIPE, stdin=subprocess.PIPE)
(out, err) = proc.communicate(stdin)
if proc.returncode:
display_error(
display.display_error(
'Error: "%s" while mounting %s.' % (err.rstrip(), dmgname))
(pliststr, out) = getFirstPlist(out)
if pliststr:
@@ -238,7 +239,7 @@ def mountdmg(dmgpath, use_shadow=False, use_existing_mounts=False):
if 'mount-point' in entity:
mountpoints.append(entity['mount-point'])
except FoundationPlist.NSPropertyListSerializationException:
display_error(
display.display_error(
'Bad plist string returned when mounting diskimage %s:\n%s'
% (dmgname, pliststr))
return mountpoints
@@ -254,20 +255,15 @@ def unmountdmg(mountpoint):
(dummy_output, err) = proc.communicate()
if proc.returncode:
# ordinary unmount unsuccessful, try forcing
display_warning('Polite unmount failed: %s' % err)
display_warning('Attempting to force unmount %s' % mountpoint)
display.display_warning('Polite unmount failed: %s' % err)
display.display_warning('Attempting to force unmount %s' % mountpoint)
cmd.append('-force')
proc = subprocess.Popen(cmd, bufsize=-1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(dummy_output, err) = proc.communicate()
if proc.returncode:
display_warning('Failed to unmount %s: %s', mountpoint, err)
def main():
"""Placeholder"""
print 'This is a library of support tools for the Munki Suite.'
display.display_warning('Failed to unmount %s: %s', mountpoint, err)
if __name__ == '__main__':
main()
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -0,0 +1,71 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2016 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.
"""
hash.py
Created by Greg Neagle on 2016-12-14.
Munki's hash functions
"""
import hashlib
import os
def gethash(filename, hash_function):
"""
Calculates the hashvalue of the given file with the given hash_function.
Args:
filename: The file name to calculate the hash value of.
hash_function: The hash function object to use, which was instanciated
before calling this function, e.g. hashlib.md5().
Returns:
The hashvalue of the given file as hex string.
"""
if not os.path.isfile(filename):
return 'NOT A FILE'
fileref = open(filename, 'rb')
while 1:
chunk = fileref.read(2**16)
if not chunk:
break
hash_function.update(chunk)
fileref.close()
return hash_function.hexdigest()
def getmd5hash(filename):
"""
Returns hex of MD5 checksum of a file
"""
hash_function = hashlib.md5()
return gethash(filename, hash_function)
def getsha256hash(filename):
"""
Returns the SHA-256 hash value of a file as a hex string.
"""
hash_function = hashlib.sha256()
return gethash(filename, hash_function)
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -37,10 +37,11 @@ import LaunchServices
from Foundation import NSDate, NSMetadataQuery, NSPredicate, NSRunLoop
# pylint: enable=E0611
from .osutils import getOsVersion, listdir
from .output import display_debug1, display_error, display_warning, log
from .pkgutils import getBundleVersion
from .prefs import pref
from . import display
from . import munkilog
from . import osutils
from . import pkgutils
from . import prefs
from .. import FoundationPlist
# we use lots of camelCase-style names. Deal with it.
@@ -241,7 +242,7 @@ def getFilesystems():
# see man GETFSSTAT(2) for struct
statfs_32_struct = '=hh ll ll ll lQ lh hl 2l 15s 90s 90s x 16x'
statfs_64_struct = '=Ll QQ QQ Q ll l LLL 16s 1024s 1024s 32x'
os_version = getOsVersion(as_tuple=True)
os_version = osutils.getOsVersion(as_tuple=True)
if os_version <= (10, 5):
mode = 32
else:
@@ -265,7 +266,7 @@ def getFilesystems():
n = libc.getfsstat(ctypes.byref(buf), bufsize, MNT_NOWAIT)
if n < 0:
display_debug1('getfsstat() returned errno %d' % n)
display.display_debug1('getfsstat() returned errno %d' % n)
return {}
ofs = 0
@@ -336,10 +337,12 @@ def isExcludedFilesystem(path, _retry=False):
# perhaps the stat() on the path caused autofs to mount
# the required filesystem and now it will be available.
# try one more time to look for it after flushing the cache.
display_debug1('Trying isExcludedFilesystem again for %s' % path)
display.display_debug1(
'Trying isExcludedFilesystem again for %s' % path)
return isExcludedFilesystem(path, True)
else:
display_debug1('Could not match path %s to a filesystem' % path)
display.display_debug1(
'Could not match path %s to a filesystem' % path)
return None
exc_flags = ('read-only' in FILESYSTEMS[st.st_dev]['f_flags_set'] or
@@ -347,7 +350,7 @@ def isExcludedFilesystem(path, _retry=False):
is_nfs = FILESYSTEMS[st.st_dev]['f_fstypename'] == 'nfs'
if is_nfs or exc_flags:
display_debug1(
display.display_debug1(
'Excluding %s (flags %s, nfs %s)' % (path, exc_flags, is_nfs))
return is_nfs or exc_flags
@@ -377,7 +380,7 @@ def findAppsInDirs(dirlist):
query.stopQuery()
if runtime >= maxruntime:
display_warning(
display.display_warning(
'Spotlight search for applications terminated due to excessive '
'time. Possible causes: Spotlight indexing is turned off for a '
'volume; Spotlight is reindexing a volume.')
@@ -399,7 +402,7 @@ def getSpotlightInstalledApplications():
dirlist = []
applist = []
for f in listdir(u'/'):
for f in osutils.listdir(u'/'):
p = os.path.join(u'/', f)
if os.path.isdir(p) and not os.path.islink(p) \
and not isExcludedFilesystem(p):
@@ -410,7 +413,7 @@ def getSpotlightInstalledApplications():
# Future code changes may mean we wish to look for Applications
# installed on any r/w local volume.
#for f in listdir(u'/Volumes'):
#for f in osutils.listdir(u'/Volumes'):
# p = os.path.join(u'/Volumes', f)
# if os.path.isdir(p) and not os.path.islink(p) \
# and not isExcludedFilesystem(p):
@@ -445,76 +448,68 @@ def getLSInstalledApplications():
return applist
# we save SP_APPCACHE in a global to avoid querying system_profiler more than
# once per session for application data, which can be slow
SP_APPCACHE = None
@Memoize
def getSPApplicationData():
'''Uses system profiler to get application info for this machine'''
global SP_APPCACHE
if SP_APPCACHE is None:
cmd = ['/usr/sbin/system_profiler', 'SPApplicationsDataType', '-xml']
# uses our internal Popen instead of subprocess's so we can timeout
proc = Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
try:
output, dummy_error = proc.communicate(timeout=60)
except TimeoutError:
display_error(
'system_profiler hung; skipping SPApplicationsDataType query')
# return empty dict
SP_APPCACHE = {}
return SP_APPCACHE
try:
plist = FoundationPlist.readPlistFromString(output)
# system_profiler xml is an array
SP_APPCACHE = {}
for item in plist[0]['_items']:
SP_APPCACHE[item.get('path')] = item
except BaseException:
SP_APPCACHE = {}
return SP_APPCACHE
cmd = ['/usr/sbin/system_profiler', 'SPApplicationsDataType', '-xml']
# uses our internal Popen instead of subprocess's so we can timeout
proc = Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
try:
output, dummy_error = proc.communicate(timeout=60)
except TimeoutError:
display.display_error(
'system_profiler hung; skipping SPApplicationsDataType query')
# return empty dict
return {}
try:
plist = FoundationPlist.readPlistFromString(output)
# system_profiler xml is an array
app_data = {}
for item in plist[0]['_items']:
app_data[item.get('path')] = item
except BaseException:
app_data = {}
return app_data
# we save APPDATA in a global to avoid querying LaunchServices more than
# once per session
APPDATA = None
@Memoize
def getAppData():
"""Gets info on currently installed apps.
Returns a list of dicts containing path, name, version and bundleid"""
global APPDATA
if APPDATA is None:
APPDATA = []
display_debug1('Getting info on currently installed applications...')
applist = set(getLSInstalledApplications())
applist.update(getSpotlightInstalledApplications())
for pathname in applist:
iteminfo = {}
iteminfo['name'] = os.path.splitext(os.path.basename(pathname))[0]
iteminfo['path'] = pathname
plistpath = os.path.join(pathname, 'Contents', 'Info.plist')
if os.path.exists(plistpath):
try:
plist = FoundationPlist.readPlist(plistpath)
iteminfo['bundleid'] = plist.get('CFBundleIdentifier', '')
if 'CFBundleName' in plist:
iteminfo['name'] = plist['CFBundleName']
iteminfo['version'] = getBundleVersion(pathname)
APPDATA.append(iteminfo)
except BaseException:
pass
else:
# possibly a non-bundle app. Use system_profiler data
# to get app name and version
sp_app_data = getSPApplicationData()
if pathname in sp_app_data:
item = sp_app_data[pathname]
iteminfo['bundleid'] = ''
iteminfo['version'] = item.get('version') or '0.0.0.0.0'
if item.get('_name'):
iteminfo['name'] = item['_name']
APPDATA.append(iteminfo)
return APPDATA
app_data = []
display.display_debug1(
'Getting info on currently installed applications...')
applist = set(getLSInstalledApplications())
applist.update(getSpotlightInstalledApplications())
for pathname in applist:
iteminfo = {}
iteminfo['name'] = os.path.splitext(os.path.basename(pathname))[0]
iteminfo['path'] = pathname
plistpath = os.path.join(pathname, 'Contents', 'Info.plist')
if os.path.exists(plistpath):
try:
plist = FoundationPlist.readPlist(plistpath)
iteminfo['bundleid'] = plist.get('CFBundleIdentifier', '')
if 'CFBundleName' in plist:
iteminfo['name'] = plist['CFBundleName']
iteminfo['version'] = pkgutils.getBundleVersion(pathname)
app_data.append(iteminfo)
except BaseException:
pass
else:
# possibly a non-bundle app. Use system_profiler data
# to get app name and version
sp_app_data = getSPApplicationData()
if pathname in sp_app_data:
item = sp_app_data[pathname]
iteminfo['bundleid'] = ''
iteminfo['version'] = item.get('version') or '0.0.0.0.0'
if item.get('_name'):
iteminfo['name'] = item['_name']
app_data.append(iteminfo)
return app_data
def get_version():
@@ -598,7 +593,7 @@ def getIntel64Support():
libc.sysctlbyname(
"hw.optional.x86_64", ctypes.byref(buf), ctypes.byref(size), None, 0)
return (buf.value == 1)
return buf.value == 1
def getAvailableDiskSpace(volumepath='/'):
@@ -614,7 +609,7 @@ def getAvailableDiskSpace(volumepath='/'):
try:
st = os.statvfs(volumepath)
except OSError, e:
display_error(
display.display_error(
'Error getting disk space in %s: %s', volumepath, str(e))
return 0
@@ -628,7 +623,7 @@ def getMachineFacts():
machine = dict()
machine['hostname'] = os.uname()[1]
machine['arch'] = os.uname()[4]
machine['os_vers'] = getOsVersion(only_major_minor=False)
machine['os_vers'] = osutils.getOsVersion(only_major_minor=False)
hardware_info = get_hardware_info()
machine['machine_model'] = hardware_info.get('machine_model', 'UNKNOWN')
machine['munki_version'] = get_version()
@@ -647,7 +642,7 @@ def validPlist(path):
"""Uses plutil to determine if path contains a valid plist.
Returns True or False."""
retcode = subprocess.call(['/usr/bin/plutil', '-lint', '-s', path])
return (retcode == 0)
return retcode == 0
@Memoize
@@ -660,7 +655,7 @@ def getConditions():
conditionalscriptdir = os.path.join(scriptdir, "conditions")
# define path to ConditionalItems.plist
conditionalitemspath = os.path.join(
pref('ManagedInstallDir'), 'ConditionalItems.plist')
prefs.pref('ManagedInstallDir'), 'ConditionalItems.plist')
try:
# delete CondtionalItems.plist so that we're starting fresh
os.unlink(conditionalitemspath)
@@ -668,7 +663,7 @@ def getConditions():
pass
if os.path.exists(conditionalscriptdir):
from munkilib import utils
for conditionalscript in listdir(conditionalscriptdir):
for conditionalscript in osutils.listdir(conditionalscriptdir):
if conditionalscript.startswith('.'):
# skip files that start with a period
continue
@@ -704,7 +699,7 @@ def saveappdata():
"""Save installed application data"""
# data from getAppData() is meant for use by updatecheck
# we need to massage it a bit for more general usage
log('Saving application inventory...')
munkilog.log('Saving application inventory...')
app_inventory = []
for item in getAppData():
inventory_item = {}
@@ -720,16 +715,11 @@ def saveappdata():
FoundationPlist.writePlist(
app_inventory,
os.path.join(
pref('ManagedInstallDir'), 'ApplicationInventory.plist'))
prefs.pref('ManagedInstallDir'), 'ApplicationInventory.plist'))
except FoundationPlist.NSPropertyListSerializationException, err:
display_warning(
display.display_warning(
'Unable to save inventory report: %s' % err)
def main():
"""Placeholder"""
print 'This is a library of support tools for the Munki Suite.'
if __name__ == '__main__':
main()
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -0,0 +1,134 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2016 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.
"""
munkilog.py
Created by Greg Neagle on 2016-12-14.
Logging functions for Munki
"""
import logging
import logging.handlers
import os
import time
from . import prefs
def log(msg, logname=''):
"""Generic logging function."""
if len(msg) > 1000:
# See http://bugs.python.org/issue11907 and RFC-3164
# break up huge msg into chunks and send 1000 characters at a time
msg_buffer = msg
while msg_buffer:
logging.info(msg_buffer[:1000])
msg_buffer = msg_buffer[1000:]
else:
logging.info(msg) # noop unless configure_syslog() is called first.
# date/time format string
formatstr = '%b %d %Y %H:%M:%S %z'
if not logname:
# use our regular logfile
logpath = prefs.pref('LogFile')
else:
logpath = os.path.join(os.path.dirname(prefs.pref('LogFile')), logname)
try:
fileobj = open(logpath, mode='a', buffering=1)
try:
print >> fileobj, time.strftime(formatstr), msg.encode('UTF-8')
except (OSError, IOError):
pass
fileobj.close()
except (OSError, IOError):
pass
def configure_syslog():
"""Configures logging to system.log, when pref('LogToSyslog') == True."""
logger = logging.getLogger()
# Remove existing handlers to avoid sending unexpected messages.
for handler in logger.handlers:
logger.removeHandler(handler)
logger.setLevel(logging.DEBUG)
# If /System/Library/LaunchDaemons/com.apple.syslogd.plist is restarted
# then /var/run/syslog stops listening. If we fail to catch this then
# Munki completely errors.
try:
syslog = logging.handlers.SysLogHandler('/var/run/syslog')
except BaseException:
log('LogToSyslog is enabled but socket connection failed.')
return
syslog.setFormatter(logging.Formatter('munki: %(message)s'))
syslog.setLevel(logging.INFO)
logger.addHandler(syslog)
def rotatelog(logname=''):
"""Rotate a log"""
if not logname:
# use our regular logfile
logpath = prefs.pref('LogFile')
else:
logpath = os.path.join(os.path.dirname(prefs.pref('LogFile')), logname)
if os.path.exists(logpath):
for i in range(3, -1, -1):
try:
os.unlink(logpath + '.' + str(i + 1))
except (OSError, IOError):
pass
try:
os.rename(logpath + '.' + str(i), logpath + '.' + str(i + 1))
except (OSError, IOError):
pass
try:
os.rename(logpath, logpath + '.0')
except (OSError, IOError):
pass
def rotate_main_log():
"""Rotate our main log"""
main_log = prefs.pref('LogFile')
if os.path.exists(main_log):
if os.path.getsize(main_log) > 1000000:
rotatelog(main_log)
def reset_warnings():
"""Rotate our warnings log."""
warningsfile = os.path.join(
os.path.dirname(prefs.pref('LogFile')), 'warnings.log')
if os.path.exists(warningsfile):
rotatelog(warningsfile)
def reset_errors():
"""Rotate our errors.log"""
errorsfile = os.path.join(
os.path.dirname(prefs.pref('LogFile')), 'errors.log')
if os.path.exists(errorsfile):
rotatelog(errorsfile)
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -35,7 +35,7 @@ import tempfile
from SystemConfiguration import SCDynamicStoreCopyConsoleUser
# pylint: enable=E0611
from .output import display_warning
from . import display
# we use lots of camelCase-style names. Deal with it.
# pylint: disable=C0103
@@ -72,7 +72,7 @@ def cleanUpTmpDir():
try:
shutil.rmtree(_TMPDIR)
except (OSError, IOError), err:
display_warning(
display.display_warning(
'Unable to clean up temporary dir %s: %s', _TMPDIR, str(err))
_TMPDIR = None
@@ -183,10 +183,5 @@ def osascript(osastring):
_TMPDIR = None
def main():
"""Placeholder"""
print 'This is a library of support tools for the Munki Suite.'
if __name__ == '__main__':
main()
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -1,475 +0,0 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2016 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.
"""
output.py
Created by Greg Neagle on 2016-12-13.
Common output, logging, and reporting functions
"""
import logging
import logging.handlers
import os
import subprocess
import sys
import time
import warnings
# 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 NSDate
# pylint: enable=E0611
from .. import munkistatus
from .prefs import pref
from .. import FoundationPlist
# output functions
def getsteps(num_of_steps, limit):
"""
Helper function for display_percent_done
"""
steps = []
current = 0.0
for i in range(0, num_of_steps):
if i == num_of_steps-1:
steps.append(int(round(limit)))
else:
steps.append(int(round(current)))
current += float(limit)/float(num_of_steps-1)
return steps
def display_percent_done(current, maximum):
"""
Mimics the command-line progress meter seen in some
of Apple's tools (like softwareupdate), or tells
MunkiStatus to display percent done via progress bar.
"""
if munkistatusoutput:
step = getsteps(21, maximum)
if current in step:
if current == maximum:
percentdone = 100
else:
percentdone = int(float(current)/float(maximum)*100)
munkistatus.percent(str(percentdone))
elif verbose > 0:
step = getsteps(16, maximum)
output = ''
indicator = ['\t0', '.', '.', '20', '.', '.', '40', '.', '.',
'60', '.', '.', '80', '.', '.', '100\n']
for i in range(0, 16):
if current >= step[i]:
output += indicator[i]
if output:
sys.stdout.write('\r' + output)
sys.stdout.flush()
def str_to_ascii(a_string):
"""Given str (unicode, latin-1, or not) return ascii.
Args:
s: str, likely in Unicode-16BE, UTF-8, or Latin-1 charset
Returns:
str, ascii form, no >7bit chars
"""
try:
return unicode(a_string).encode('ascii', 'ignore')
except UnicodeDecodeError:
return a_string.decode('ascii', 'ignore')
def to_unicode(obj, encoding='UTF-8'):
"""Coerces basestring obj to unicode"""
if isinstance(obj, basestring):
if not isinstance(obj, unicode):
obj = unicode(obj, encoding)
return obj
def concat_log_message(msg, *args):
"""Concatenates a string with any additional arguments,
making sure everything is unicode"""
# coerce msg to unicode if it's not already
msg = to_unicode(msg)
if args:
# coerce all args to unicode as well
args = [to_unicode(arg) for arg in args]
try:
msg = msg % tuple(args)
except TypeError, dummy_err:
warnings.warn(
'String format does not match concat args: %s'
% (str(sys.exc_info())))
return msg.rstrip()
def display_status_major(msg, *args):
"""
Displays major status messages, formatting as needed
for verbose/non-verbose and munkistatus-style output.
"""
msg = concat_log_message(msg, *args)
log(msg)
if munkistatusoutput:
munkistatus.message(msg)
munkistatus.detail('')
munkistatus.percent(-1)
elif verbose > 0:
if msg.endswith('.') or msg.endswith(u''):
print '%s' % msg.encode('UTF-8')
else:
print '%s...' % msg.encode('UTF-8')
sys.stdout.flush()
def display_status_minor(msg, *args):
"""
Displays minor status messages, formatting as needed
for verbose/non-verbose and munkistatus-style output.
"""
msg = concat_log_message(msg, *args)
log(u' ' + msg)
if munkistatusoutput:
munkistatus.detail(msg)
elif verbose > 0:
if msg.endswith('.') or msg.endswith(u''):
print ' %s' % msg.encode('UTF-8')
else:
print ' %s...' % msg.encode('UTF-8')
sys.stdout.flush()
def display_info(msg, *args):
"""
Displays info messages.
Not displayed in MunkiStatus.
"""
msg = concat_log_message(msg, *args)
log(u' ' + msg)
if munkistatusoutput:
pass
elif verbose > 0:
print ' %s' % msg.encode('UTF-8')
sys.stdout.flush()
def display_detail(msg, *args):
"""
Displays minor info messages.
Not displayed in MunkiStatus.
These are usually logged only, but can be printed to
stdout if verbose is set greater than 1
"""
msg = concat_log_message(msg, *args)
if munkistatusoutput:
pass
elif verbose > 1:
print ' %s' % msg.encode('UTF-8')
sys.stdout.flush()
if pref('LoggingLevel') > 0:
log(u' ' + msg)
def display_debug1(msg, *args):
"""
Displays debug messages, formatting as needed
for verbose/non-verbose and munkistatus-style output.
"""
msg = concat_log_message(msg, *args)
if munkistatusoutput:
pass
elif verbose > 2:
print ' %s' % msg.encode('UTF-8')
sys.stdout.flush()
if pref('LoggingLevel') > 1:
log('DEBUG1: %s' % msg)
def display_debug2(msg, *args):
"""
Displays debug messages, formatting as needed
for verbose/non-verbose and munkistatus-style output.
"""
msg = concat_log_message(msg, *args)
if munkistatusoutput:
pass
elif verbose > 3:
print ' %s' % msg.encode('UTF-8')
if pref('LoggingLevel') > 2:
log('DEBUG2: %s' % msg)
def reset_warnings():
"""Rotate our warnings log."""
warningsfile = os.path.join(os.path.dirname(pref('LogFile')),
'warnings.log')
if os.path.exists(warningsfile):
rotatelog(warningsfile)
def display_warning(msg, *args):
"""
Prints warning msgs to stderr and the log
"""
msg = concat_log_message(msg, *args)
warning = 'WARNING: %s' % msg
if verbose > 0:
print >> sys.stderr, warning.encode('UTF-8')
log(warning)
# append this warning to our warnings log
log(warning, 'warnings.log')
# collect the warning for later reporting
if not 'Warnings' in report:
report['Warnings'] = []
report['Warnings'].append('%s' % msg)
def reset_errors():
"""Rotate our errors.log"""
errorsfile = os.path.join(os.path.dirname(pref('LogFile')), 'errors.log')
if os.path.exists(errorsfile):
rotatelog(errorsfile)
def display_error(msg, *args):
"""
Prints msg to stderr and the log
"""
msg = concat_log_message(msg, *args)
errmsg = 'ERROR: %s' % msg
if verbose > 0:
print >> sys.stderr, errmsg.encode('UTF-8')
log(errmsg)
# append this error to our errors log
log(errmsg, 'errors.log')
# collect the errors for later reporting
if not 'Errors' in report:
report['Errors'] = []
report['Errors'].append('%s' % msg)
# logging functions
def format_time(timestamp=None):
"""Return timestamp as an ISO 8601 formatted string, in the current
timezone.
If timestamp isn't given the current time is used."""
if timestamp is None:
return str(NSDate.new())
else:
return str(NSDate.dateWithTimeIntervalSince1970_(timestamp))
def validateDateFormat(datetime_string):
"""Returns a formatted date/time string"""
formatted_datetime_string = ''
try:
formatted_datetime_string = time.strftime(
'%Y-%m-%dT%H:%M:%SZ', time.strptime(datetime_string,
'%Y-%m-%dT%H:%M:%SZ'))
except BaseException:
pass
return formatted_datetime_string
def log(msg, logname=''):
"""Generic logging function."""
if len(msg) > 1000:
# See http://bugs.python.org/issue11907 and RFC-3164
# break up huge msg into chunks and send 1000 characters at a time
msg_buffer = msg
while msg_buffer:
logging.info(msg_buffer[:1000])
msg_buffer = msg_buffer[1000:]
else:
logging.info(msg) # noop unless configure_syslog() is called first.
# date/time format string
formatstr = '%b %d %Y %H:%M:%S %z'
if not logname:
# use our regular logfile
logpath = pref('LogFile')
else:
logpath = os.path.join(os.path.dirname(pref('LogFile')), logname)
try:
fileobj = open(logpath, mode='a', buffering=1)
try:
print >> fileobj, time.strftime(formatstr), msg.encode('UTF-8')
except (OSError, IOError):
pass
fileobj.close()
except (OSError, IOError):
pass
def configure_syslog():
"""Configures logging to system.log, when pref('LogToSyslog') == True."""
logger = logging.getLogger()
# Remove existing handlers to avoid sending unexpected messages.
for handler in logger.handlers:
logger.removeHandler(handler)
logger.setLevel(logging.DEBUG)
# If /System/Library/LaunchDaemons/com.apple.syslogd.plist is restarted
# then /var/run/syslog stops listening. If we fail to catch this then
# Munki completely errors.
try:
syslog = logging.handlers.SysLogHandler('/var/run/syslog')
except:
log('LogToSyslog is enabled but socket connection failed.')
return
syslog.setFormatter(logging.Formatter('munki: %(message)s'))
syslog.setLevel(logging.INFO)
logger.addHandler(syslog)
def rotatelog(logname=''):
"""Rotate a log"""
if not logname:
# use our regular logfile
logpath = pref('LogFile')
else:
logpath = os.path.join(os.path.dirname(pref('LogFile')), logname)
if os.path.exists(logpath):
for i in range(3, -1, -1):
try:
os.unlink(logpath + '.' + str(i + 1))
except (OSError, IOError):
pass
try:
os.rename(logpath + '.' + str(i), logpath + '.' + str(i + 1))
except (OSError, IOError):
pass
try:
os.rename(logpath, logpath + '.0')
except (OSError, IOError):
pass
def rotate_main_log():
"""Rotate our main log"""
if os.path.exists(pref('LogFile')):
if os.path.getsize(pref('LogFile')) > 1000000:
rotatelog(pref('LogFile'))
# reporting functions
def printreportitem(label, value, indent=0):
"""Prints a report item in an 'attractive' way"""
indentspace = ' '
if type(value) == type(None):
print indentspace*indent, '%s: !NONE!' % label
elif type(value) == list or type(value).__name__ == 'NSCFArray':
if label:
print indentspace*indent, '%s:' % label
index = 0
for item in value:
index += 1
printreportitem(index, item, indent+1)
elif type(value) == dict or type(value).__name__ == 'NSCFDictionary':
if label:
print indentspace*indent, '%s:' % label
for subkey in value.keys():
printreportitem(subkey, value[subkey], indent+1)
else:
print indentspace*indent, '%s: %s' % (label, value)
def printreport(reportdict):
"""Prints the report dictionary in a pretty(?) way"""
for key in reportdict.keys():
printreportitem(key, reportdict[key])
def savereport():
"""Save our report"""
FoundationPlist.writePlist(
report, os.path.join(pref('ManagedInstallDir'),
'ManagedInstallReport.plist'))
def readreport():
"""Read report data from file"""
global report
reportfile = os.path.join(pref('ManagedInstallDir'),
'ManagedInstallReport.plist')
try:
report = FoundationPlist.readPlist(reportfile)
except FoundationPlist.NSPropertyListSerializationException:
report = {}
def archive_report():
"""Archive a report"""
reportfile = os.path.join(pref('ManagedInstallDir'),
'ManagedInstallReport.plist')
if os.path.exists(reportfile):
modtime = os.stat(reportfile).st_mtime
formatstr = '%Y-%m-%d-%H%M%S'
archivename = ('ManagedInstallReport-%s.plist'
% time.strftime(formatstr, time.localtime(modtime)))
archivepath = os.path.join(pref('ManagedInstallDir'), 'Archives')
if not os.path.exists(archivepath):
try:
os.mkdir(archivepath)
except (OSError, IOError):
display_warning('Could not create report archive path.')
try:
os.rename(reportfile, os.path.join(archivepath, archivename))
except (OSError, IOError):
display_warning('Could not archive report.')
# now keep number of archived reports to 100 or fewer
proc = subprocess.Popen(['/bin/ls', '-t1', archivepath],
bufsize=1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(output, dummy_err) = proc.communicate()
if output:
archiveitems = [item
for item in str(output).splitlines()
if item.startswith('ManagedInstallReport-')]
if len(archiveitems) > 100:
for item in archiveitems[100:]:
itempath = os.path.join(archivepath, item)
if os.path.isfile(itempath):
try:
os.unlink(itempath)
except (OSError, IOError):
display_warning(
'Could not remove archive item %s', itempath)
# module globals
#debug = False
verbose = 1
munkistatusoutput = False
report = {}
def main():
"""Placeholder"""
print 'This is a library of support tools for the Munki Suite.'
if __name__ == '__main__':
main()

View File

@@ -33,8 +33,8 @@ from distutils import version
from types import StringType
from xml.dom import minidom
from .osutils import listdir, tmpdir
from .output import display_debug2, display_error, display_warning
from . import osutils
from . import display
from .. import FoundationPlist
# we use lots of camelCase-style names. Deal with it.
@@ -56,8 +56,7 @@ def getPkgRestartInfo(filename):
stderr=subprocess.PIPE)
(out, err) = proc.communicate()
if proc.returncode:
display_error("installer -query failed: %s %s" %
(out.decode('UTF-8'), err.decode('UTF-8')))
display.display_error("installer -query failed: %s %s", out, err)
return {}
if out:
@@ -210,7 +209,7 @@ def getBundleVersion(bundlepath, key=None):
infopath = os.path.join(
bundlepath, 'Contents', 'Resources', 'English.lproj')
if os.path.exists(infopath):
for item in listdir(infopath):
for item in osutils.osutils.listdir(infopath):
if os.path.join(infopath, item).endswith('.info'):
infofile = os.path.join(infopath, item)
fileobj = open(infofile, mode='r')
@@ -316,7 +315,7 @@ def getFlatPackageInfo(pkgpath):
# get the absolute path to the pkg because we need to do a chdir later
abspkgpath = os.path.abspath(pkgpath)
# make a tmp dir to expand the flat package into
pkgtmp = tempfile.mkdtemp(dir=tmpdir())
pkgtmp = tempfile.mkdtemp(dir=osutils.tmpdir())
# record our current working dir
cwd = os.getcwd()
# change into our tmpdir so we can use xar to unarchive the flat package
@@ -340,8 +339,9 @@ def getFlatPackageInfo(pkgpath):
infoarray = parsePkgRefs(packageinfoabspath)
break
else:
display_warning("An error occurred while extracting %s: %s"
% (toc_entry, err))
display.display_warning(
"An error occurred while extracting %s: %s",
toc_entry, err)
# If there are PackageInfo files elsewhere, gather them up
elif toc_entry.endswith('.pkg/PackageInfo'):
cmd_extract = ['/usr/bin/xar', '-xf', abspkgpath, toc_entry]
@@ -351,8 +351,9 @@ def getFlatPackageInfo(pkgpath):
os.path.join(pkgtmp, toc_entry))
infoarray.extend(parsePkgRefs(packageinfoabspath))
else:
display_warning("An error occurred while extracting %s: %s"
% (toc_entry, err))
display.display_warning(
"An error occurred while extracting %s: %s",
toc_entry, err)
if len(infoarray) == 0:
for toc_entry in [item for item in toc
if item.startswith('Distribution')]:
@@ -366,13 +367,15 @@ def getFlatPackageInfo(pkgpath):
path_to_pkg=pkgpath)
break
else:
display_warning("An error occurred while extracting %s: %s"
% (toc_entry, err))
display.display_warning(
"An error occurred while extracting %s: %s",
toc_entry, err)
if len(infoarray) == 0:
display_warning('No valid Distribution or PackageInfo found.')
display.display_warning(
'No valid Distribution or PackageInfo found.')
else:
display_warning(err)
display.display_warning(err)
# change back to original working dir
os.chdir(cwd)
@@ -384,12 +387,12 @@ def getBomList(pkgpath):
'''Gets bom listing from pkgpath, which should be a path
to a bundle-style package'''
bompath = None
for item in listdir(os.path.join(pkgpath, 'Contents')):
for item in osutils.listdir(os.path.join(pkgpath, 'Contents')):
if item.endswith('.bom'):
bompath = os.path.join(pkgpath, 'Contents', item)
break
if not bompath:
for item in listdir(os.path.join(pkgpath, 'Contents', 'Resources')):
for item in osutils.listdir(os.path.join(pkgpath, 'Contents', 'Resources')):
if item.endswith('.bom'):
bompath = os.path.join(pkgpath, 'Contents', 'Resources', item)
break
@@ -443,7 +446,7 @@ def getOnePackageInfo(pkgpath):
infopath = os.path.join(
pkgpath, 'Contents', 'Resources', 'English.lproj')
if os.path.exists(infopath):
for item in listdir(infopath):
for item in osutils.listdir(infopath):
if os.path.join(infopath, item).endswith('.info'):
pkginfo['filename'] = os.path.basename(pkgpath)
pkginfo['packageid'] = os.path.basename(pkgpath)
@@ -478,7 +481,7 @@ def getBundlePackageInfo(pkgpath):
bundlecontents = os.path.join(pkgpath, 'Contents')
if os.path.exists(bundlecontents):
for item in listdir(bundlecontents):
for item in osutils.listdir(bundlecontents):
if item.endswith('.dist'):
filename = os.path.join(bundlecontents, item)
# return info using the distribution file
@@ -500,7 +503,7 @@ def getBundlePackageInfo(pkgpath):
for subdir in dirsToSearch:
searchdir = os.path.join(pkgpath, subdir)
if os.path.exists(searchdir):
for item in listdir(searchdir):
for item in osutils.listdir(searchdir):
itempath = os.path.join(searchdir, item)
if os.path.isdir(itempath):
if itempath.endswith('.pkg'):
@@ -519,7 +522,7 @@ def getReceiptInfo(pkgname):
"""Get receipt info from a package"""
info = []
if hasValidPackageExt(pkgname):
display_debug2('Examining %s' % pkgname)
display.display_debug2('Examining %s' % pkgname)
if os.path.isfile(pkgname): # new flat package
info = getFlatPackageInfo(pkgname)
@@ -557,15 +560,15 @@ def getInstalledPackageVersion(pkgid):
foundbundleid = plist.get('pkgid')
foundvers = plist.get('pkg-version', '0.0.0.0.0')
if pkgid == foundbundleid:
display_debug2('\tThis machine has %s, version %s',
pkgid, foundvers)
display.display_debug2('\tThis machine has %s, version %s',
pkgid, foundvers)
return foundvers
# If we got to this point, we haven't found the pkgid yet.
# Check /Library/Receipts
receiptsdir = '/Library/Receipts'
if os.path.exists(receiptsdir):
installitems = listdir(receiptsdir)
installitems = osutils.listdir(receiptsdir)
highestversion = '0'
for item in installitems:
if item.endswith('.pkg'):
@@ -580,13 +583,13 @@ def getInstalledPackageVersion(pkgid):
highestversion = foundvers
if highestversion != '0':
display_debug2('\tThis machine has %s, version %s',
pkgid, highestversion)
display.display_debug2('\tThis machine has %s, version %s',
pkgid, highestversion)
return highestversion
# This package does not appear to be currently installed
display_debug2('\tThis machine does not have %s' % pkgid)
display.display_debug2('\tThis machine does not have %s' % pkgid)
return ""
@@ -756,11 +759,35 @@ def getPackageMetaData(pkgitem):
return cataloginfo
def main():
"""Placeholder"""
print 'This is a library of support tools for the Munki Suite.'
# This function doesn't really have anything to do with packages or receipts
# but is used by makepkginfo, munkiimport, and installer.py, so it might as
# well live here for now
def isApplication(pathname):
"""Returns true if path appears to be an OS X application"""
# No symlinks, please
if os.path.islink(pathname):
return False
if pathname.endswith('.app'):
return True
if os.path.isdir(pathname):
# look for app bundle structure
# use Info.plist to determine the name of the executable
infoplist = os.path.join(pathname, 'Contents', 'Info.plist')
if os.path.exists(infoplist):
plist = FoundationPlist.readPlist(infoplist)
if 'CFBundlePackageType' in plist:
if plist['CFBundlePackageType'] != 'APPL':
return False
# get CFBundleExecutable,
# falling back to bundle name if it's missing
bundleexecutable = plist.get(
'CFBundleExecutable', os.path.basename(pathname))
bundleexecutablepath = os.path.join(
pathname, 'Contents', 'MacOS', bundleexecutable)
if os.path.exists(bundleexecutablepath):
return True
return False
if __name__ == '__main__':
main()
print 'This is a library of support tools for the Munki Suite.'

7
code/client/munkilib/munkicommon/prefs.py Executable file → Normal file
View File

@@ -190,10 +190,5 @@ def pref(pref_name):
return pref_value
def main():
"""Placeholder"""
print 'This is a library of support tools for the Munki Suite.'
if __name__ == '__main__':
main()
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -23,9 +23,12 @@ Created by Greg Neagle on 2016-12-14.
Functions for finding, listing, etc processes
"""
import os
import signal
import subprocess
from .output import display_debug1, display_detail
from .constants import LOGINWINDOW
from . import display
# we use lots of camelCase-style names. Deal with it.
# pylint: disable=C0103
@@ -64,7 +67,7 @@ def getRunningProcesses():
def isAppRunning(appname):
"""Tries to determine if the application in appname is currently
running"""
display_detail('Checking if %s is running...' % appname)
display.display_detail('Checking if %s is running...' % appname)
proc_list = getRunningProcesses()
matching_items = []
if appname.startswith('/'):
@@ -86,14 +89,39 @@ def isAppRunning(appname):
if matching_items:
# it's running!
display_debug1('Matching process list: %s' % matching_items)
display_detail('%s is running!' % appname)
display.display_debug1('Matching process list: %s' % matching_items)
display.display_detail('%s is running!' % appname)
return True
# if we get here, we have no evidence that appname is running
return False
def blockingApplicationsRunning(pkginfoitem):
"""Returns true if any application in the blocking_applications list
is running or, if there is no blocking_applications list, if any
application in the installs list is running."""
if 'blocking_applications' in pkginfoitem:
appnames = pkginfoitem['blocking_applications']
else:
# if no blocking_applications specified, get appnames
# from 'installs' list if it exists
appnames = [os.path.basename(item.get('path'))
for item in pkginfoitem.get('installs', [])
if item['type'] == 'application']
display.display_debug1("Checking for %s" % appnames)
running_apps = [appname for appname in appnames
if isAppRunning(appname)]
if running_apps:
display.display_detail(
"Blocking apps for %s are running:" % pkginfoitem['name'])
display.display_detail(" %s" % running_apps)
return True
return False
def findProcesses(user=None, exe=None):
"""Find processes in process list.
@@ -111,7 +139,8 @@ def findProcesses(user=None, exe=None):
list of pids, or {} if none
"""
argv = ['/bin/ps', '-x', '-w', '-w', '-a', '-o', 'pid=,user=,comm=']
ps_proc = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
ps_proc = subprocess.Popen(
argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, dummy_stderr) = ps_proc.communicate()
pids = {}
@@ -141,10 +170,32 @@ def findProcesses(user=None, exe=None):
return pids
def main():
"""Placeholder"""
print 'This is a library of support tools for the Munki Suite.'
def forceLogoutNow():
"""Force the logout of interactive GUI users and spawn MSU."""
try:
procs = findProcesses(exe=LOGINWINDOW)
users = {}
for pid in procs:
users[procs[pid]['user']] = pid
if 'root' in users:
del users['root']
# force MSU GUI to raise
fileref = open('/private/tmp/com.googlecode.munki.installatlogout', 'w')
fileref.close()
# kill loginwindows to cause logout of current users, whether
# active or switched away via fast user switching.
for user in users:
try:
os.kill(users[user], signal.SIGKILL)
except OSError:
pass
except BaseException, err:
display.display_error('Exception in forceLogoutNow(): %s' % str(err))
if __name__ == '__main__':
main()
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -0,0 +1,150 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2016 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.
"""
reports.py
Created by Greg Neagle on 2016-12-14.
Reporting functions
"""
import os
import subprocess
import sys
import time
# 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 NSDate
# pylint: enable=E0611
from . import munkilog
from . import prefs
from .. import FoundationPlist
def format_time(timestamp=None):
"""Return timestamp as an ISO 8601 formatted string, in the current
timezone.
If timestamp isn't given the current time is used."""
if timestamp is None:
return str(NSDate.new())
else:
return str(NSDate.dateWithTimeIntervalSince1970_(timestamp))
def printreportitem(label, value, indent=0):
"""Prints a report item in an 'attractive' way"""
indentspace = ' '
if type(value) == type(None):
print indentspace*indent, '%s: !NONE!' % label
elif type(value) == list or type(value).__name__ == 'NSCFArray':
if label:
print indentspace*indent, '%s:' % label
index = 0
for item in value:
index += 1
printreportitem(index, item, indent+1)
elif type(value) == dict or type(value).__name__ == 'NSCFDictionary':
if label:
print indentspace*indent, '%s:' % label
for subkey in value.keys():
printreportitem(subkey, value[subkey], indent+1)
else:
print indentspace*indent, '%s: %s' % (label, value)
def printreport(reportdict):
"""Prints the report dictionary in a pretty(?) way"""
for key in reportdict.keys():
printreportitem(key, reportdict[key])
def savereport():
"""Save our report"""
FoundationPlist.writePlist(
report, os.path.join(prefs.pref('ManagedInstallDir'),
'ManagedInstallReport.plist'))
def readreport():
"""Read report data from file"""
global report
reportfile = os.path.join(prefs.pref('ManagedInstallDir'),
'ManagedInstallReport.plist')
try:
report = FoundationPlist.readPlist(reportfile)
except FoundationPlist.NSPropertyListSerializationException:
report = {}
def _warn(msg):
"""We can't use display module functions here because that would require
circular imports. So a partial reimplementation."""
warning = 'WARNING: %s' % msg
print >> sys.stderr, warning.encode('UTF-8')
munkilog.log(warning)
# append this warning to our warnings log
munkilog.log(warning, 'warnings.log')
def archive_report():
"""Archive a report"""
reportfile = os.path.join(prefs.pref('ManagedInstallDir'),
'ManagedInstallReport.plist')
if os.path.exists(reportfile):
modtime = os.stat(reportfile).st_mtime
formatstr = '%Y-%m-%d-%H%M%S'
archivename = ('ManagedInstallReport-%s.plist'
% time.strftime(formatstr, time.localtime(modtime)))
archivepath = os.path.join(prefs.pref('ManagedInstallDir'), 'Archives')
if not os.path.exists(archivepath):
try:
os.mkdir(archivepath)
except (OSError, IOError):
_warn('Could not create report archive path.')
try:
os.rename(reportfile, os.path.join(archivepath, archivename))
except (OSError, IOError):
_warn('Could not archive report.')
# now keep number of archived reports to 100 or fewer
proc = subprocess.Popen(['/bin/ls', '-t1', archivepath],
bufsize=1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(output, dummy_err) = proc.communicate()
if output:
archiveitems = [item
for item in str(output).splitlines()
if item.startswith('ManagedInstallReport-')]
if len(archiveitems) > 100:
for item in archiveitems[100:]:
itempath = os.path.join(archivepath, item)
if os.path.isfile(itempath):
try:
os.unlink(itempath)
except (OSError, IOError):
_warn('Could not remove archive item %s' % item)
# module globals
report = {}
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'

View File

@@ -0,0 +1,137 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2016 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.
"""
scriptutils.py
Created by Greg Neagle on 2016-12-14.
Functions to run scripts inside Munki
"""
import os
import subprocess
from . import osutils
from . import output
from .. import munkistatus
# we use lots of camelCase-style names. Deal with it.
# pylint: disable=C0103
def _writefile(stringdata, path):
'''Writes string data to path.
Returns the path on success, empty string on failure.'''
try:
fileobject = open(path, mode='w', buffering=1)
# write line-by-line to ensure proper UNIX line-endings
for line in stringdata.splitlines():
print >> fileobject, line.encode('UTF-8')
fileobject.close()
return path
except (OSError, IOError):
output.display_error("Couldn't write %s" % stringdata)
return ""
def runEmbeddedScript(scriptname, pkginfo_item, suppress_error=False):
'''Runs a script embedded in the pkginfo.
Returns the result code.'''
# get the script text from the pkginfo
script_text = pkginfo_item.get(scriptname)
itemname = pkginfo_item.get('name')
if not script_text:
output.display_error(
'Missing script %s for %s' % (scriptname, itemname))
return -1
# write the script to a temp file
scriptpath = os.path.join(osutils.tmpdir(), scriptname)
if _writefile(script_text, scriptpath):
cmd = ['/bin/chmod', '-R', 'o+x', scriptpath]
retcode = subprocess.call(cmd)
if retcode:
output.display_error(
'Error setting script mode in %s for %s'
% (scriptname, itemname))
return -1
else:
output.display_error(
'Cannot write script %s for %s' % (scriptname, itemname))
return -1
# now run the script
return runScript(
itemname, scriptpath, scriptname, suppress_error=suppress_error)
def runScript(itemname, path, scriptname, suppress_error=False):
'''Runs a script, Returns return code.'''
if suppress_error:
output.display_detail(
'Running %s for %s ' % (scriptname, itemname))
else:
output.display_status_minor(
'Running %s for %s ' % (scriptname, itemname))
if output.munkistatusoutput:
# set indeterminate progress bar
munkistatus.percent(-1)
scriptoutput = []
try:
proc = subprocess.Popen(path, shell=False, bufsize=1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
except OSError, err:
output.display_error(
'Error executing script %s: %s' % (scriptname, str(err)))
return -1
while True:
msg = proc.stdout.readline().decode('UTF-8')
if not msg and (proc.poll() != None):
break
# save all script output in case there is
# an error so we can dump it to the log
scriptoutput.append(msg)
msg = msg.rstrip("\n")
output.display_info(msg)
retcode = proc.poll()
if retcode and not suppress_error:
output.display_error(
'Running %s for %s failed.' % (scriptname, itemname))
output.display_error("-"*78)
for line in scriptoutput:
output.display_error("\t%s" % line.rstrip("\n"))
output.display_error("-"*78)
elif not suppress_error:
output.log('Running %s for %s was successful.' % (scriptname, itemname))
if output.munkistatusoutput:
# clear indeterminate progress bar
munkistatus.percent(0)
return retcode
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'