Files
munki/code/client/ManagedInstaller
Greg Neagle 59f69cf162 Major rewrite and refactoring of the core tools.
installcheck replaces catalogcheck.py.  installcheck supports the new catalog format and the new dependencies.  Cleaned up output and logging.
ManagedInstaller and removepackages tweaked for better logging and MunkiStatus output.
Removed the logout hook examples (for now)
makecatalogitem is now makepkginfo
New makecatalogs tool.

git-svn-id: http://munki.googlecode.com/svn/trunk@50 a4e17f2e-e282-11dd-95e1-755cbddbdd66
2009-05-11 18:03:40 +00:00

519 lines
20 KiB
Python
Executable File

#!/usr/bin/env python
#
# Copyright 2009 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
#
# 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.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
ManagedInstaller
Tool to automatically install pkgs, mpkgs, and dmgs
(containing pkgs and mpkgs) from a defined folder. Intended
to be run as part of a logout hook, but can be run manually
"""
import os
import subprocess
import sys
import time
import plistlib
import optparse
import managedinstalls
import munkistatus
pathtoremovepackages = "/Users/Shared/munki/munki/code/client/removepackages"
def stopRequested():
if options.munkistatusoutput:
if munkistatus.getStopButtonState() == 1:
log("### User stopped session ###")
return True
return False
def cleanup():
if options.munkistatusoutput:
munkistatus.quit()
def createDirsIfNeeded(dirlist):
for dir in dirlist:
if not os.path.exists(dir):
try:
os.makedirs(dir, mode=0755)
except:
print >>sys.stderr, "Could not create %s" % dir
return False
return True
def log(message):
global logdir
logfile = os.path.join(logdir,'ManagedInstaller.log')
try:
f = open(logfile, mode='a', buffering=1)
print >>f, time.ctime(), message
f.close()
except:
pass
def install(pkgpath):
"""
Uses the apple installer to install the package or metapackage
at pkgpath. Prints status messages to STDOUT.
Returns the installer return code and true if a restart is needed.
"""
global options
restartneeded = False
installeroutput = []
cmd = ['/usr/sbin/installer', '-pkginfo', '-pkg', pkgpath]
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
packagename = output.splitlines()[0]
if options.munkistatusoutput:
munkistatus.message("Installing %s..." % packagename)
munkistatus.detail("")
# clear indeterminate progress bar
munkistatus.percent(0)
log("Installing %s from %s" % (packagename, os.path.basename(pkgpath)))
cmd = ['/usr/sbin/installer', '-query', 'RestartAction', '-pkg', pkgpath]
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
restartaction = output.rstrip("\n")
if restartaction == "RequireRestart":
message = "%s requires a restart after installation." % packagename
if options.munkistatusoutput:
munkistatus.detail(message)
else:
print message
sys.stdout.flush()
log(message)
restartneeded = True
cmd = ['/usr/sbin/installer', '-verboseR', '-pkg', pkgpath, '-target', '/']
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while (p.poll() == None):
installinfo = p.stdout.readline()
if installinfo.startswith("installer:"):
# save all installer output in case there is
# an error so we can dump it to the log
installeroutput.append(installinfo)
msg = installinfo[10:].rstrip("\n")
if msg.startswith("PHASE:"):
phase = msg[6:]
if phase:
if options.munkistatusoutput:
munkistatus.detail(phase)
else:
print phase
sys.stdout.flush()
elif msg.startswith("STATUS:"):
status = msg[7:]
if status:
if options.munkistatusoutput:
munkistatus.detail(status)
else:
print status
sys.stdout.flush()
elif msg.startswith("%"):
if options.munkistatusoutput:
percent = float(msg[1:])
percent = int(percent * 100)
munkistatus.percent(percent)
elif msg.startswith(" Error"):
if options.munkistatusoutput:
munkistatus.detail(msg)
else:
print >>sys.stderr, msg
log(msg)
elif msg.startswith(" Cannot install"):
if options.munkistatusoutput:
munkistatus.detail(msg)
else:
print >>sys.stderr, msg
log(msg)
else:
log(msg)
retcode = p.poll()
if retcode:
message = "Install of %s failed." % packagename
if options.munkistatusoutput:
munkistatus.detail(message)
print >>sys.stderr, message
log(message)
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
for line in installeroutput:
print >>sys.stderr, " ", line.rstrip("\n")
log(line.rstrip("\n"))
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
restartneeded = False
else:
log("Install of %s was successful." % packagename)
if options.munkistatusoutput:
munkistatus.percent(100)
return (retcode, restartneeded)
def installall(dirpath):
"""
Attempts to install all pkgs and mpkgs in a given directory.
Will mount dmg files and install pkgs and mpkgs found at the
root of any mountpoints.
"""
restartflag = False
installitems = os.listdir(dirpath)
for item in installitems:
if stopRequested():
return restartflag
itempath = os.path.join(dirpath, item)
if (item.endswith(".pkg") or item.endswith(".mpkg")):
(retcode, needsrestart) = install(itempath)
if needsrestart:
restartflag = True
if item.endswith(".dmg"):
mountpoints = mountdmg(itempath)
if stopRequested():
for mountpoint in mountpoints:
unmountdmg(mountpoint)
return restartflag
for mountpoint in mountpoints:
# install all the pkgs and mpkgs at the root
# of the mountpoint -- call us recursively!
needtorestart = installall(mountpoint)
if needtorestart:
restartflag = True
unmountdmg(mountpoint)
return restartflag
def getInstallCount(installList):
count = 0
for item in installList:
if 'installed' in item:
if not item['installed']:
count +=1
return count
def installWithInfo(dirpath, installlist):
"""
Uses the installlist to install items in the
correct order.
"""
restartflag = False
for item in installlist:
if stopRequested():
return restartflag
if "installer_item" in item:
itempath = os.path.join(dirpath, item["installer_item"])
if not os.path.exists(itempath):
#can't install, so we should stop
return restartflag
if (itempath.endswith(".pkg") or itempath.endswith(".mpkg")):
(retcode, needsrestart) = install(itempath)
if needsrestart:
restartflag = True
if itempath.endswith(".dmg"):
mountpoints = mountdmg(itempath)
if stopRequested():
for mountpoint in mountpoints:
unmountdmg(mountpoint)
return restartflag
for mountpoint in mountpoints:
# install all the pkgs and mpkgs at the root
# of the mountpoint -- call us recursively!
needtorestart = installall(mountpoint)
if needtorestart:
restartflag = True
unmountdmg(mountpoint)
# now remove the item from the install cache
# (using rm -f in case it's a bundle pkg)
retcode = subprocess.call(["/bin/rm", "-rf", itempath])
return restartflag
def getRemovalCount(removalList):
count = 0
for item in removalList:
if 'installed' in item:
if item['installed']:
count +=1
return count
def processRemovals(removalList):
global logdir
restartFlag = False
for item in removalList:
if stopRequested():
return restartFlag
if 'installed' in item:
if item['installed']:
name = item.get('name','')
if 'uninstall_method' in item:
uninstallmethod = item['uninstall_method'].split(' ')
if uninstallmethod[0] == "removepackages":
if 'packages' in item:
if item.get('RestartAction') == "RequireRestart":
restartFlag = True
if options.munkistatusoutput:
munkistatus.message("Removing %s..." % name)
munkistatus.detail("")
# clear indeterminate progress bar
munkistatus.percent(0)
else:
print "Removing %s..." % name
log("Removing %s..." % name)
cmd = [pathtoremovepackages, '-f', '--logfile', os.path.join(logdir,'ManagedInstaller.log')]
if options.munkistatusoutput:
cmd.append('-m')
cmd.append('-d')
for package in item['packages']:
cmd.append(package)
uninstalleroutput = []
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while (p.poll() == None):
msg = p.stdout.readline()
# save all uninstaller output in case there is
# an error so we can dump it to the log
uninstalleroutput.append(msg)
msg = msg.rstrip("\n")
if msg.startswith("STATUS: "):
status = msg[8:]
if status:
print status
sys.stdout.flush()
elif msg.startswith("INFO: "):
info = msg[6:]
if info:
print >>sys.stderr, info
elif msg.startswith("ERROR: "):
error = msg[7:]
if error:
print >>sys.stderr, error
else:
print msg
sys.stdout.flush()
retcode = p.poll()
if retcode:
message = "Uninstall of %s failed." % name
print >>sys.stderr, message
log(message)
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
for line in uninstalleroutput:
print >>sys.stderr, " ", line.rstrip("\n")
log(line.rstrip("\n"))
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
else:
log("Uninstall of %s was successful." % name)
elif os.path.exists(uninstallmethod[0]) and os.access(uninstallmethod[0], os.X_OK):
# it's a script or program to uninstall
if options.munkistatusoutput:
munkistatus.message("Running uninstall script for %s..." % name)
munkistatus.detail("")
# set indeterminate progress bar
munkistatus.percent(-1)
if item.get('RestartAction') == "RequireRestart":
restartFlag = True
cmd = uninstallmethod
uninstalleroutput = []
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while (p.poll() == None):
msg = p.stdout.readline()
# save all uninstaller output in case there is
# an error so we can dump it to the log
uninstalleroutput.append(msg)
msg = msg.rstrip("\n")
if options.munkistatusoutput:
# do nothing with the output
pass
else:
print msg
retcode = p.poll()
if retcode:
message = "Uninstall of %s failed." % name
print >>sys.stderr, message
log(message)
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
for line in uninstalleroutput:
print >>sys.stderr, " ", line.rstrip("\n")
log(line.rstrip("\n"))
message = "-------------------------------------------------"
print >>sys.stderr, message
log(message)
else:
log("Uninstall of %s was successful." % name)
if options.munkistatusoutput:
# clear indeterminate progress bar
munkistatus.percent(0)
else:
log("Uninstall of %s failed because there was no valid uninstall method." % name)
return restartFlag
def mountdmg(dmgpath):
"""
Attempts to mount the dmg at dmgpath
and returns a list of mountpoints
"""
mountpoints = []
dmgname = os.path.basename(dmgpath)
if not options.munkistatusoutput:
print "Mounting disk image %s" % dmgname
log("Mounting disk image %s" % dmgname)
p = subprocess.Popen(['/usr/bin/hdiutil', 'attach', dmgpath, '-mountRandom', '/tmp', '-nobrowse', '-plist'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(plist, err) = p.communicate()
if plist:
pl = plistlib.readPlistFromString(plist)
for entity in pl['system-entities']:
if 'mount-point' in entity:
mountpoints.append(entity['mount-point'])
return mountpoints
def unmountdmg(mountpoint):
"""
Unmounts the dmg at mountpoint
"""
p = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
if err:
print >>sys.stderr, err
p = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint, '-force'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
# module (global) variables
managedinstallbase = managedinstalls.managed_install_dir()
installdir = os.path.join(managedinstallbase , 'Cache')
logdir = os.path.join(managedinstallbase, 'Logs')
p = optparse.OptionParser()
p.add_option('--munkistatusoutput', '-m', action='store_true')
options, arguments = p.parse_args()
def main():
global installdir
needtorestart = False
createDirsIfNeeded([logdir])
log("### Beginning managed installer session ###")
installinfo = os.path.join(managedinstallbase, 'InstallInfo.plist')
if os.path.exists(installinfo):
try:
pl = plistlib.readPlist(installinfo)
except:
print >>sys.stderr, "Invalid %s" % installinfo
exit(-1)
# remove the install info file
# it's no longer valid once we start running
os.unlink(installinfo)
if "removals" in pl:
removalcount = getRemovalCount(pl['removals'])
if removalcount:
if options.munkistatusoutput:
if removalcount == 1:
munkistatus.message("Removing 1 item...")
else:
munkistatus.message("Removing %i items..." % removalcount)
# set indeterminate progress bar
munkistatus.percent(-1)
log("Processing removals")
needtorestart = processRemovals(pl['removals'])
if "managed_installs" in pl:
if not stopRequested():
installcount = getInstallCount(pl['managed_installs'])
if installcount:
if options.munkistatusoutput:
if installcount == 1:
munkistatus.message("Installing 1 item...")
else:
munkistatus.message("Installing %i items..." % installcount)
# set indeterminate progress bar
munkistatus.percent(-1)
log("Processing installs")
needtorestart = installWithInfo(installdir, pl['managed_installs'])
else:
log("No %s found." % installinfo)
if needtorestart:
log("Software installed or removed requires a restart.")
if options.munkistatusoutput:
munkistatus.message("Software installed or removed requires a restart.")
munkistatus.detail("")
munkistatus.percent(-1)
else:
print "Software installed or removed requires a restart."
sys.stdout.flush()
log("### End managed installer session ###")
if needtorestart:
time.sleep(5)
cleanup()
# uncomment this when testing is done so it will restart.
#retcode = subprocess.call(["/sbin/shutdown", "-r", "now"])
else:
cleanup()
if __name__ == '__main__':
main()