mirror of
https://github.com/munki/munki.git
synced 2026-01-05 22:20:00 -06:00
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
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# 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.
|
||||
|
||||
MIDIR=`defaults read /Library/Preferences/ManagedInstalls managed_install_dir`
|
||||
IHOOK="/Applications/Utilities/radmind/iHook.app/Contents/MacOS/iHook"
|
||||
SCRIPT="/Users/gneagle/Documents/managedinstalls/code/client/ManagedInstaller"
|
||||
ARGS="-i"
|
||||
|
||||
if [ "$MIDIR" != "" ]; then
|
||||
CACHEDIR="$MIDIR/Cache"
|
||||
if [ -d "$CACHEDIR" ]; then
|
||||
CACHECOUNT=`ls -1 "$CACHEDIR" | wc -l`
|
||||
if [ $CACHECOUNT -gt 0 ]; then
|
||||
"$IHOOK" --script="$SCRIPT" "$ARGS"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/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.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import managedinstalls
|
||||
|
||||
managedinstalldir = managedinstalls.managed_install_dir()
|
||||
installinfo = os.path.join(managedinstalldir, "InstallInfo.plist")
|
||||
ihook = "/Applications/Utilities/radmind/iHook.app/Contents/MacOS/iHook"
|
||||
script = "/Users/gneagle/Documents/managedinstalls/code/client/ManagedInstaller"
|
||||
args = "-i"
|
||||
|
||||
if os.path.exists(installinfo):
|
||||
retcode = subprocess.call([ihook, "--script=" + script, args])
|
||||
@@ -59,24 +59,13 @@ def createDirsIfNeeded(dirlist):
|
||||
|
||||
def log(message):
|
||||
global logdir
|
||||
createDirsIfNeeded([logdir])
|
||||
logfile = os.path.join(logdir,'autoinstall.log')
|
||||
f = open(logfile, mode='a', buffering=1)
|
||||
if f:
|
||||
logfile = os.path.join(logdir,'ManagedInstaller.log')
|
||||
try:
|
||||
f = open(logfile, mode='a', buffering=1)
|
||||
print >>f, time.ctime(), message
|
||||
f.close()
|
||||
|
||||
|
||||
def countinstallcandidates(dirpath):
|
||||
"""
|
||||
Get the count of items to be installed
|
||||
"""
|
||||
candidatecount = 0
|
||||
items = os.listdir(dirpath)
|
||||
for item in items:
|
||||
if (item.endswith(".pkg") or item.endswith(".mpkg") or item.endswith(".dmg")):
|
||||
candidatecount += 1
|
||||
return candidatecount
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def install(pkgpath):
|
||||
@@ -95,9 +84,10 @@ def install(pkgpath):
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(output, err) = p.communicate()
|
||||
packagename = output.splitlines()[0]
|
||||
print >>sys.stderr, "Package name is", packagename
|
||||
|
||||
if options.munkistatusoutput:
|
||||
munkistatus.message("Installing %s..." % packagename)
|
||||
munkistatus.detail("")
|
||||
# clear indeterminate progress bar
|
||||
munkistatus.percent(0)
|
||||
|
||||
@@ -162,7 +152,7 @@ def install(pkgpath):
|
||||
print >>sys.stderr, msg
|
||||
log(msg)
|
||||
else:
|
||||
print >>sys.stderr, msg
|
||||
log(msg)
|
||||
|
||||
retcode = p.poll()
|
||||
if retcode:
|
||||
@@ -278,29 +268,32 @@ def getRemovalCount(removalList):
|
||||
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 = ""
|
||||
if 'name' in item:
|
||||
name = item['name']
|
||||
elif 'catalogitem' in item:
|
||||
name = item['catalogitem']
|
||||
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
|
||||
cmd = [pathtoremovepackages, '-f']
|
||||
|
||||
log("Removing %s..." % name)
|
||||
cmd = [pathtoremovepackages, '-f', '--logfile', os.path.join(logdir,'ManagedInstaller.log')]
|
||||
if options.munkistatusoutput:
|
||||
cmd.append('-m')
|
||||
cmd.append('-d')
|
||||
@@ -354,9 +347,13 @@ def processRemovals(removalList):
|
||||
# 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,
|
||||
@@ -391,7 +388,7 @@ def processRemovals(removalList):
|
||||
else:
|
||||
log("Uninstall of %s was successful." % name)
|
||||
|
||||
if options.munkstatusoutput:
|
||||
if options.munkistatusoutput:
|
||||
# clear indeterminate progress bar
|
||||
munkistatus.percent(0)
|
||||
|
||||
@@ -409,7 +406,8 @@ def mountdmg(dmgpath):
|
||||
"""
|
||||
mountpoints = []
|
||||
dmgname = os.path.basename(dmgpath)
|
||||
print "Mounting disk image %s" % dmgname
|
||||
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)
|
||||
@@ -451,65 +449,61 @@ def main():
|
||||
global installdir
|
||||
|
||||
needtorestart = False
|
||||
|
||||
log("### Beginning automated install session ###")
|
||||
if os.path.exists(installdir):
|
||||
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)
|
||||
#log("Installing everything in the cache.")
|
||||
# install all pkgs and mpkgs
|
||||
#needtorestart = installall(installdir)
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
# 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 ###")
|
||||
|
||||
log("### End automated install session ###")
|
||||
if needtorestart:
|
||||
time.sleep(5)
|
||||
cleanup()
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
The actual tools are:
|
||||
|
||||
catalogcheck.py - gets the catalog for the client and processes it to determine what, if anything, needs to be installed.
|
||||
ManagedInstaller - meant to be run as part of a logout hook. Does the actual installs.
|
||||
makecatalogitem.py: Helper tool to help create catalogitem plists for each installer item.
|
||||
installcheck - gets the manifest for the client and processes it to determine what, if anything, needs to be installed.
|
||||
ManagedInstaller - meant to be run as part of a logout hook or as a launchd job. Does the actual installs and removals.
|
||||
ManagedSoftwareUpdate.app - user notification tool.
|
||||
MunkiStatus - used by ManagedInstaller to provide user feedback on the installation and removal process.
|
||||
makepkginfo: Helper tool to help create info files for each installer item.
|
||||
makecatalogs: Creates the software catalogs from the pkginfo files.
|
||||
removepackages: used by ManagedInstaller to do package removals.
|
||||
|
||||
|
||||
Supporting libraries:
|
||||
managedinstalls.py - shared functions
|
||||
munkistatus.py - functions to display status using MunkiStatus.app
|
||||
|
||||
@@ -1,945 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
catalogcheck.py
|
||||
|
||||
Created by Greg Neagle on 2008-11-13.
|
||||
"""
|
||||
|
||||
#standard libs
|
||||
import sys
|
||||
import os
|
||||
import plistlib
|
||||
import tempfile
|
||||
import subprocess
|
||||
from distutils import version
|
||||
import urlparse
|
||||
import optparse
|
||||
import hashlib
|
||||
|
||||
#our lib
|
||||
import managedinstalls
|
||||
|
||||
# appdict is a global so we don't call system_profiler more than once per session
|
||||
appdict = {}
|
||||
def getAppData():
|
||||
"""
|
||||
Queries system_profiler and returns a dict
|
||||
of app info items
|
||||
"""
|
||||
global appdict
|
||||
if appdict == {}:
|
||||
if options.verbose:
|
||||
print "Getting info on currently installed applications..."
|
||||
cmd = ['/usr/sbin/system_profiler', '-XML', 'SPApplicationsDataType']
|
||||
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(plist, err) = p.communicate()
|
||||
if p.returncode == 0:
|
||||
pl = plistlib.readPlistFromString(plist)
|
||||
# top level is an array instead of a dict, so get dict
|
||||
spdict = pl[0]
|
||||
if '_items' in spdict:
|
||||
appdict = spdict['_items']
|
||||
|
||||
return appdict
|
||||
|
||||
|
||||
def getAppBundleID(path):
|
||||
"""
|
||||
Returns CFBundleIdentifier if available
|
||||
for application at path
|
||||
"""
|
||||
infopath = os.path.join(path, "Contents", "Info.plist")
|
||||
if os.path.exists(infopath):
|
||||
try:
|
||||
pl = plistlib.readPlist(infopath)
|
||||
if 'CFBundleIdentifier' in pl:
|
||||
return pl['CFBundleIdentifier']
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def compareVersions(thisvers, thatvers):
|
||||
"""
|
||||
Returns -1 if thisvers is older than thatvers
|
||||
Returns 1 if thisvers is the same as thatvers
|
||||
Returns 2 if thisvers is newer than thatvers
|
||||
"""
|
||||
if version.LooseVersion(thisvers) < version.LooseVersion(thatvers):
|
||||
if options.verbose:
|
||||
print "\tInstalled version is older (%s)." % thisvers
|
||||
return -1
|
||||
elif version.LooseVersion(thisvers) == version.LooseVersion(thatvers):
|
||||
if options.verbose:
|
||||
print "\tThis version is currently installed."
|
||||
return 1
|
||||
else:
|
||||
if options.verbose:
|
||||
print "\tA newer version is currently installed (%s)." % thisvers
|
||||
return 2
|
||||
|
||||
|
||||
def isSameOrNewerApplicationInstalled(app):
|
||||
return compareApplicationVersion(app, newer_ok=True)
|
||||
|
||||
|
||||
def compareApplicationVersion(app, newer_ok=False):
|
||||
"""
|
||||
app is a dict with application
|
||||
bundle info
|
||||
uses system profiler data to look for
|
||||
an app that is the same version
|
||||
(or newer version if newer_ok is True)
|
||||
"""
|
||||
|
||||
name = bundleid = ''
|
||||
if 'CFBundleName' in app:
|
||||
name = app['CFBundleName']
|
||||
if 'CFBundleIdentifier' in app:
|
||||
bundleid = app['CFBundleIdentifier']
|
||||
if 'CFBundleShortVersionString' in app:
|
||||
versionstring = app['CFBundleShortVersionString']
|
||||
|
||||
if name == '' and bundleid == '':
|
||||
print >>sys.stderr,"No application name or bundleid was specified!"
|
||||
# return True so we don't install
|
||||
return True
|
||||
|
||||
if options.verbose:
|
||||
print "Looking for application %s with bundleid: %s, version %s..." % (name, bundleid, versionstring)
|
||||
appinfo = []
|
||||
appdata = getAppData()
|
||||
if appdata:
|
||||
for item in appdata:
|
||||
if bundleid:
|
||||
if 'path' in item:
|
||||
if getAppBundleID(item['path']) == bundleid:
|
||||
appinfo.append(item)
|
||||
elif name:
|
||||
if '_name' in item:
|
||||
if item['_name'] == name:
|
||||
appinfo.append(item)
|
||||
|
||||
for item in appinfo:
|
||||
if '_name' in item:
|
||||
if options.verbose:
|
||||
print "\tName: \t %s" % item['_name'].encode("UTF-8")
|
||||
if 'path' in item:
|
||||
if options.verbose:
|
||||
print "\tPath: \t %s" % item['path'].encode("UTF-8")
|
||||
print "\tCFBundleIdentifier: \t %s" % getAppBundleID(item['path'])
|
||||
if 'version' in item:
|
||||
if options.verbose:
|
||||
print "\tVersion: \t %s" % item['version'].encode("UTF-8")
|
||||
if compareVersions(item['version'], versionstring) == 1:
|
||||
# version is the same
|
||||
return True
|
||||
if newer_ok and compareVersions(item['version'], versionstring) == 2:
|
||||
# version is newer
|
||||
return True
|
||||
|
||||
# if we got this far, we didn't find the same or newer
|
||||
if options.verbose:
|
||||
print "Did not find the same or newer application on the startup disk."
|
||||
return False
|
||||
|
||||
|
||||
def compareBundleVersion(item):
|
||||
"""
|
||||
Returns 0 if the bundle isn't installed
|
||||
or doesn't have valid Info.plist
|
||||
-1 if it's older
|
||||
1 if the version is the same
|
||||
2 if the version is newer
|
||||
-2 if there's an error in the input
|
||||
"""
|
||||
if 'path' in item and 'CFBundleShortVersionString' in item:
|
||||
filepath = os.path.join(item['path'], 'Contents', 'Info.plist')
|
||||
vers = item['CFBundleShortVersionString']
|
||||
else:
|
||||
print >>sys.stderr, "Missing bundle path or version!"
|
||||
return -2
|
||||
|
||||
if options.verbose:
|
||||
print "Checking %s for version %s..." % (filepath, vers)
|
||||
if not os.path.exists(filepath):
|
||||
if options.verbose:
|
||||
print "\tNo Info.plist found at %s" % filepath
|
||||
return 0
|
||||
|
||||
try:
|
||||
pl = plistlib.readPlist(filepath)
|
||||
except:
|
||||
if options.verbose:
|
||||
print "\t%s may not be a plist!" % filepath
|
||||
return 0
|
||||
|
||||
if 'CFBundleShortVersionString' in pl:
|
||||
installedvers = pl['CFBundleShortVersionString']
|
||||
return compareVersions(installedvers, vers)
|
||||
else:
|
||||
if options.verbose:
|
||||
print "\tNo version info in %s." % filepath
|
||||
return 0
|
||||
|
||||
|
||||
def comparePlistVersion(item):
|
||||
"""
|
||||
Gets the CFBundleShortVersionString from the plist
|
||||
at filepath and compares versions.
|
||||
Returns 0 if the plist isn't installed
|
||||
-1 if it's older
|
||||
1 if the version is the same
|
||||
2 if the version is newer
|
||||
-2 if there's an error in the input
|
||||
"""
|
||||
if 'path' in item and 'CFBundleShortVersionString' in item:
|
||||
filepath = item['path']
|
||||
vers = item['CFBundleShortVersionString']
|
||||
else:
|
||||
print >>sys.stderr, "Missing plist path or version!"
|
||||
return -2
|
||||
|
||||
if options.verbose:
|
||||
print "Checking %s for version %s..." % (filepath, vers)
|
||||
if not os.path.exists(filepath):
|
||||
if options.verbose:
|
||||
print "\tNo plist found at %s" % filepath
|
||||
return 0
|
||||
|
||||
try:
|
||||
pl = plistlib.readPlist(filepath)
|
||||
except:
|
||||
if options.verbose:
|
||||
print "\t%s may not be a plist!" % filepath
|
||||
return 0
|
||||
|
||||
if 'CFBundleShortVersionString' in pl:
|
||||
installedvers = pl['CFBundleShortVersionString']
|
||||
return compareVersions(installedvers, vers)
|
||||
else:
|
||||
if options.verbose:
|
||||
print "\tNo version info in %s." % filepath
|
||||
return 0
|
||||
|
||||
|
||||
def getmd5hash(filename):
|
||||
if not os.path.isfile(filename):
|
||||
return "NOT A FILE"
|
||||
|
||||
f = open(filename, 'rb')
|
||||
m = hashlib.md5()
|
||||
while 1:
|
||||
chunk = f.read(2**16)
|
||||
if not chunk:
|
||||
break
|
||||
m.update(chunk)
|
||||
f.close()
|
||||
return m.hexdigest()
|
||||
|
||||
|
||||
def filesystemItemExists(item):
|
||||
"""
|
||||
Checks to see if a filesystem item exists
|
||||
To do: add checksum support
|
||||
"""
|
||||
if 'path' in item:
|
||||
filepath = item['path']
|
||||
if options.verbose:
|
||||
print "Checking existence of %s..." % filepath
|
||||
if os.path.exists(filepath):
|
||||
if options.verbose:
|
||||
print "\tExists."
|
||||
if 'md5checksum' in item:
|
||||
storedchecksum = item['md5checksum']
|
||||
ondiskchecksum = getmd5hash(filepath)
|
||||
if options.verbose:
|
||||
print "Comparing checksums..."
|
||||
if storedchecksum == ondiskchecksum:
|
||||
if options.verbose:
|
||||
print "Checksums match."
|
||||
return 1
|
||||
else:
|
||||
if options.verbose:
|
||||
print "Checksums differ: expected %s, got %s" % (storedchecksum, ondiskchecksum)
|
||||
return 0
|
||||
return 1
|
||||
else:
|
||||
if options.verbose:
|
||||
print "\tDoes not exist."
|
||||
return 0
|
||||
else:
|
||||
print >>sys.stderr, "No path specified!"
|
||||
return -2
|
||||
|
||||
|
||||
def compareReceiptVersion(item):
|
||||
"""
|
||||
Determines if the given package is already installed.
|
||||
packageid is a 'com.apple.pkg.ServerAdminTools' style id
|
||||
Returns 0 if the receipt isn't present
|
||||
-1 if it's older
|
||||
1 if the version is the same
|
||||
2 if the version is newer
|
||||
-2 if there's an error in the input
|
||||
"""
|
||||
if 'packageid' in item and 'version' in item:
|
||||
pkgid = item['packageid']
|
||||
vers = item['version']
|
||||
else:
|
||||
print "Missing packageid or version info!"
|
||||
return -2
|
||||
|
||||
if options.verbose:
|
||||
print "Looking for package %s, version %s" % (pkgid, vers)
|
||||
installedvers = managedinstalls.getInstalledPackageVersion(pkgid)
|
||||
if installedvers:
|
||||
return compareVersions(installedvers, vers)
|
||||
else:
|
||||
if options.verbose:
|
||||
print "\tThis package is not currently installed."
|
||||
return 0
|
||||
|
||||
|
||||
def download_installeritem(pkgurl):
|
||||
"""
|
||||
Downloads a installer item from pkgurl.
|
||||
"""
|
||||
global mytmpdir
|
||||
|
||||
managed_install_dir = managedinstalls.managed_install_dir()
|
||||
mycachedir = os.path.join(managed_install_dir, "Cache")
|
||||
pkgname = os.path.basename(urlparse.urlsplit(pkgurl)[2])
|
||||
destinationpath = os.path.join(mycachedir, pkgname)
|
||||
if os.path.exists(destinationpath):
|
||||
itemmodtime = os.stat(destinationpath).st_mtime
|
||||
else:
|
||||
itemmodtime = None
|
||||
|
||||
print "Downloading %s from %s" % (pkgname, pkgurl)
|
||||
tempfilepath = os.path.join(mytmpdir, pkgname)
|
||||
result = managedinstalls.getfilefromhttpurl(pkgurl, tempfilepath, showprogress=True, ifmodifiedsince=itemmodtime)
|
||||
if result == 0:
|
||||
os.rename(tempfilepath, destinationpath)
|
||||
return True
|
||||
elif result == 304:
|
||||
# not modified
|
||||
print "Installer item %s is already in the install cache." % pkgname
|
||||
return True
|
||||
else:
|
||||
print >>sys.stderr, "Error code: %s" % result
|
||||
if os.path.exists(tempfilepath):
|
||||
os.remove(tempfilepath)
|
||||
print "Couldn't get %s: %s" % (pkgname, result)
|
||||
return False
|
||||
|
||||
|
||||
def isItemInInstallList(catalogitem_pl, thelist):
|
||||
"""
|
||||
Returns True if the filename from the download location
|
||||
for catalogitem is already in the install list,
|
||||
and therefore already scheduled to be installed.
|
||||
"""
|
||||
if 'installer_item_location' in catalogitem_pl:
|
||||
location = catalogitem_pl['installer_item_location']
|
||||
filename = os.path.split(location)[1]
|
||||
for item in thelist:
|
||||
if "installer_item" in item:
|
||||
if filename == item['installer_item']:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def getCatalogItemDetail(item, defaultbranch=''):
|
||||
"""
|
||||
Retrieves detailed info for a catalogitem
|
||||
"""
|
||||
managedinstallprefs = managedinstalls.prefs()
|
||||
sw_repo_baseurl = managedinstallprefs['sw_repo_url']
|
||||
managed_install_dir = managedinstallprefs['managed_install_dir']
|
||||
|
||||
catalogitempath = os.path.join(managed_install_dir, "catalogitems")
|
||||
catalogbaseurl = sw_repo_baseurl + "/catalogitems"
|
||||
|
||||
if defaultbranch == '':
|
||||
defaultbranch = "production"
|
||||
|
||||
itemname = os.path.split(item)[1]
|
||||
itempath = os.path.join(catalogitempath, itemname)
|
||||
if os.path.exists(itempath):
|
||||
itemmodtime = os.stat(itempath).st_mtime
|
||||
else:
|
||||
itemmodtime = None
|
||||
|
||||
if item.startswith("/"):
|
||||
# branch in item name
|
||||
itemurl = catalogbaseurl + item
|
||||
else:
|
||||
# use default branch
|
||||
itemurl = catalogbaseurl + "/" + defaultbranch + "/" + item
|
||||
|
||||
tempfilepath = os.path.join(mytmpdir, itemname)
|
||||
if options.verbose:
|
||||
print "Getting detail for %s from %s..." % (item, itemurl)
|
||||
result = managedinstalls.getfilefromhttpurl(itemurl, tempfilepath, showprogress=True, ifmodifiedsince=itemmodtime)
|
||||
if result == 0:
|
||||
os.rename(tempfilepath, itempath)
|
||||
elif result == 304:
|
||||
# not modified, just return existing item
|
||||
if options.verbose:
|
||||
print "Item %s in local cache is up-to-date." % item
|
||||
return itempath
|
||||
else:
|
||||
print >>sys.stderr, "Error code: %s" % result
|
||||
if os.path.exists(tempfilepath):
|
||||
os.remove(tempfilepath)
|
||||
|
||||
if os.path.exists(itempath):
|
||||
return itempath
|
||||
else:
|
||||
print "Couldn't get detail for item %s: %s" % (item, result)
|
||||
return ""
|
||||
|
||||
|
||||
def enoughDiskSpace(catalogitem_pl):
|
||||
"""
|
||||
Used to determine if there is enough disk space
|
||||
to be able to download and install the catalogitem
|
||||
"""
|
||||
# fudgefactor is set to 100MB
|
||||
fudgefactor = 100000
|
||||
installeritemsize = 0
|
||||
installedsize = 0
|
||||
if 'installer_item_size' in catalogitem_pl:
|
||||
installeritemsize = catalogitem_pl['installer_item_size']
|
||||
if 'installed_size' in catalogitem_pl:
|
||||
installedsize = catalogitem_pl['installed_size']
|
||||
diskspaceneeded = installeritemsize + installedsize + fudgefactor
|
||||
availablediskspace = managedinstalls.getAvailableDiskSpace()
|
||||
if availablediskspace > diskspaceneeded:
|
||||
return True
|
||||
else:
|
||||
print "There is insufficient disk space to download and install. %sMB needed; %sMB available" % (int(diskspaceneeded/1024), int(availablediskspace/1024))
|
||||
return False
|
||||
|
||||
|
||||
def processInstalls(catalogitem, defaultbranch, installlist):
|
||||
"""
|
||||
Processes a catalog item. Determines if it needs to be
|
||||
installed, and if so, if any items it is dependent on need to
|
||||
be installed first. Items to be installed are added to the
|
||||
installlist (a list of filenames)
|
||||
Calls itself recursively as it processes dependencies.
|
||||
Returns a boolean; when processing dependencies, a false return
|
||||
will stop the installation of a dependent item
|
||||
"""
|
||||
|
||||
managedinstallprefs = managedinstalls.prefs()
|
||||
sw_repo_baseurl = managedinstallprefs['sw_repo_url']
|
||||
managed_install_dir = managedinstallprefs['managed_install_dir']
|
||||
|
||||
catalogitempath = os.path.join(managed_install_dir, "catalogitems")
|
||||
downloadbaseurl = sw_repo_baseurl + "/pkgs/"
|
||||
|
||||
catalogitemname = os.path.split(catalogitem)[1]
|
||||
print "\nProcessing catalog item %s..." % catalogitemname
|
||||
catalogitemdetail = getCatalogItemDetail(catalogitem, defaultbranch)
|
||||
|
||||
if not catalogitemdetail:
|
||||
return False
|
||||
|
||||
try:
|
||||
pl = plistlib.readPlist(catalogitemdetail)
|
||||
except:
|
||||
print >>sys.stderr, "%s is not a valid plist!" % catalogitem
|
||||
return False
|
||||
|
||||
# check to see if item is already in the installlist:
|
||||
if isItemInInstallList(pl, installlist):
|
||||
print "%s is already scheduled to be installed." % catalogitemname
|
||||
return True
|
||||
|
||||
# check dependencies
|
||||
dependenciesMet = True
|
||||
if 'dependencies' in pl:
|
||||
dependencies = pl['dependencies']
|
||||
for item in dependencies:
|
||||
print "%s is dependent on %s. Getting info on %s..." % (catalogitemname, item, item)
|
||||
success = processInstalls(item, defaultbranch, installlist)
|
||||
if not success:
|
||||
dependenciesMet = False
|
||||
|
||||
if not dependenciesMet:
|
||||
print "Didn't attempt to install %s because could not resolve all dependencies." % catalogitemname
|
||||
return False
|
||||
|
||||
needToInstall = False
|
||||
if 'installs' in pl:
|
||||
installitems = pl['installs']
|
||||
for item in installitems:
|
||||
if 'type' in item:
|
||||
if item['type'] == 'application':
|
||||
if not isSameOrNewerApplicationInstalled(item):
|
||||
needToInstall = True
|
||||
# once we know we need to install this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
if item['type'] == 'bundle':
|
||||
comparisonResult = compareBundleVersion(item)
|
||||
if comparisonResult == -1 or comparisonResult == 0 :
|
||||
# not there or older
|
||||
needToInstall = True
|
||||
# once we know we need to install this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
if item['type'] == 'plist':
|
||||
comparisonResult = comparePlistVersion(item)
|
||||
if comparisonResult == -1 or comparisonResult == 0 :
|
||||
# not there or older
|
||||
needToInstall = True
|
||||
# once we know we need to install this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
if item['type'] == 'file':
|
||||
if filesystemItemExists(item) == 0 :
|
||||
# not there, or wrong checksum
|
||||
needToInstall = True
|
||||
# once we know we need to install this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
elif 'receipts' in pl:
|
||||
receipts = pl['receipts']
|
||||
for item in receipts:
|
||||
comparisonResult = compareReceiptVersion(item)
|
||||
if comparisonResult == -1 or comparisonResult == 0 :
|
||||
# not there or older
|
||||
needToInstall = True
|
||||
# once we know we need to install this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
|
||||
name = description = ""
|
||||
try:
|
||||
name = pl['name']
|
||||
description = pl['description']
|
||||
except:
|
||||
pass
|
||||
iteminfo = {}
|
||||
iteminfo["name"] = name
|
||||
iteminfo["catalogitem"] = catalogitemname
|
||||
iteminfo["description"] = description
|
||||
|
||||
if needToInstall:
|
||||
print "Need to install %s" % catalogitemname
|
||||
# check to see if there is enough free space to download and install
|
||||
if not enoughDiskSpace(pl):
|
||||
iteminfo["installed"] = False
|
||||
installlist.append(iteminfo)
|
||||
return False
|
||||
|
||||
if 'installer_item_location' in pl:
|
||||
location = pl['installer_item_location']
|
||||
url = downloadbaseurl + location
|
||||
if download_installeritem(url):
|
||||
filename = os.path.split(location)[1]
|
||||
iteminfo["installer_item"] = filename
|
||||
iteminfo["installed"] = False
|
||||
installlist.append(iteminfo)
|
||||
return True
|
||||
else:
|
||||
iteminfo["installed"] = False
|
||||
installlist.append(iteminfo)
|
||||
return False
|
||||
else:
|
||||
print "Can't install %s because there's no download info for the installer item" % catalogitemname
|
||||
iteminfo["installed"] = False
|
||||
installlist.append(iteminfo)
|
||||
return False
|
||||
else:
|
||||
print "No need to install %s" % catalogitemname
|
||||
iteminfo["installed"] = True
|
||||
installlist.append(iteminfo)
|
||||
return True
|
||||
|
||||
|
||||
def processCatalogForInstalls(catalogpath, listofinstalls=[]):
|
||||
"""
|
||||
Processes catalogs. Can be recursive if catalogs inlcude other catalogs.
|
||||
Probably doesn't handle circular catalog references well...
|
||||
"""
|
||||
defaultbranch = getCatalogValueForKey(catalogpath, 'default_branch')
|
||||
|
||||
nestedcatalogs = getCatalogValueForKey(catalogpath, "included_catalogs")
|
||||
if nestedcatalogs:
|
||||
for item in nestedcatalogs:
|
||||
nestedcatalogpath = getcatalog(item)
|
||||
if nestedcatalogpath:
|
||||
listofinstalls = processCatalogForInstalls(nestedcatalogpath, listofinstalls)
|
||||
|
||||
installitems = getCatalogValueForKey(catalogpath, "managed_installs")
|
||||
if installitems:
|
||||
for item in installitems:
|
||||
result = processInstalls(item, defaultbranch, listofinstalls)
|
||||
|
||||
return listofinstalls
|
||||
|
||||
|
||||
def processRemovals(catalogitem, defaultbranch, removallist):
|
||||
"""
|
||||
Processes a catalog item. Determines if it needs to be
|
||||
removed, (and if so, if any items dependent on it need to
|
||||
be removed first.) Items to be removed are added to the
|
||||
removallist
|
||||
Calls itself recursively as it processes dependencies.
|
||||
Returns a boolean; when processing dependencies, a false return
|
||||
will stop the removal of a dependent item.
|
||||
"""
|
||||
|
||||
managedinstallprefs = managedinstalls.prefs()
|
||||
sw_repo_baseurl = managedinstallprefs['sw_repo_url']
|
||||
managed_install_dir = managedinstallprefs['managed_install_dir']
|
||||
|
||||
catalogitempath = os.path.join(managed_install_dir, "catalogitems")
|
||||
downloadbaseurl = sw_repo_baseurl + "/pkgs/"
|
||||
|
||||
catalogitemname = os.path.split(catalogitem)[1]
|
||||
print "\nProcessing catalog item %s..." % catalogitemname
|
||||
catalogitemdetail = getCatalogItemDetail(catalogitem, defaultbranch)
|
||||
|
||||
if not catalogitemdetail:
|
||||
return False
|
||||
|
||||
try:
|
||||
pl = plistlib.readPlist(catalogitemdetail)
|
||||
except:
|
||||
print >>sys.stderr, "%s is not a valid plist!" % catalogitem
|
||||
return False
|
||||
|
||||
# check for uninstall info
|
||||
if not 'uninstallable' in pl or not pl['uninstallable']:
|
||||
print "%s is not uninstallable." % catalogitemname
|
||||
return False
|
||||
|
||||
if not 'uninstall_method' in pl:
|
||||
print "No uninstall info for %s." % catalogitemname
|
||||
return False
|
||||
|
||||
# check for dependent items
|
||||
# look at all the items in the local catalogitem cache
|
||||
# and see if any depend on the current item; if so
|
||||
# we should remove them as well
|
||||
dependentitemsremoved = True
|
||||
for item in os.listdir(catalogitempath):
|
||||
if item != catalogitemname:
|
||||
try:
|
||||
itempath = os.path.join(catalogitempath, item)
|
||||
itempl = plistlib.readPlist(itempath)
|
||||
if 'dependencies' in itempl:
|
||||
if catalogitemname in itempl['dependencies']:
|
||||
print "%s is dependent on %s and must be removed as well" % (item, catalogitemname)
|
||||
success = processRemovals(item, defaultbranch, removallist)
|
||||
if not success:
|
||||
dependentitemsremoved = False
|
||||
except:
|
||||
pass
|
||||
|
||||
if not dependentitemsremoved:
|
||||
print "Didn't attempt to remove %s because could not remove all items dependent on it." % catalogitemname
|
||||
return False
|
||||
|
||||
# check to see if item is already in the removallist:
|
||||
if isItemInInstallList(pl, removallist):
|
||||
print "%s is already scheduled to be removed." % catalogitemname
|
||||
return True
|
||||
|
||||
needToRemove = False
|
||||
if 'installs' in pl:
|
||||
installitems = pl['installs']
|
||||
for item in installitems:
|
||||
if 'type' in item:
|
||||
if item['type'] == 'application':
|
||||
if compareApplicationVersion(item, newer_ok=False):
|
||||
# exact version found
|
||||
needToRemove = True
|
||||
# once we know we need to remove this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
if item['type'] == 'bundle':
|
||||
comparisonResult = compareBundleVersion(item)
|
||||
if comparisonResult == 1:
|
||||
# same version is installed
|
||||
needToRemove = True
|
||||
# once we know we need to remove this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
if item['type'] == 'plist':
|
||||
comparisonResult = comparePlistVersion(item)
|
||||
if comparisonResult == 1:
|
||||
# same version is installed
|
||||
needToRemove = True
|
||||
# once we know we need to remove this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
if item['type'] == 'file':
|
||||
if filesystemItemExists(item) == 1:
|
||||
needToRemove = True
|
||||
# once we know we need to remove this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
if 'receipts' in pl:
|
||||
receipts = pl['receipts']
|
||||
for item in receipts:
|
||||
comparisonResult = compareReceiptVersion(item)
|
||||
if comparisonResult == 1:
|
||||
# same version is installed
|
||||
needToRemove = True
|
||||
# once we know we need to remove this one,
|
||||
# no need to keep checking
|
||||
break
|
||||
|
||||
name = description = ""
|
||||
try:
|
||||
name = pl['name']
|
||||
description = pl['description']
|
||||
except:
|
||||
pass
|
||||
iteminfo = {}
|
||||
iteminfo["name"] = name
|
||||
iteminfo["catalogitem"] = catalogitemname
|
||||
iteminfo["description"] = description
|
||||
|
||||
if needToRemove:
|
||||
print "Need to remove %s" % catalogitemname
|
||||
uninstallmethod = pl['uninstall_method']
|
||||
|
||||
if uninstallmethod == 'removepackages':
|
||||
# build list of packages based on receipts
|
||||
if options.verbose:
|
||||
print "Building list of packages to remove"
|
||||
packages = []
|
||||
if 'receipts' in pl:
|
||||
for item in pl['receipts']:
|
||||
if compareReceiptVersion(item) == 1:
|
||||
packages.append(item['packageid'])
|
||||
iteminfo['packages'] = packages
|
||||
|
||||
iteminfo["uninstall_method"] = uninstallmethod
|
||||
iteminfo["installed"] = True
|
||||
removallist.append(iteminfo)
|
||||
return True
|
||||
|
||||
else:
|
||||
print "No need to remove %s" % catalogitemname
|
||||
iteminfo["installed"] = False
|
||||
removallist.append(iteminfo)
|
||||
return True
|
||||
|
||||
|
||||
def processCatalogForRemovals(catalogpath, listofremovals=[]):
|
||||
"""
|
||||
Processes catalogs for removals. Can be recursive if catalogs inlcude other catalogs.
|
||||
Probably doesn't handle circular catalog references well...
|
||||
"""
|
||||
defaultbranch = getCatalogValueForKey(catalogpath, 'default_branch')
|
||||
|
||||
nestedcatalogs = getCatalogValueForKey(catalogpath, "included_catalogs")
|
||||
if nestedcatalogs:
|
||||
for item in nestedcatalogs:
|
||||
nestedcatalogpath = getcatalog(item)
|
||||
if nestedcatalogpath:
|
||||
listofremovals = processCatalogForRemovals(nestedcatalogpath, listofremovals)
|
||||
|
||||
removalitems = getCatalogValueForKey(catalogpath, "managed_uninstalls")
|
||||
if removalitems:
|
||||
for item in removalitems:
|
||||
result = processRemovals(item, defaultbranch, listofremovals)
|
||||
|
||||
return listofremovals
|
||||
|
||||
|
||||
def getCatalogValueForKey(catalogpath, keyname):
|
||||
|
||||
try:
|
||||
pl = plistlib.readPlist(catalogpath)
|
||||
except:
|
||||
print >>sys.stderr, "Could not read plist %s" % catalogpath
|
||||
return None
|
||||
|
||||
if keyname in pl:
|
||||
return pl[keyname]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def createDirsIfNeeded(dirlist):
|
||||
for dir in dirlist:
|
||||
if not os.path.exists(dir):
|
||||
try:
|
||||
os.mkdir(dir)
|
||||
except:
|
||||
print >>sys.stderr, "Could not create %s" % dir
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def getcatalog(partialurl):
|
||||
"""
|
||||
Gets a catalog from the server
|
||||
"""
|
||||
managedinstallprefs = managedinstalls.prefs()
|
||||
sw_repo_baseurl = managedinstallprefs['sw_repo_url']
|
||||
catalog_dir = os.path.join(managedinstallprefs['managed_install_dir'], "catalogs")
|
||||
if not createDirsIfNeeded([catalog_dir]):
|
||||
exit(-1)
|
||||
|
||||
if partialurl.startswith("http"):
|
||||
# then it's really a request for the main catalog
|
||||
catalogurl = partialurl
|
||||
catalogname = "MainCatalog.plist"
|
||||
else:
|
||||
# request for nested catalog
|
||||
catalogname = os.path.split(partialurl)[1]
|
||||
catalogurl = sw_repo_baseurl + "/catalogs/" + partialurl
|
||||
|
||||
catalogpath = os.path.join(catalog_dir, catalogname)
|
||||
if os.path.exists(catalogpath):
|
||||
catalogmodtime = os.stat(catalogpath).st_mtime
|
||||
else:
|
||||
catalogmodtime = None
|
||||
tempfilepath = os.path.join(mytmpdir, catalogname)
|
||||
print "Getting catalog %s from %s..." % (catalogname, catalogurl)
|
||||
result = managedinstalls.getfilefromhttpurl(catalogurl, tempfilepath, showprogress=True, ifmodifiedsince=catalogmodtime)
|
||||
if result == 0:
|
||||
try:
|
||||
os.rename(tempfilepath, catalogpath)
|
||||
return catalogpath
|
||||
except:
|
||||
print >>sys.stderr, "Could not write to %s" % catalogpath
|
||||
return ""
|
||||
elif result == 304:
|
||||
# not modified, do nothing
|
||||
print "Catalog %s in local cache is up-to-date." % catalogname
|
||||
return catalogpath
|
||||
else:
|
||||
print >>sys.stderr, "Error code: %s retreiving catalog %s" % (result, catalogname)
|
||||
if os.path.exists(tempfilepath):
|
||||
os.remove(tempfilepath)
|
||||
return ""
|
||||
|
||||
|
||||
def getMainCatalog(alternate_id):
|
||||
"""
|
||||
Gets the main client catalog (aka manifest) from the server
|
||||
"""
|
||||
managedinstallprefs = managedinstalls.prefs()
|
||||
manifesturl = managedinstallprefs['manifest_url']
|
||||
clientidentifier = managedinstallprefs['client_identifier']
|
||||
|
||||
if not manifesturl.endswith('?') and not manifesturl.endswith('/'):
|
||||
manifesturl = manifesturl + "/"
|
||||
if alternate_id:
|
||||
# use id passed in at command-line
|
||||
manifesturl = manifesturl + alternate_id
|
||||
elif clientidentifier:
|
||||
# use client_identfier from /Library/Preferences/ManagedInstalls.plist
|
||||
manifesturl = manifesturl + clientidentifier
|
||||
else:
|
||||
# no client identifier specified, so use the hostname
|
||||
manifesturl = manifesturl + os.uname()[1]
|
||||
|
||||
return getcatalog(manifesturl)
|
||||
|
||||
# some globals
|
||||
mytmpdir = tempfile.mkdtemp()
|
||||
p = optparse.OptionParser()
|
||||
p.add_option('--id', '-i', default='',
|
||||
help='Alternate identifier for catalog retreival')
|
||||
p.add_option('--verbose', '-v', action='store_true',
|
||||
help='More verbose output.')
|
||||
options, arguments = p.parse_args()
|
||||
|
||||
def main():
|
||||
global mytmpdir, options
|
||||
|
||||
managedinstallprefs = managedinstalls.prefs()
|
||||
managed_install_dir = managedinstallprefs['managed_install_dir']
|
||||
catalogitemsdir = os.path.join(managed_install_dir, "catalogitems")
|
||||
cachedir = os.path.join(managed_install_dir, "Cache")
|
||||
|
||||
if not createDirsIfNeeded([managed_install_dir, catalogitemsdir, cachedir]):
|
||||
print >>sys.stderr, "No write access to managed install directory: %s" % managed_install_dir
|
||||
exit(-1)
|
||||
|
||||
maincatalogpath = getMainCatalog(options.id)
|
||||
if not maincatalogpath:
|
||||
print >>sys.stderr, "Could not retreive managed install catalog."
|
||||
exit(-1)
|
||||
|
||||
installlist = processCatalogForInstalls(maincatalogpath)
|
||||
|
||||
# clean up cache dir
|
||||
# remove any item in the install cache that isn't scheduled
|
||||
# to be installed --
|
||||
# this allows us to 'pull back' an item before it is installed
|
||||
# by removing it from the manifest
|
||||
installer_item_list = []
|
||||
for item in installlist:
|
||||
if "installer_item" in item:
|
||||
installer_item_list.append(item["installer_item"])
|
||||
|
||||
for item in os.listdir(cachedir):
|
||||
if item not in installer_item_list:
|
||||
if options.verbose:
|
||||
print "Removing %s from cache" % item
|
||||
os.unlink(os.path.join(cachedir, item))
|
||||
|
||||
# now generate a list of items to be uninstalled
|
||||
removallist = processCatalogForRemovals(maincatalogpath)
|
||||
|
||||
# need to write out install list so the autoinstaller
|
||||
# can use it to install things in the right order
|
||||
pldict = {}
|
||||
pldict['managed_installs'] = installlist
|
||||
pldict['removals'] = removallist
|
||||
plistlib.writePlist(pldict, os.path.join(managed_install_dir, "InstallInfo.plist"))
|
||||
|
||||
# now clean up catalogitem dir, removing items no longer needed
|
||||
currentcatalogitems = []
|
||||
for item in installlist:
|
||||
currentcatalogitems.append(item['catalogitem'])
|
||||
for item in removallist:
|
||||
currentcatalogitems.append(item['catalogitem'])
|
||||
|
||||
for item in os.listdir(catalogitemsdir):
|
||||
if item not in currentcatalogitems:
|
||||
os.unlink(os.path.join(catalogitemsdir,item))
|
||||
|
||||
try:
|
||||
# clean up our tmp dir
|
||||
os.rmdir(mytmpdir)
|
||||
except:
|
||||
# not fatal if it fails
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
1239
code/client/installcheck
Executable file
1239
code/client/installcheck
Executable file
File diff suppressed because it is too large
Load Diff
102
code/client/makecatalogs
Executable file
102
code/client/makecatalogs
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
makecatalogs
|
||||
|
||||
Created by Greg Neagle on 2009-03-30.
|
||||
Copyright (c) 2009 Walt Disney Animation Studios. All rights reserved.
|
||||
|
||||
Recursively scans a directory, looking for installer item info files. Builds a repo catalog from these files.
|
||||
|
||||
TO-DOs:
|
||||
Maybe generate a checksum so we can verify the installer item is the right one
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import optparse
|
||||
import plistlib
|
||||
import subprocess
|
||||
|
||||
def validPlist(path):
|
||||
cmd = ['/usr/bin/plutil', '-lint', '-s' , path]
|
||||
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
if p.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def makeCatalogs(repopath):
|
||||
pkgsinfopath = os.path.join(repopath, 'pkgsinfo')
|
||||
if not os.path.exists(pkgsinfopath):
|
||||
print >>sys.stderr, "pkgsinfo path %s doesn't exist!" % pkgsinfopath
|
||||
exit(-1)
|
||||
|
||||
catalogs = {}
|
||||
catalogs['all'] = []
|
||||
for dirpath, dirnames, filenames in os.walk(pkgsinfopath):
|
||||
subdir = dirpath[len(pkgsinfopath):]
|
||||
for name in filenames:
|
||||
filepath = os.path.join(dirpath,name)
|
||||
if validPlist(filepath):
|
||||
#if it's a valid plist, assume it's a pkginfo file
|
||||
pkginfo = plistlib.readPlist(filepath)
|
||||
#simple sanity checking
|
||||
if 'installer_item_location' in pkginfo:
|
||||
installeritempath = os.path.join(repopath, "pkgs", pkginfo['installer_item_location'])
|
||||
if os.path.exists(installeritempath):
|
||||
catalogs['all'].append(pkginfo)
|
||||
for catalogname in pkginfo.get("catalogs",[]):
|
||||
if not catalogname in catalogs:
|
||||
catalogs[catalogname] = []
|
||||
catalogs[catalogname].append(pkginfo)
|
||||
else:
|
||||
print >>sys.stderr, "WARNING: Info file %s refers to missing installer item: %s" % (filepath[len(pkgsinfopath)+1:], pkginfo['installer_item_location'])
|
||||
|
||||
# clear out old catalogs
|
||||
path = os.path.join(repopath, "catalogs")
|
||||
for item in os.listdir(path):
|
||||
itempath = os.path.join(path,item)
|
||||
if os.path.isfile(itempath):
|
||||
os.remove(itempath)
|
||||
|
||||
# write the new catalogs
|
||||
for key in catalogs.keys():
|
||||
catalogpath = os.path.join(repopath, "catalogs", key)
|
||||
plistlib.writePlist(catalogs[key], catalogpath)
|
||||
|
||||
|
||||
def main():
|
||||
usage = "usage: %prog [options] /path/to/repo_root"
|
||||
p = optparse.OptionParser(usage=usage)
|
||||
options, arguments = p.parse_args()
|
||||
if len(arguments) == 0:
|
||||
print >>sys.stderr, "Need to specify a path to the repo root!"
|
||||
exit(-1)
|
||||
|
||||
repopath = arguments[0].rstrip("/")
|
||||
if not os.path.exists(repopath):
|
||||
print >>sys.stderr, "Repo root path %s doesn't exist!" % repopath
|
||||
exit(-1)
|
||||
|
||||
makeCatalogs(repopath)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
makecatalogitem.py
|
||||
makepkginfo
|
||||
|
||||
Created by Greg Neagle on 2008-11-25.
|
||||
Creates a managed install catalog item plist given an Installer item:
|
||||
Creates a managed install pkg info plist given an Installer item:
|
||||
a .pkg, a .mpkg, or a .dmg containing a .pkg or .mpkg
|
||||
at the root of the mounted disk image.
|
||||
|
||||
@@ -29,7 +29,7 @@ reinstalled.
|
||||
|
||||
The generated plist is printed to STDOUT.
|
||||
|
||||
Usage: makecatalogitem /path/to/package_or_dmg [-f /path/to/item/it/installs ...]
|
||||
Usage: makepkginfo /path/to/package_or_dmg [-f /path/to/item/it/installs ...]
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -131,6 +131,9 @@ def getCatalogInfo(pkgitem):
|
||||
metaversion = nameAndVersion(shortname)[1]
|
||||
if not len(metaversion):
|
||||
metaversion = highestpkgversion
|
||||
elif highestpkgversion.startswith(metaversion):
|
||||
#for example, highestpkgversion is 2.0.3124.0, version in filename is 2.0
|
||||
metaversion = highestpkgversion
|
||||
|
||||
if 'installed_size' in installerinfo:
|
||||
if installerinfo['installed_size'] > 0:
|
||||
@@ -139,11 +142,10 @@ def getCatalogInfo(pkgitem):
|
||||
cataloginfo = {}
|
||||
cataloginfo['name'] = nameAndVersion(shortname)[0]
|
||||
cataloginfo['version'] = metaversion
|
||||
if 'RestartAction' in installerinfo:
|
||||
cataloginfo['RestartAction'] = installerinfo['RestartAction']
|
||||
cataloginfo['description'] = ""
|
||||
if 'description' in installerinfo:
|
||||
cataloginfo['description'] = installerinfo['description']
|
||||
for key in ('display_name', 'RestartAction', 'description'):
|
||||
if key in installerinfo:
|
||||
cataloginfo[key] = installerinfo[key]
|
||||
|
||||
if installedsize > 0:
|
||||
cataloginfo['installed_size'] = installedsize
|
||||
|
||||
@@ -230,6 +232,7 @@ def getiteminfo(itempath):
|
||||
infodict = {}
|
||||
if itempath.endswith('.app'):
|
||||
infodict['type'] = 'application'
|
||||
infodict['path'] = itempath
|
||||
pl = getBundleInfo(itempath)
|
||||
if 'CFBundleName' in pl:
|
||||
infodict['CFBundleName'] = pl['CFBundleName']
|
||||
@@ -241,12 +244,14 @@ def getiteminfo(itempath):
|
||||
infodict['minosversion'] = pl['LSMinimumSystemVersion']
|
||||
elif 'SystemVersionCheck:MinimumSystemVersion' in pl:
|
||||
infodict['minosversion'] = pl['SystemVersionCheck:MinimumSystemVersion']
|
||||
|
||||
elif os.path.exists(os.path.join(itempath,'Contents','Info.plist')) or os.path.exists(os.path.join(itempath,'Resources','Info.plist')):
|
||||
infodict['type'] = 'bundle'
|
||||
infodict['path'] = itempath
|
||||
pl = getBundleInfo(itempath)
|
||||
if 'CFBundleShortVersionString' in pl:
|
||||
infodict['CFBundleShortVersionString'] = pl['CFBundleShortVersionString']
|
||||
|
||||
elif itempath.endswith("Info.plist") or itempath.endswith("version.plist"):
|
||||
infodict['type'] = 'plist'
|
||||
infodict['path'] = itempath
|
||||
@@ -307,6 +312,12 @@ def main():
|
||||
if options.file:
|
||||
installs = []
|
||||
for fitem in options.file:
|
||||
# no trailing slashes, please.
|
||||
fitem = fitem.rstrip('/')
|
||||
if fitem.startswith('/Library/Receipts'):
|
||||
# no receipts, please!
|
||||
print >>sys.stderr, "Item %s appears to be a receipt. Skipping." % fitem
|
||||
continue
|
||||
if os.path.exists(fitem):
|
||||
iteminfodict = getiteminfo(fitem)
|
||||
if 'minosversion' in iteminfodict:
|
||||
@@ -15,7 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
managedinstallslib.py
|
||||
managedinstalls
|
||||
|
||||
Created by Greg Neagle on 2008-11-18.
|
||||
|
||||
@@ -47,6 +47,7 @@ def getManagedInstallsPrefs():
|
||||
prefs['manifest_url'] = "http:/managedinstalls/cgi-bin/getmanifest"
|
||||
prefs['sw_repo_url'] = "http://managedinstalls/swrepo"
|
||||
prefs['client_identifier'] = ""
|
||||
prefs['logging_level'] = 1
|
||||
prefsfile = "/Library/Preferences/ManagedInstalls.plist"
|
||||
|
||||
if os.path.exists(prefsfile):
|
||||
@@ -63,6 +64,9 @@ def getManagedInstallsPrefs():
|
||||
prefs['sw_repo_url'] = pl['sw_repo_url']
|
||||
if 'client_identifier' in pl:
|
||||
prefs['client_identifier'] = pl['client_identifier']
|
||||
if 'logging_level' in pl:
|
||||
prefs['logging_level'] = pl['logging_level']
|
||||
|
||||
|
||||
return prefs
|
||||
|
||||
@@ -113,6 +117,8 @@ def getInstallerPkgInfo(filename):
|
||||
if 'Will Restart' in pl:
|
||||
if pl['Will Restart'] == "YES":
|
||||
installerinfo['RestartAction'] = "RequireRestart"
|
||||
if "Title" in pl:
|
||||
installerinfo['display_name'] = pl['Title']
|
||||
|
||||
return installerinfo
|
||||
|
||||
@@ -398,11 +404,13 @@ def display_percent_done(current,maximum):
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def httpDownload(url, filename, headers={}, postData=None, reporthook=None):
|
||||
def httpDownload(url, filename, headers={}, postData=None, reporthook=None, message=None):
|
||||
reqObj = urllib2.Request(url, postData, headers)
|
||||
fp = urllib2.urlopen(reqObj)
|
||||
headers = fp.info()
|
||||
|
||||
if message: print message
|
||||
|
||||
#read & write fileObj to filename
|
||||
tfp = open(filename, 'wb')
|
||||
result = filename, headers
|
||||
@@ -438,7 +446,7 @@ def httpDownload(url, filename, headers={}, postData=None, reporthook=None):
|
||||
|
||||
|
||||
|
||||
def getfilefromhttpurl(url,filepath,showprogress=False,ifmodifiedsince=None):
|
||||
def getfilefromhttpurl(url,filepath,showprogress=False,ifmodifiedsince=None, message=None):
|
||||
"""
|
||||
gets a file from a url.
|
||||
If 'ifmodifiedsince' is specified, this header is set
|
||||
@@ -455,7 +463,7 @@ def getfilefromhttpurl(url,filepath,showprogress=False,ifmodifiedsince=None):
|
||||
if ifmodifiedsince:
|
||||
modtimestr = time.strftime("%a, %d %b %Y %H:%M:%S GMT",time.gmtime(ifmodifiedsince))
|
||||
request_headers["If-Modified-Since"] = modtimestr
|
||||
(f,headers) = httpDownload(url, filename=filepath, headers=request_headers, reporthook=reporthook)
|
||||
(f,headers) = httpDownload(url, filename=filepath, headers=request_headers, reporthook=reporthook, message=message)
|
||||
if 'last-modified' in headers:
|
||||
# set the modtime of the downloaded file to the modtime of the
|
||||
# file on the server
|
||||
@@ -472,8 +480,40 @@ def getfilefromhttpurl(url,filepath,showprogress=False,ifmodifiedsince=None):
|
||||
return (-1, "Unexpected error")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
def getHTTPfileIfNewerAtomically(url,destinationpath,showprogress=False, message=None):
|
||||
"""
|
||||
Gets file from HTTP URL, only if newer on web server.
|
||||
Replaces pre-existing file only on success. (thus 'Atomically')
|
||||
"""
|
||||
mytmpdir = tempfile.mkdtemp()
|
||||
mytemppath = os.path.join(mytmpdir,"TempDownload")
|
||||
if os.path.exists(destinationpath):
|
||||
modtime = os.stat(destinationpath).st_mtime
|
||||
else:
|
||||
modtime = None
|
||||
result = getfilefromhttpurl(url, mytemppath, showprogress=True, ifmodifiedsince=modtime, message=message)
|
||||
if result == 0:
|
||||
try:
|
||||
os.rename(mytemppath, destinationpath)
|
||||
return destinationpath
|
||||
except:
|
||||
print >>sys.stderr, "Could not write to %s" % destinationpath
|
||||
destinationpath = None
|
||||
elif result == 304:
|
||||
# not modified, return existing file
|
||||
return destinationpath
|
||||
else:
|
||||
print >>sys.stderr, "Error code: %s retreiving %s" % (result, url)
|
||||
destinationpath = None
|
||||
|
||||
if os.path.exists(mytemppath):
|
||||
os.remove(mytemppath)
|
||||
os.rmdir(mytmpdir)
|
||||
return destinationpath
|
||||
|
||||
|
||||
debug = False
|
||||
def main():
|
||||
pass
|
||||
|
||||
@@ -150,8 +150,8 @@ def main():
|
||||
help='Disable the stop button.')
|
||||
p.add_option('--enableStopButton', action='store_true',
|
||||
help='Enable the stop button.')
|
||||
|
||||
|
||||
|
||||
|
||||
options, arguments = p.parse_args()
|
||||
|
||||
if options.quit:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
removePackages - a tool to analyze installed packages and remove
|
||||
removepackages - a tool to analyze installed packages and remove
|
||||
files unique to the packages given at the command line. No attempt
|
||||
is made to revert to older versions of a file when uninstalling;
|
||||
only file removals are done.
|
||||
@@ -30,7 +30,7 @@ import plistlib
|
||||
import sqlite3
|
||||
import time
|
||||
import munkistatus
|
||||
#import applereceiptutils
|
||||
|
||||
|
||||
##################################################################
|
||||
# Schema of /Library/Receipts/db/a.receiptsdb:
|
||||
@@ -143,8 +143,9 @@ def display_percent_done(current,maximum):
|
||||
def display_status(msg):
|
||||
"""
|
||||
Displays major status messages, formatting as needed
|
||||
for verbose/non-verbose and iHook-style output.
|
||||
for verbose/non-verbose and munkistatus-style output.
|
||||
"""
|
||||
log(msg)
|
||||
if options.munkistatusoutput:
|
||||
munkistatus.detail(msg)
|
||||
elif options.verbose:
|
||||
@@ -157,7 +158,7 @@ def display_status(msg):
|
||||
def display_info(msg):
|
||||
"""
|
||||
Displays minor info messages, formatting as needed
|
||||
for verbose/non-verbose and iHook-style output.
|
||||
for verbose/non-verbose and munkistatus-style output.
|
||||
"""
|
||||
if options.munkistatusoutput:
|
||||
#munkistatus.detail(msg)
|
||||
@@ -168,12 +169,22 @@ def display_info(msg):
|
||||
|
||||
def display_error(msg):
|
||||
"""
|
||||
Prints msg to stderr and eventually to the log
|
||||
Prints msg to stderr and the log
|
||||
"""
|
||||
print >>sys.stderr, "ERROR: %s" % msg
|
||||
print >>sys.stderr, msg
|
||||
log(msg)
|
||||
|
||||
|
||||
|
||||
def log(msg):
|
||||
try:
|
||||
f = open(options.logfile, mode='a', buffering=1)
|
||||
print >>f, time.ctime(), msg
|
||||
f.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def shouldRebuildDB(pkgdbpath):
|
||||
"""
|
||||
Checks to see if our internal package DB should be rebuilt.
|
||||
@@ -599,6 +610,7 @@ def removeReceipts(pkgkeylist):
|
||||
receiptpath = os.path.join('/Library/Receipts/boms', pkgname)
|
||||
if os.path.exists(receiptpath):
|
||||
display_info("Removing %s..." % receiptpath)
|
||||
log("Removing %s..." % receiptpath)
|
||||
retcode = subprocess.call(["/bin/rm", "-rf", receiptpath])
|
||||
|
||||
# remove pkg info from our database
|
||||
@@ -709,6 +721,7 @@ def removeFilesystemItems(removalpaths):
|
||||
# use os.path.lexists so broken links return true so we can remove them
|
||||
if os.path.lexists(pathtoremove):
|
||||
display_info("Removing: " + pathtoremove.encode("UTF-8"))
|
||||
log("Removing: " + pathtoremove.encode("UTF-8"))
|
||||
if (os.path.isdir(pathtoremove) and not os.path.islink(pathtoremove)):
|
||||
diritems = os.listdir(pathtoremove)
|
||||
if diritems == ['.DS_Store']:
|
||||
@@ -780,6 +793,8 @@ p.add_option('--dontquitmunkistatus', '-d', action='store_true',
|
||||
help="Don't quit MunkiStatus on exit. (For example, when called by ManagedInstaller.)")
|
||||
p.add_option('--verbose', '-v', action='store_true',
|
||||
help='More verbose output.')
|
||||
p.add_option('--logfile', default='',
|
||||
help="Path to a log file.")
|
||||
# Get our options and our package names
|
||||
options, pkgnames = p.parse_args()
|
||||
|
||||
@@ -812,7 +827,6 @@ if stopRequested():
|
||||
cleanup()
|
||||
exit()
|
||||
|
||||
|
||||
if removalpaths:
|
||||
if options.listfiles:
|
||||
removalpaths.sort()
|
||||
|
||||
Reference in New Issue
Block a user