Merge branch 'master' into curl-refactor

This commit is contained in:
Greg Neagle
2011-12-20 14:23:20 -08:00
8 changed files with 314 additions and 102 deletions
+2 -2
View File
@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.2</string>
<string>3.3.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>3.2</string>
<string>3.3.0</string>
<key>LSHasLocalizedDisplayName</key>
<true/>
<key>NSMainNibFile</key>
+39 -11
View File
@@ -636,6 +636,29 @@ class MSUAppDelegate(NSObject):
else:
return False
def alertIfRunnningOnBattery(self):
power_info = munki.getPowerInfo()
if (power_info.get('PowerSource') == 'Battery Power'
and power_info.get('BatteryCharge', 0) < 50):
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
NSLocalizedString(u"Your computer is not connected to a power source.", None),
NSLocalizedString(u"Continue", None),
NSLocalizedString(u"Cancel", None),
objc.nil,
NSLocalizedString(u"For best results, you should connect your computer to a power source before updating. Are you sure you want to continue the update?", None))
munki.log("MSU", "alert_on_battery_power")
# making UI consistent with Apple Software Update...
# set Cancel button to be activated by return key
alert.buttons()[1].setKeyEquivalent_('\r')
# set Ccontinue button to be activated by Escape key
alert.buttons()[0].setKeyEquivalent_(chr(27))
buttonPressed = alert.runModal()
if buttonPressed == NSAlertAlternateReturn:
return True
return False
def installSessionErrorAlert(self):
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
NSLocalizedString(u"Cannot start installation session", None),
@@ -653,6 +676,9 @@ class MSUAppDelegate(NSObject):
def logoutAlertDidEnd_returnCode_contextInfo_(self, alert, returncode, contextinfo):
self._currentAlert = None
if returncode == NSAlertDefaultReturn:
if self.alertIfRunnningOnBattery():
munki.log("user", "alerted_on_battery_power_and_cancelled")
return
NSLog("User chose to logout")
munki.log("user", "install_with_logout")
result = munki.logoutAndUpdate()
@@ -666,18 +692,20 @@ class MSUAppDelegate(NSObject):
# another alert
alert.window().orderOut_(self)
if self.alertIfBlockingAppsRunning():
pass
return
if self.alertIfRunnningOnBattery():
munki.log("user", "alerted_on_battery_power_and_cancelled")
return
NSLog("User chose to update without logging out")
munki.log("user", "install_without_logout")
result = munki.justUpdate()
if result:
self.installSessionErrorAlert()
else:
NSLog("User chose to update without logging out")
munki.log("user", "install_without_logout")
result = munki.justUpdate()
if result:
self.installSessionErrorAlert()
else:
self.managedsoftwareupdate_task = "installwithnologout"
self.mainWindowController.theWindow.orderOut_(self)
self.munkiStatusController.window.makeKeyAndOrderFront_(self)
self.munkiStatusController.startMunkiStatusSession()
self.managedsoftwareupdate_task = "installwithnologout"
self.mainWindowController.theWindow.orderOut_(self)
self.munkiStatusController.window.makeKeyAndOrderFront_(self)
self.munkiStatusController.startMunkiStatusSession()
@PyObjCTools.AppHelper.endSheetMethod
+63 -1
View File
@@ -434,7 +434,7 @@ def getRunningBlockingApps(appnames):
if not matching_items:
# try adding '.app' to the name and check again
matching_items = [item for item in proc_list
if '/'+ appname + '.app/' in item]
if '/' + appname + '.app/' in item]
matching_items = set(matching_items)
for path in matching_items:
@@ -447,6 +447,68 @@ def getRunningBlockingApps(appnames):
return list(set(running_apps))
def getPowerInfo():
'''Returns power info in a dictionary'''
power_dict = {}
power_dict['PowerSource'] = 'Unknown Power'
power_dict['BatteryCharge'] = -1
power_dict['ChargingStatus'] = 'unknown'
power_dict['TimeRemaining'] = -1
cmd = ['/usr/bin/pmset', '-g', 'ps']
proc = subprocess.Popen(cmd, bufsize=-1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(output, unused_error) = proc.communicate()
if proc.returncode:
# handle error
return power_dict
#
# output from `pmset -g ps` looks like:
#
# Currently drawing from 'AC Power'
# -InternalBattery-0 100%; charged; 0:00 remaining
#
# or
#
# Currently drawing from 'AC Power'
# -InternalBattery-0 98%; charging; 0:08 remaining
#
# or
#
# Currently drawing from 'Battery Power'
# -InternalBattery-0 100%; discharging; (no estimate)
#
# or
#
# Currently drawing from 'Battery Power'
# -InternalBattery-0 100%; discharging; 5:55 remaining
#
line = output.splitlines()
if 'AC Power' in line[0]:
power_dict['PowerSource'] = 'AC Power'
power_dict['ChargingStatus'] = 'not applicable'
if 'Battery Power' in line[0]:
power_dict['PowerSource'] = 'Battery Power'
if len(line) > 1:
part = line[1].split()
try:
power_dict['BatteryCharge'] = int(part[1].rstrip('%;'))
except (IndexError, ValueError):
pass
try:
power_dict['ChargingStatus'] = part[2].rstrip(';')
except IndexError:
pass
try:
time_remaining_text = part[3]
time_part = time_remaining_text.split(':')
minutes = 60 * int(time_part[0]) + int(time_part[1])
power_dict['TimeRemaining'] = minutes
except (IndexError, ValueError):
pass
return power_dict
def setupLogging(username=None):
"""Setup logging module.
+3 -3
View File
@@ -18,11 +18,11 @@
managedsoftwareupdate
"""
import grp
#import grp
import optparse
import os
import re
import stat
#import stat
import subprocess
import sys
import time
@@ -696,7 +696,7 @@ def main():
if not munkicommon.pref('SuppressAutoInstall'):
doInstallTasks(only_unattended=True)
else:
munkicommon.log('Skipping unattended installs because '
munkicommon.log('Skipping unattended installs because '
'SuppressAutoInstall is true.')
# send a notification event so MSU can update its display
# if needed
+16 -11
View File
@@ -28,16 +28,17 @@ import optparse
import os
import readline
try:
from munkilib import FoundationPlist as plistlib
except ImportError:
try:
import FoundationPlist as plistlib
except ImportError:
# maybe we're not on an OS X machine...
print >> sys.stderr, ("WARNING: FoundationPlist is not available, "
"using plistlib instead.")
import plistlib
#try:
# from munkilib import FoundationPlist as plistlib
#except ImportError:
# try:
# import FoundationPlist as plistlib
# except ImportError:
# # maybe we're not on an OS X machine...
# print >> sys.stderr, ("WARNING: FoundationPlist is not available, "
# "using plistlib instead.")
# import plistlib
import plistlib
try:
from munkilib.munkicommon import get_version
@@ -357,7 +358,11 @@ def find(args):
count = 0
for name in getManifestNames():
pathname = os.path.join(manifests_path, name)
manifest = plistlib.readPlist(pathname)
try:
manifest = plistlib.readPlist(pathname)
except Exception:
print >> sys.stderr, 'Error reading %s' % pathname
continue
if keyname:
if keyname in manifest:
value = manifest[keyname]
+37 -12
View File
@@ -22,6 +22,7 @@ munki module to automatically install pkgs, mpkgs, and dmgs
import datetime
import os
import pwd
import signal
import subprocess
import time
@@ -79,7 +80,8 @@ def removeBundleRelocationInfo(pkgpath):
pass
def install(pkgpath, choicesXMLpath=None, suppressBundleRelocation=False):
def install(pkgpath, choicesXMLpath=None, suppressBundleRelocation=False,
environment=None):
"""
Uses the apple installer to install the package or metapackage
at pkgpath. Prints status messages to STDOUT.
@@ -136,12 +138,28 @@ def install(pkgpath, choicesXMLpath=None, suppressBundleRelocation=False):
if choicesXMLpath:
cmd.extend(['-applyChoiceChangesXML', choicesXMLpath])
# set up environment for installer
env_vars = os.environ.copy()
# get info for root
userinfo = pwd.getpwuid(0)
env_vars['USER'] = userinfo.pw_name
env_vars['HOME'] = userinfo.pw_dir
if environment:
# Munki admin has specified custom installer environment
for key in environment.keys():
if key == 'USER' and environment[key] == 'CURRENT_CONSOLE_USER':
# current console user (if there is one) 'owns' /dev/console
userinfo = pwd.getpwuid(os.stat('/dev/console').st_uid)
env_vars['USER'] = userinfo.pw_name
env_vars['HOME'] = userinfo.pw_dir
else:
env_vars[key] = environment[key]
munkicommon.display_debug1(
'Using custom installer environment variables: %s', env_vars)
# run installer, setting the program id of the process (all child
# processes will also use the same program id), making it easier to kill
# not only hung installer but also any child processes it started.
env_vars = os.environ.copy()
if not 'USER' in env_vars:
env_vars['USER'] = 'root'
proc = munkicommon.Popen(cmd, shell=False, bufsize=1, env=env_vars,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
@@ -222,7 +240,8 @@ def install(pkgpath, choicesXMLpath=None, suppressBundleRelocation=False):
return (retcode, restartneeded)
def installall(dirpath, choicesXMLpath=None, suppressBundleRelocation=False):
def installall(dirpath, choicesXMLpath=None, suppressBundleRelocation=False,
environment=None):
"""
Attempts to install all pkgs and mpkgs in a given directory.
Will mount dmg files and install pkgs and mpkgs found at the
@@ -250,7 +269,8 @@ def installall(dirpath, choicesXMLpath=None, suppressBundleRelocation=False):
# of the mountpoint -- call us recursively!
(retcode, needsrestart) = installall(mountpoint,
choicesXMLpath,
suppressBundleRelocation)
suppressBundleRelocation,
environment)
if needsrestart:
restartflag = True
if retcode:
@@ -262,7 +282,8 @@ def installall(dirpath, choicesXMLpath=None, suppressBundleRelocation=False):
if (item.endswith(".pkg") or item.endswith(".mpkg")):
(retcode, needsrestart) = install(itempath, choicesXMLpath,
suppressBundleRelocation)
suppressBundleRelocation,
environment)
if needsrestart:
restartflag = True
if retcode:
@@ -640,6 +661,7 @@ def installWithInfo(
choicesXMLfile)
else:
choicesXMLfile = ''
installer_environment = item.get('installer_environment')
if itempath.endswith(".dmg"):
munkicommon.display_status("Mounting disk image %s" %
item["installer_item"])
@@ -672,14 +694,16 @@ def installWithInfo(
if os.path.exists(fullpkgpath):
(retcode, needtorestart) = install(fullpkgpath,
choicesXMLfile,
suppressBundleRelocation)
suppressBundleRelocation,
installer_environment)
else:
# no relative path to pkg on dmg, so just install all
# pkgs found at the root of the first mountpoint
# (hopefully there's only one)
(retcode, needtorestart) = installall(mountpoints[0],
choicesXMLfile,
suppressBundleRelocation)
choicesXMLfile,
suppressBundleRelocation,
installer_environment)
if (needtorestart or
item.get("RestartAction") == "RequireRestart" or
item.get("RestartAction") == "RecommendRestart"):
@@ -688,8 +712,9 @@ def installWithInfo(
elif (itempath.endswith(".pkg") or itempath.endswith(".mpkg")
or itempath.endswith(".dist")):
(retcode, needtorestart) = install(itempath,
choicesXMLfile,
suppressBundleRelocation)
choicesXMLfile,
suppressBundleRelocation,
installer_environment)
if (needtorestart or
item.get("RestartAction") == "RequireRestart" or
item.get("RestartAction") == "RecommendRestart"):
+15 -11
View File
@@ -6,9 +6,9 @@
# 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
#
#
# http://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.
@@ -69,7 +69,7 @@ def sendCommand(command_text):
global SOCK
if SOCK == None:
launchAndConnectToMunkiStatus()
if SOCK:
if SOCK:
try:
# we can send only a single line.
messagelines = command_text.splitlines(True)
@@ -97,17 +97,21 @@ def readResponse():
print err, errmsg
SOCK.close()
SOCK = None
return ''
def getPIDforProcessName(processname):
'''Returns a process ID for processname'''
cmd = ['/bin/ps', '-eo', 'pid=,command=']
proc = subprocess.Popen(cmd, shell=False, bufsize=1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True:
try:
proc = subprocess.Popen(cmd, shell=False, bufsize=1,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
except OSError:
return 0
while True:
line = proc.stdout.readline().decode('UTF-8')
if not line and (proc.poll() != None):
break
@@ -121,7 +125,7 @@ def getPIDforProcessName(processname):
else:
if process.find(processname) != -1:
return str(pid)
return 0
@@ -147,7 +151,7 @@ def getMunkiStatusSocket():
socketpath = "/tmp/com.googlecode.munki.munkistatus.%s" % pid
if os.path.exists(socketpath):
return socketpath
# sleep and try again
time.sleep(.25)
return ""
@@ -202,7 +206,7 @@ def showStopButton():
def disableStopButton():
'''Disables (grays out) the stop button.'''
sendCommand(u"DISABLESTOPBUTTON: \n")
sendCommand(u"DISABLESTOPBUTTON: \n")
def enableStopButton():
+139 -51
View File
@@ -21,7 +21,7 @@ Created by Greg Neagle on 2008-11-13.
"""
#standard libs
# standard libs
import calendar
import errno
import os
@@ -33,15 +33,16 @@ import time
import urllib2
import urlparse
import xattr
#from distutils import version
from OpenSSL.crypto import load_certificate, FILETYPE_PEM
#our libs
# our libs
import munkicommon
import munkistatus
import appleupdates
import FoundationPlist
from Foundation import NSDate
# Apple's libs
from Foundation import NSDate, NSPredicate
# This many hours before a force install deadline, start notifying the user.
@@ -1576,6 +1577,7 @@ def processInstall(manifestitem, cataloglist, installinfo):
# optional keys
optional_keys = ['suppress_bundle_relocation',
'installer_choices_xml',
'installer_environment',
'adobe_install_info',
'RestartAction',
'installer_type',
@@ -1662,58 +1664,117 @@ def processInstall(manifestitem, cataloglist, installinfo):
return True
def processManifestForKey(manifestpath, manifest_key, installinfo,
INFO_OBJECT = {}
def makePredicateInfoObject():
if INFO_OBJECT:
return
for key in MACHINE.keys():
INFO_OBJECT[key] = MACHINE[key]
os_vers = MACHINE['os_vers']
os_vers = os_vers + '.0.0'
INFO_OBJECT['os_vers_major'] = int(os_vers.split('.')[0])
INFO_OBJECT['os_vers_minor'] = int(os_vers.split('.')[1])
INFO_OBJECT['os_vers_patch'] = int(os_vers.split('.')[2])
if 'Book' in MACHINE.get('machine_model', ''):
INFO_OBJECT['machine_type'] = 'laptop'
else:
INFO_OBJECT['machine_type'] = 'desktop'
def predicateEvaluatesAsTrue(predicate_string):
'''Evaluates predicate against our info object'''
munkicommon.display_debug1('Evaluating predicate: %s' % predicate_string)
try:
p = NSPredicate.predicateWithFormat_(predicate_string)
except Exception, e:
munkicommon.display_warning('%s' % e)
# can't parse predicate, so return False
return False
result = p.evaluateWithObject_(INFO_OBJECT)
munkicommon.display_debug1(
'Predicate %s is %s' % (predicate_string, result))
return result
def processManifestForKey(manifest, manifest_key, installinfo,
parentcatalogs=None):
"""Processes keys in manifests to build the lists of items to install and
remove.
Can be recursive if manifests include other manifests.
Probably doesn't handle circular manifest references well.
manifest can be a path to a manifest file or a dictionary object.
"""
munkicommon.display_debug1(
"** Processing manifest %s for %s" %
(os.path.basename(manifestpath), manifest_key))
if isinstance(manifest, basestring):
munkicommon.display_debug1(
"** Processing manifest %s for %s" %
(os.path.basename(manifest), manifest_key))
manifestdata = getManifestData(manifest)
else:
manifestdata = manifest
manifest = 'embedded manifest'
cataloglist = getManifestValueForKey(manifestpath, 'catalogs')
cataloglist = manifestdata.get('catalogs')
if cataloglist:
getCatalogs(cataloglist)
elif parentcatalogs:
cataloglist = parentcatalogs
if cataloglist:
nestedmanifests = getManifestValueForKey(manifestpath,
'included_manifests')
if nestedmanifests:
for item in nestedmanifests:
try:
nestedmanifestpath = getmanifest(item)
except ManifestException:
nestedmanifestpath = None
if munkicommon.stopRequested():
return {}
if nestedmanifestpath:
processManifestForKey(nestedmanifestpath, manifest_key,
installinfo, cataloglist)
items = getManifestValueForKey(manifestpath, manifest_key)
if items:
for item in items:
if munkicommon.stopRequested():
return {}
if manifest_key == 'managed_installs':
unused_result = processInstall(item, cataloglist,
installinfo)
elif manifest_key == 'managed_updates':
processManagedUpdate(item, cataloglist, installinfo)
elif manifest_key == 'optional_installs':
processOptionalInstall(item, cataloglist, installinfo)
elif manifest_key == 'managed_uninstalls':
unused_result = processRemoval(item, cataloglist,
installinfo)
else:
if not cataloglist:
munkicommon.display_warning('Manifest %s has no catalogs' %
manifestpath)
return
nestedmanifests = manifestdata.get('included_manifests')
if nestedmanifests:
for item in nestedmanifests:
try:
nestedmanifestpath = getmanifest(item)
except ManifestException:
nestedmanifestpath = None
if munkicommon.stopRequested():
return {}
if nestedmanifestpath:
processManifestForKey(nestedmanifestpath, manifest_key,
installinfo, cataloglist)
conditionalitems = manifestdata.get('conditional_items')
if conditionalitems:
munkicommon.display_debug1(
'** Processing conditional_items in %s' % manifest)
# conditionalitems should be an array of dicts
# each dict has a predicate; the rest consists of the
# same keys as a manifest
for item in conditionalitems:
try:
predicate = item['condition']
except (AttributeError, KeyError):
munkicommon.display_warning(
'Missing predicate for conditional_item %s' % item)
continue
INFO_OBJECT['catalogs'] = cataloglist
if predicateEvaluatesAsTrue(predicate):
conditionalmanifest = item
processManifestForKey(conditionalmanifest, manifest_key,
installinfo, cataloglist)
items = manifestdata.get(manifest_key)
if items:
for item in items:
if munkicommon.stopRequested():
return {}
if manifest_key == 'managed_installs':
unused_result = processInstall(item, cataloglist,
installinfo)
elif manifest_key == 'managed_updates':
processManagedUpdate(item, cataloglist, installinfo)
elif manifest_key == 'optional_installs':
processOptionalInstall(item, cataloglist, installinfo)
elif manifest_key == 'managed_uninstalls':
unused_result = processRemoval(item, cataloglist,
installinfo)
def getReceiptsToRemove(item):
@@ -2003,17 +2064,21 @@ def processRemoval(manifestitem, cataloglist, installinfo):
return True
def getManifestValueForKey(manifestpath, keyname):
"""Returns a value for keyname in manifestpath"""
def getManifestData(manifestpath):
'''Reads a manifest file, returns a
dictionary-like object'''
plist = {}
try:
plist = FoundationPlist.readPlist(manifestpath)
except FoundationPlist.NSPropertyListSerializationException:
munkicommon.display_error('Could not read plist %s' % manifestpath)
return None
if keyname in plist:
return plist[keyname]
else:
return None
return plist
def getManifestValueForKey(manifestpath, keyname):
"""Returns a value for keyname in manifestpath"""
plist = getManifestData(manifestpath)
return plist.get(keyname, None)
# global to hold our catalog DBs
@@ -2811,8 +2876,26 @@ def getHTTPfileIfChangedAtomically(url, destinationpath,
return True
# we only want to call sw_vers and the like once. Since these values don't
# change often, we store the info in MACHINE.
def get_hardware_info():
'''Uses system profiler to get hardware info for this machine'''
cmd = ['/usr/sbin/system_profiler', 'SPHardwareDataType', '-xml']
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, error) = proc.communicate()
try:
plist = FoundationPlist.readPlistFromString(output)
# system_profiler xml is an array
sp_dict = plist[0]
items = sp_dict['_items']
sp_hardware_dict = items[0]
return sp_hardware_dict
except Exception, e:
return {}
# we only want to call system_profiler and the like once. Since these values
# don't change often, we store the info in MACHINE.
MACHINE = {}
def getMachineFacts():
"""Gets some facts about this machine we use to determine if a given
@@ -2822,6 +2905,8 @@ def getMachineFacts():
MACHINE['hostname'] = os.uname()[1]
MACHINE['arch'] = os.uname()[4]
MACHINE['os_vers'] = munkicommon.getOsVersion(only_major_minor=False)
hardware_info = get_hardware_info()
MACHINE['machine_model'] = hardware_info.get('machine_model', 'UNKNOWN')
def check(client_id='', localmanifestpath=None):
@@ -2859,7 +2944,10 @@ def check(client_id='', localmanifestpath=None):
installinfo['optional_installs'] = []
installinfo['managed_installs'] = []
installinfo['removals'] = []
# set up INFO_OBJECT for conditional item comparisons
makePredicateInfoObject()
munkicommon.display_detail('**Checking for installs**')
processManifestForKey(mainmanifestpath, 'managed_installs',
installinfo)