mirror of
https://github.com/munki/munki.git
synced 2026-01-15 03:30:29 -06:00
git-svn-id: http://munki.googlecode.com/svn/trunk@220 a4e17f2e-e282-11dd-95e1-755cbddbdd66
1768 lines
69 KiB
Python
1768 lines
69 KiB
Python
#!/usr/bin/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.
|
|
"""
|
|
updatecheck
|
|
|
|
Created by Greg Neagle on 2008-11-13.
|
|
|
|
"""
|
|
|
|
#standard libs
|
|
import sys
|
|
import os
|
|
import subprocess
|
|
from distutils import version
|
|
import urllib2
|
|
import urlparse
|
|
import httplib
|
|
import hashlib
|
|
import datetime
|
|
import time
|
|
import calendar
|
|
import socket
|
|
|
|
#our lib
|
|
import munkicommon
|
|
import munkistatus
|
|
import FoundationPlist
|
|
|
|
def reporterrors():
|
|
# just a placeholder right now;
|
|
# this needs to be expanded to support error reporting
|
|
# via email and HTTP CGI.
|
|
# (and maybe moved to a library module so the installer
|
|
# can use it, too.)
|
|
|
|
managedinstallprefs = munkicommon.prefs()
|
|
clientidentifier = managedinstallprefs.get('ClientIdentifier','')
|
|
#alternate_id = option_id
|
|
hostname = os.uname()[1]
|
|
|
|
print "installcheck errors %s:" % datetime.datetime.now().ctime()
|
|
print "Hostname: %s" % hostname
|
|
print "Client identifier: %s" % clientidentifier
|
|
#print "Alternate ID: %s" % alternate_id
|
|
print "-----------------------------------------"
|
|
print munkicommon.errors
|
|
|
|
|
|
# global to hold our catalog DBs
|
|
catalog = {}
|
|
def makeCatalogDB(catalogitems):
|
|
'''Takes an array of catalog items and builds some indexes so we can
|
|
get our common data faster. Returns a dict we can use like a database'''
|
|
name_table = {}
|
|
pkgid_table = {}
|
|
|
|
itemindex = -1
|
|
for item in catalogitems:
|
|
itemindex = itemindex + 1
|
|
name = item['name']
|
|
vers = item['version']
|
|
|
|
# build indexes for items by name and version
|
|
if not name in name_table:
|
|
name_table[name] = {}
|
|
if not vers in name_table[name]:
|
|
name_table[name][vers] = []
|
|
name_table[name][vers].append(itemindex)
|
|
|
|
# do the same for any aliases
|
|
if 'aliases' in item:
|
|
for alias in item['aliases']:
|
|
if not alias in name_table:
|
|
name_table[alias] = {}
|
|
if not vers in name_table[alias]:
|
|
name_table[alias][vers] = []
|
|
name_table[alias][vers].append(itemindex)
|
|
|
|
# build table of receipts
|
|
if 'receipts' in item:
|
|
for receipt in item['receipts']:
|
|
if 'packageid' in receipt:
|
|
if not receipt['packageid'] in pkgid_table:
|
|
pkgid_table[receipt['packageid']] = {}
|
|
if not receipt['version'] in pkgid_table[receipt['packageid']]:
|
|
pkgid_table[receipt['packageid']][receipt['version']] = []
|
|
pkgid_table[receipt['packageid']][receipt['version']].append(itemindex)
|
|
|
|
pkgdb = {}
|
|
pkgdb['named'] = name_table
|
|
pkgdb['receipts'] = pkgid_table
|
|
#pkgdb['installeritem_table'] = installeritem_table
|
|
pkgdb['items'] = catalogitems
|
|
|
|
return pkgdb
|
|
|
|
|
|
def addPackageids(catalogitems, pkgid_table):
|
|
'''
|
|
Adds packageids from each catalogitem to a dictionary
|
|
'''
|
|
for item in catalogitems:
|
|
name = item['name']
|
|
if item.get('receipts'):
|
|
if not name in pkgid_table:
|
|
pkgid_table[name] = []
|
|
|
|
for receipt in item['receipts']:
|
|
if 'packageid' in receipt:
|
|
if not receipt['packageid'] in pkgid_table[name]:
|
|
pkgid_table[name].append(receipt['packageid'])
|
|
|
|
|
|
def getInstalledPackages():
|
|
"""
|
|
Builds a dictionary of installed receipts and their version number
|
|
"""
|
|
installedpkgs = {}
|
|
# Check /Library/Receipts
|
|
receiptsdir = "/Library/Receipts"
|
|
if os.path.exists(receiptsdir):
|
|
installitems = os.listdir(receiptsdir)
|
|
for item in installitems:
|
|
if item.endswith(".pkg"):
|
|
infoplist = os.path.join(receiptsdir, item, "Contents/Info.plist")
|
|
if os.path.exists(infoplist):
|
|
try:
|
|
pl = FoundationPlist.readPlist(infoplist)
|
|
pkgid = pl.get('CFBundleIdentifier')
|
|
if not pkgid:
|
|
# special case JAMF Composer packages
|
|
pkgid = pl.get('Bundle identifier')
|
|
if pkgid:
|
|
thisversion = munkicommon.getExtendedVersion(os.path.join(receiptsdir, item))
|
|
if not pkgid in installedpkgs:
|
|
installedpkgs[pkgid] = thisversion
|
|
else:
|
|
# pkgid is already in our list. There must be multiple receipts with the same pkgid.
|
|
# in this case, we want the highest version number, since that's the one that's installed,
|
|
# since presumably the newer package replaced the older
|
|
storedversion = installedpkgs[pkgid]
|
|
if version.LooseVersion(thisversion) > version.LooseVersion(storedversion):
|
|
installedpkgs[pkgid] = thisversion
|
|
except:
|
|
pass
|
|
|
|
# Now check new (Leopard and later) package database
|
|
p = subprocess.Popen(["/usr/sbin/pkgutil", "--pkgs"], bufsize=1,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(out, err) = p.communicate()
|
|
|
|
if out:
|
|
pkgs = out.split("\n")
|
|
for pkg in pkgs:
|
|
p = subprocess.Popen(["/usr/sbin/pkgutil", "--pkg-info-plist", pkg], bufsize=1,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(out, err) = p.communicate()
|
|
|
|
if out:
|
|
pl = FoundationPlist.readPlistFromString(out)
|
|
if "pkg-version" in pl:
|
|
installedpkgs[pkg] = pl["pkg-version"]
|
|
|
|
return installedpkgs
|
|
|
|
# global pkgdata
|
|
pkgdata = {}
|
|
def analyzeInstalledPkgs():
|
|
global pkgdata
|
|
managed_pkgids = {}
|
|
for catalogname in catalog.keys():
|
|
catalogitems = catalog[catalogname]['items']
|
|
addPackageids(catalogitems, managed_pkgids)
|
|
|
|
installedpkgs = getInstalledPackages()
|
|
|
|
installed = []
|
|
partiallyinstalled = []
|
|
installedpkgsmatchedtoname = {}
|
|
for name in managed_pkgids.keys():
|
|
somepkgsfound = False
|
|
allpkgsfound = True
|
|
for pkg in managed_pkgids[name]:
|
|
if pkg in installedpkgs.keys():
|
|
somepkgsfound = True
|
|
if not name in installedpkgsmatchedtoname:
|
|
installedpkgsmatchedtoname[name] = []
|
|
installedpkgsmatchedtoname[name].append(pkg)
|
|
else:
|
|
allpkgsfound = False
|
|
if allpkgsfound:
|
|
installed.append(name)
|
|
elif somepkgsfound:
|
|
partiallyinstalled.append(name)
|
|
|
|
# we pay special attention to the items that seem partially installed.
|
|
# we need to see if there are any packages that are unique to this item
|
|
# if there aren't, then this item probably isn't installed, and we're
|
|
# just finding receipts that are shared with other items.
|
|
for name in partiallyinstalled:
|
|
# get a list of pkgs for this item that are installed
|
|
pkgsforthisname = installedpkgsmatchedtoname[name]
|
|
# now build a list of all the pkgs referred to by all the other
|
|
# items that are either partially or entirely installed
|
|
allotherpkgs = []
|
|
for othername in installed:
|
|
allotherpkgs.extend(installedpkgsmatchedtoname[othername])
|
|
for othername in partiallyinstalled:
|
|
if othername != name:
|
|
allotherpkgs.extend(installedpkgsmatchedtoname[othername])
|
|
# use Python sets to find pkgs that are unique to this name
|
|
uniquepkgs = list(set(pkgsforthisname) - set(allotherpkgs))
|
|
if uniquepkgs:
|
|
installed.append(name)
|
|
|
|
# build our reference table
|
|
references = {}
|
|
for name in installed:
|
|
for pkg in installedpkgsmatchedtoname[name]:
|
|
if not pkg in references:
|
|
references[pkg] = []
|
|
references[pkg].append(name)
|
|
|
|
pkgdata = {}
|
|
pkgdata['receipts_for_name'] = installedpkgsmatchedtoname
|
|
pkgdata['installed_names'] = installed
|
|
pkgdata['pkg_references'] = references
|
|
|
|
|
|
# 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 == {}:
|
|
munkicommon.display_debug1("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 = FoundationPlist.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 = FoundationPlist.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
|
|
"""
|
|
thisvers = munkicommon.padVersionString(thisvers,5)
|
|
thatvers = munkicommon.padVersionString(thatvers,5)
|
|
if version.LooseVersion(thisvers) < version.LooseVersion(thatvers):
|
|
return -1
|
|
elif version.LooseVersion(thisvers) == version.LooseVersion(thatvers):
|
|
return 1
|
|
else:
|
|
return 2
|
|
|
|
|
|
def compareApplicationVersion(app):
|
|
"""
|
|
app is a dict with application
|
|
bundle info
|
|
|
|
First checks the given path if it's available,
|
|
then uses system profiler data to look for the app
|
|
|
|
Returns 0 if the app 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 app and 'CFBundleShortVersionString' in app:
|
|
filepath = os.path.join(app['path'], 'Contents', 'Info.plist')
|
|
if os.path.exists(filepath):
|
|
return compareBundleVersion(app)
|
|
|
|
# not in default location, so let's search:
|
|
name = app.get('CFBundleName','')
|
|
bundleid = app.get('CFBundleIdentifier','')
|
|
versionstring = app.get('CFBundleShortVersionString')
|
|
|
|
if name == '' and bundleid == '':
|
|
if 'path' in app:
|
|
# already looked at default path, and we don't have
|
|
# any additional info, so we have to assume it's not installed.
|
|
return 0
|
|
else:
|
|
# no path, no name, no bundleid. Error!
|
|
munkicommon.display_error("No application name or bundleid was specified!")
|
|
return -2
|
|
|
|
munkicommon.display_debug1("Looking for application %s with bundleid: %s, version %s..." % (name, bundleid, versionstring))
|
|
appinfo = []
|
|
appdata = getAppData()
|
|
if appdata:
|
|
for item in appdata:
|
|
if 'path' in item:
|
|
# in case we get items from other disks
|
|
if not item['path'].startswith('/Volumes/'):
|
|
if bundleid:
|
|
if getAppBundleID(item['path']) == bundleid:
|
|
appinfo.append(item)
|
|
elif name:
|
|
if '_name' in item:
|
|
if item['_name'] == name:
|
|
appinfo.append(item)
|
|
|
|
if not appinfo:
|
|
# app isn't present!
|
|
munkicommon.display_debug1("\tDid not find this application on the startup disk.")
|
|
return 0
|
|
|
|
for item in appinfo:
|
|
if '_name' in item:
|
|
munkicommon.display_debug2("\tName: \t %s" % item['_name'].encode("UTF-8"))
|
|
if 'path' in item:
|
|
munkicommon.display_debug2("\tPath: \t %s" % item['path'].encode("UTF-8"))
|
|
munkicommon.display_debug2("\tCFBundleIdentifier: \t %s" % getAppBundleID(item['path']))
|
|
if 'version' in item:
|
|
munkicommon.display_debug2("\tVersion: \t %s" % item['version'].encode("UTF-8"))
|
|
if compareVersions(item['version'], versionstring) == 1:
|
|
# version is the same
|
|
return 1
|
|
if compareVersions(item['version'], versionstring) == 2:
|
|
# version is newer
|
|
return 2
|
|
|
|
# if we got this far, must only be older
|
|
munkicommon.display_debug1("An older version of this application is present.")
|
|
return -1
|
|
|
|
|
|
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:
|
|
munkicommon.display_error("Missing bundle path or version!")
|
|
return -2
|
|
|
|
munkicommon.display_debug1("Checking %s for version %s..." % (filepath, vers))
|
|
if not os.path.exists(filepath):
|
|
munkicommon.display_debug1("\tNo Info.plist found at %s" % filepath)
|
|
return 0
|
|
|
|
try:
|
|
pl = FoundationPlist.readPlist(filepath)
|
|
except:
|
|
munkicommon.display_debug1("\t%s may not be a plist!" % filepath)
|
|
return 0
|
|
|
|
if 'CFBundleShortVersionString' in pl:
|
|
installedvers = pl['CFBundleShortVersionString']
|
|
return compareVersions(installedvers, vers)
|
|
elif 'CFBundleVersion' in pl:
|
|
installedvers = pl['CFBundleVersion']
|
|
return compareVersions(installedvers, vers)
|
|
else:
|
|
munkicommon.display_debug1("\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:
|
|
munkicommon.display_error("Missing plist path or version!")
|
|
return -2
|
|
|
|
munkicommon.display_debug1("Checking %s for version %s..." % (filepath, vers))
|
|
if not os.path.exists(filepath):
|
|
munkicommon.display_debug1("\tNo plist found at %s" % filepath)
|
|
return 0
|
|
|
|
try:
|
|
pl = FoundationPlist.readPlist(filepath)
|
|
except:
|
|
munkicommon.display_debug1("\t%s may not be a plist!" % filepath)
|
|
return 0
|
|
|
|
if 'CFBundleShortVersionString' in pl:
|
|
installedvers = pl['CFBundleShortVersionString']
|
|
return compareVersions(installedvers, vers)
|
|
elif 'CFBundleVersion' in pl:
|
|
installedvers = pl['CFBundleVersion']
|
|
return compareVersions(installedvers, vers)
|
|
else:
|
|
munkicommon.display_debug1("\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
|
|
If item has m5checksum attribute, compares ondisk file's checksum
|
|
"""
|
|
if 'path' in item:
|
|
filepath = item['path']
|
|
munkicommon.display_debug1("Checking existence of %s..." % filepath)
|
|
if os.path.exists(filepath):
|
|
munkicommon.display_debug2("\tExists.")
|
|
if 'md5checksum' in item:
|
|
storedchecksum = item['md5checksum']
|
|
ondiskchecksum = getmd5hash(filepath)
|
|
munkicommon.display_debug2("Comparing checksums...")
|
|
if storedchecksum == ondiskchecksum:
|
|
munkicommon.display_debug2("Checksums match.")
|
|
return 1
|
|
else:
|
|
munkicommon.display_debug2("Checksums differ: expected %s, got %s" % (storedchecksum, ondiskchecksum))
|
|
return 0
|
|
return 1
|
|
else:
|
|
munkicommon.display_debug2("\tDoes not exist.")
|
|
return 0
|
|
else:
|
|
munkicommon.display_error("No path specified for filesystem item.")
|
|
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
|
|
|
|
munkicommon.display_debug1("Looking for package %s, version %s" % (pkgid, vers))
|
|
installedvers = munkicommon.getInstalledPackageVersion(pkgid)
|
|
if installedvers:
|
|
return compareVersions(installedvers, vers)
|
|
else:
|
|
munkicommon.display_debug1("\tThis package is not currently installed.")
|
|
return 0
|
|
|
|
|
|
def getInstalledVersion(pl):
|
|
"""
|
|
Attempts to determine the currently installed version of the item
|
|
described by pl
|
|
"""
|
|
if 'receipts' in pl:
|
|
for receipt in pl['receipts']:
|
|
installedpkgvers = munkicommon.getInstalledPackageVersion(receipt['packageid'])
|
|
munkicommon.display_debug2("Looking for %s, version %s" % (receipt['packageid'], receipt['version']))
|
|
if compareVersions(installedpkgvers, receipt['version']) == 2:
|
|
# version is higher
|
|
installedversion = "newer than %s" % pl['version']
|
|
return installedversion
|
|
if compareVersions(installedpkgvers, receipt['version']) == -1:
|
|
# version is lower
|
|
installedversion = "older than %s" % pl['version']
|
|
return installedversion
|
|
# if we get here all reciepts match
|
|
return pl['version']
|
|
|
|
if 'installs' in pl:
|
|
for install_item in pl['installs']:
|
|
if install_item['type'] == 'application':
|
|
name = install_item.get('CFBundleName')
|
|
bundleid = install_item.get('CFBundleIdentifier')
|
|
munkicommon.display_debug2("Looking for application %s, version %s" % (name, install_item.get('CFBundleIdentifier')))
|
|
try:
|
|
# check default location for app
|
|
filepath = os.path.join(install_item['path'], 'Contents', 'Info.plist')
|
|
pl = FoundationPlist.readPlist(filepath)
|
|
installedappvers = pl.get('CFBundleShortVersionString')
|
|
except:
|
|
# that didn't work, fall through to the slow way
|
|
# using System Profiler
|
|
appinfo = []
|
|
appdata = getAppData()
|
|
if appdata:
|
|
for ad_item in appdata:
|
|
if bundleid:
|
|
if 'path' in ad_item:
|
|
if getAppBundleID(ad_item['path']) == bundleid:
|
|
appinfo.append(ad_item)
|
|
elif name:
|
|
if '_name' in ad_item:
|
|
if ad_item['_name'] == name:
|
|
appinfo.append(ad_item)
|
|
|
|
for ai_item in appinfo:
|
|
if 'version' in ai_item:
|
|
if compareVersions(ai_item['version'], maxversion) == 2:
|
|
# version is higher
|
|
maxversion = ai_item['version']
|
|
installedappvers = maxversion
|
|
|
|
if compareVersions(installedappvers, install_item['CFBundleShortVersionString']) == 2:
|
|
# version is higher
|
|
installedversion = "newer than %s" % pl['version']
|
|
return installedversion
|
|
|
|
if compareVersions(installedappvers, install_item['CFBundleShortVersionString']) == -1:
|
|
# version is lower
|
|
installedversion = "older than %s" % pl['version']
|
|
return installedversion
|
|
|
|
# if we get here all app versions match
|
|
return pl['version']
|
|
|
|
# if we fall through to here we have no idea what version we have
|
|
return "UNKNOWN"
|
|
|
|
|
|
def download_installeritem(location):
|
|
"""
|
|
Downloads a installer item.
|
|
"""
|
|
ManagedInstallDir = munkicommon.ManagedInstallDir()
|
|
sw_repo_baseurl = munkicommon.SoftwareRepoURL()
|
|
downloadbaseurl = sw_repo_baseurl + "/pkgs/"
|
|
mycachedir = os.path.join(ManagedInstallDir, "Cache")
|
|
|
|
# build a URL, quoting the the location to encode reserved characters
|
|
pkgurl = downloadbaseurl + urllib2.quote(location)
|
|
|
|
# grab last path component of location to derive package name.
|
|
pkgname = os.path.basename(location)
|
|
destinationpath = os.path.join(mycachedir, pkgname)
|
|
|
|
# bump up verboseness so we get download percentage done feedback.
|
|
# this is kind of a hack...
|
|
oldverbose = munkicommon.verbose
|
|
munkicommon.verbose = oldverbose + 1
|
|
|
|
dl_message = "Downloading %s from %s" % (pkgname, pkgurl)
|
|
munkicommon.log(dl_message)
|
|
dl_message = "Downloading %s..." % pkgname
|
|
(path, err) = getHTTPfileIfNewerAtomically(pkgurl, destinationpath, message=dl_message)
|
|
|
|
# set verboseness back.
|
|
munkicommon.verbose = oldverbose
|
|
|
|
if path:
|
|
return True
|
|
else:
|
|
munkicommon.display_error("Could not download %s from server." % pkgname)
|
|
munkicommon.display_error(err)
|
|
return False
|
|
|
|
|
|
|
|
def isItemInInstallInfo(manifestitem_pl, thelist, vers=''):
|
|
"""
|
|
Returns True if the manifest item has already
|
|
been processed (it's in the list) and, optionally,
|
|
the version is the same or greater.
|
|
"""
|
|
names = []
|
|
names.append(manifestitem_pl.get('name'))
|
|
names.extend(manifestitem_pl.get('aliases',[]))
|
|
for item in thelist:
|
|
if item.get('name') in names:
|
|
if not vers:
|
|
return True
|
|
if item.get('installed'):
|
|
return True
|
|
#if the version already processed is the same or greater, then we're good
|
|
if compareVersions(item.get('version_to_install'), vers) in (1,2):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def nameAndVersion(s):
|
|
"""
|
|
Splits a string into the name and version number.
|
|
Name and version must be seperated with a hyphen ('-') or double hyphen ('--').
|
|
'TextWrangler-2.3b1' becomes ('TextWrangler', '2.3b1')
|
|
'AdobePhotoshopCS3--11.2.1' becomes ('AdobePhotoshopCS3', '11.2.1')
|
|
'MicrosoftOffice2008-12.2.1' becomes ('MicrosoftOffice2008', '12.2.1')
|
|
"""
|
|
for delim in ('--', '-'):
|
|
if s.count(delim) > 0:
|
|
chunks = s.split(delim)
|
|
vers = chunks.pop()
|
|
name = delim.join(chunks)
|
|
if vers[0] in "0123456789":
|
|
return (name, vers)
|
|
|
|
return (s, '')
|
|
|
|
|
|
def getAllItemsWithName(name,cataloglist):
|
|
"""
|
|
Searches the catalogs in cataloglist for all items matching
|
|
the given name. Returns a list of pkginfo items.
|
|
|
|
The returned list is sorted with newest version first. No precedence is
|
|
given to catalog order.
|
|
|
|
"""
|
|
def compare_item_versions(a, b):
|
|
return cmp(version.LooseVersion(b['version']), version.LooseVersion(a['version']))
|
|
|
|
itemlist = []
|
|
# we'll throw away any included version info
|
|
(name, includedversion) = nameAndVersion(name)
|
|
|
|
munkicommon.display_debug1("Looking for all items matching: %s..." % name)
|
|
for catalogname in cataloglist:
|
|
# is name in the catalog name table?
|
|
if name in catalog[catalogname]['named']:
|
|
versionsmatchingname = catalog[catalogname]['named'][name]
|
|
for vers in versionsmatchingname.keys():
|
|
if vers != 'latest':
|
|
indexlist = catalog[catalogname]['named'][name][vers]
|
|
for index in indexlist:
|
|
thisitem = catalog[catalogname]['items'][index]
|
|
if not thisitem in itemlist:
|
|
munkicommon.display_debug1("Adding item %s, version %s from catalog %s..." % (name, thisitem['version'], catalogname))
|
|
itemlist.append(thisitem)
|
|
|
|
if itemlist:
|
|
# sort so latest version is first
|
|
itemlist.sort(compare_item_versions)
|
|
|
|
return itemlist
|
|
|
|
|
|
def getItemDetail(name, cataloglist, vers=''):
|
|
"""
|
|
Searches the catalogs in cataloglist for an item matching
|
|
the given name and version. If no version is supplied, but the version
|
|
is appended to the name ('TextWrangler--2.3.0.0.0') that version is used.
|
|
If no version is given at all, the latest version is assumed.
|
|
Returns a pkginfo item.
|
|
"""
|
|
def compare_version_keys(a, b):
|
|
return cmp(version.LooseVersion(b), version.LooseVersion(a))
|
|
|
|
global catalog
|
|
(name, includedversion) = nameAndVersion(name)
|
|
if vers == '':
|
|
if includedversion:
|
|
vers = includedversion
|
|
if vers:
|
|
# make sure version is in 1.0.0.0.0 format
|
|
vers = munkicommon.padVersionString(vers,5)
|
|
else:
|
|
vers = 'latest'
|
|
|
|
munkicommon.display_debug1("Looking for detail for: %s, version %s..." % (name, vers))
|
|
for catalogname in cataloglist:
|
|
# is name or alias in the catalog?
|
|
if name in catalog[catalogname]['named']:
|
|
itemsmatchingname = catalog[catalogname]['named'][name]
|
|
indexlist = []
|
|
if vers == 'latest':
|
|
# order all our items, latest first
|
|
versionlist = itemsmatchingname.keys()
|
|
versionlist.sort(compare_version_keys)
|
|
for versionkey in versionlist:
|
|
indexlist.extend(itemsmatchingname[versionkey])
|
|
|
|
elif vers in itemsmatchingname:
|
|
# get the specific requested version
|
|
indexlist = itemsmatchingname[vers]
|
|
|
|
munkicommon.display_debug1("Considering %s items with name %s from catalog %s" % (len(indexlist), name, catalogname))
|
|
for index in indexlist:
|
|
item = catalog[catalogname]['items'][index]
|
|
# we have an item whose name and version matches the request.
|
|
# now check to see if it meets os and cpu requirements
|
|
if 'minimum_os_version' in item:
|
|
min_os_vers = munkicommon.padVersionString(item['minimum_os_version'],3)
|
|
munkicommon.display_debug1("Considering item %s, version %s with minimum os version required %s" % (item['name'], item['version'], min_os_vers))
|
|
munkicommon.display_debug2("Our OS version is %s" % machine['os_vers'])
|
|
if version.LooseVersion(machine['os_vers']) < version.LooseVersion(min_os_vers):
|
|
# skip this one, go to the next
|
|
continue
|
|
|
|
if 'supported_architectures' in item:
|
|
supported_arch_found = False
|
|
munkicommon.display_debug1("Considering item %s, version %s with supported architectures: %s" % (item['name'], item['version'], item['supported_architectures']))
|
|
for arch in item['supported_architectures']:
|
|
if arch == machine['arch']:
|
|
# we found a supported architecture that matches
|
|
# this machine, so we can use it
|
|
supported_arch_found = True
|
|
break
|
|
|
|
if not supported_arch_found:
|
|
# we didn't find a supported architecture that
|
|
# matches this machine
|
|
continue
|
|
|
|
# item name, version, minimum_os_version, and supported_architecture are all OK
|
|
munkicommon.display_debug1("Found %s, version %s in catalog %s" % (item['name'], item['version'], catalogname))
|
|
return item
|
|
|
|
# if we got this far, we didn't find it.
|
|
munkicommon.display_debug1("Nothing found")
|
|
return None
|
|
|
|
|
|
def enoughDiskSpace(manifestitem_pl):
|
|
"""
|
|
Used to determine if there is enough disk space
|
|
to be able to download and install the manifestitem
|
|
"""
|
|
# fudgefactor is set to 100MB
|
|
fudgefactor = 100000
|
|
installeritemsize = 0
|
|
installedsize = 0
|
|
if 'installer_item_size' in manifestitem_pl:
|
|
installeritemsize = manifestitem_pl['installer_item_size']
|
|
if 'installed_size' in manifestitem_pl:
|
|
installedsize = manifestitem_pl['installed_size']
|
|
diskspaceneeded = (installeritemsize + installedsize + fudgefactor)/1024
|
|
availablediskspace = munkicommon.getAvailableDiskSpace()/1024
|
|
if availablediskspace > diskspaceneeded:
|
|
return True
|
|
else:
|
|
munkicommon.display_info("There is insufficient disk space to download and install %s." % manifestitem_pl.get('name'))
|
|
munkicommon.display_info(" %sMB needed; %sMB available" % (diskspaceneeded, availablediskspace))
|
|
return False
|
|
|
|
|
|
def isInstalled(pl):
|
|
"""
|
|
Checks to see if the item described by pl
|
|
(or a newer version) is currently installed
|
|
All tests must pass to be considered installed.
|
|
Returns True if it looks like this or a newer version
|
|
is installed; False otherwise.
|
|
"""
|
|
if 'installs' in pl:
|
|
installitems = pl['installs']
|
|
for item in installitems:
|
|
itemtype = item.get('type')
|
|
if itemtype == 'application':
|
|
if compareApplicationVersion(item) in (-1, 0):
|
|
return False
|
|
|
|
if itemtype == 'bundle':
|
|
if compareBundleVersion(item) in (-1, 0):
|
|
# not there or older
|
|
return False
|
|
|
|
if itemtype == 'plist':
|
|
if comparePlistVersion(item) in (-1, 0):
|
|
# not there or older
|
|
return False
|
|
|
|
if itemtype == 'file':
|
|
if filesystemItemExists(item) == 0 :
|
|
# not there, or wrong checksum
|
|
return False
|
|
|
|
# if there is no 'installs' key, then we'll use receipt info
|
|
# to determine install status.
|
|
elif 'receipts' in pl:
|
|
receipts = pl['receipts']
|
|
for item in receipts:
|
|
if compareReceiptVersion(item) in (-1, 0):
|
|
# not there or older
|
|
return False
|
|
|
|
# if we got this far, we passed all the tests, so the item
|
|
# must be installed (or we don't have enough info...)
|
|
return True
|
|
|
|
|
|
def evidenceThisIsInstalled(pl):
|
|
"""
|
|
Checks to see if there is evidence that
|
|
the item described by pl
|
|
(any version) is currently installed.
|
|
If any tests pass, the item might be installed.
|
|
So this isn't the same as isInstalled()
|
|
"""
|
|
global pkgdata
|
|
# first check our pkgdata
|
|
if pl['name'] in pkgdata['installed_names']:
|
|
return True
|
|
if 'aliases' in pl:
|
|
for alias in pl['aliases']:
|
|
if alias in pkgdata['installed_names']:
|
|
return True
|
|
|
|
if 'installs' in pl:
|
|
installitems = pl['installs']
|
|
for item in installitems:
|
|
if 'path' in item:
|
|
# we can only check by path; if the item has been moved
|
|
# we're not clever enough to find it, and our removal
|
|
# methods are even less clever
|
|
if os.path.exists(item['path']):
|
|
# some version is installed
|
|
return True
|
|
|
|
# this has been superceded by our pkgdata check
|
|
#if 'receipts' in pl:
|
|
# receipts = pl['receipts']
|
|
# for item in receipts:
|
|
# if 'packageid' in item:
|
|
# if munkicommon.getInstalledPackageVersion(item['packageid']):
|
|
# # some version of this package is installed
|
|
# return True
|
|
|
|
# if we got this far, we failed all the tests, so the item
|
|
# must not be installed (or we have bad metadata...)
|
|
return False
|
|
|
|
|
|
def processInstall(manifestitem, cataloglist, installinfo):
|
|
"""
|
|
Processes a manifest 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
|
|
installinfo['managed_installs']
|
|
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 = munkicommon.prefs()
|
|
ManagedInstallDir = managedinstallprefs['ManagedInstallDir']
|
|
|
|
manifestitemname = os.path.split(manifestitem)[1]
|
|
#munkicommon.display_info("Getting detail on %s..." % manifestitemname)
|
|
pl = getItemDetail(manifestitem, cataloglist)
|
|
|
|
if not pl:
|
|
munkicommon.display_info("Could not process item %s for install because could not get detail." % manifestitem)
|
|
return False
|
|
|
|
# check to see if item is already in the installlist:
|
|
if isItemInInstallInfo(pl, installinfo['managed_installs'], pl.get('version')):
|
|
munkicommon.display_debug1("%s has already been processed for install." % manifestitemname)
|
|
return True
|
|
|
|
# check dependencies
|
|
dependenciesMet = True
|
|
|
|
# there are two kinds of dependencies.
|
|
#
|
|
# 'requires' are prerequistes: package A requires package B be installed first.
|
|
# if package A is removed, package B is unaffected. requires is one to many.
|
|
#
|
|
# 'modifies' is a package the current package modifies on install; generally this means the
|
|
# current package is an updater.. For example, 'Office2008' might resolve to Office2008--12.1.7 which modifies
|
|
# Office2008--12.1.0 which modifies Office2008--12.0.0. (Office2008--12.1.7 and
|
|
# Office2008--12.1.0 are updater packages, Office2008--12.0.0 is the original installer.)
|
|
# If you later remove Office2008, you want to remove everything installed by all three packages.
|
|
# 'modifies' provides a method to theoretically figure it all out.
|
|
# 'modifies' is one to one relationship - this item can modify only one other item.
|
|
#
|
|
# when processing installs, the two dependencies are basically equivilent;
|
|
# the real difference comes when processing removals.
|
|
|
|
if 'requires' in pl:
|
|
dependencies = pl['requires']
|
|
for item in dependencies:
|
|
munkicommon.display_detail("%s requires %s. Getting info on %s..." % (manifestitemname, item, item))
|
|
success = processInstall(item, cataloglist, installinfo)
|
|
if not success:
|
|
dependenciesMet = False
|
|
|
|
if 'modifies' in pl:
|
|
dependencies = pl['modifies']
|
|
if type(dependencies) == list:
|
|
# in case this was put in as an array
|
|
# we support only a single modified item.
|
|
item = dependencies[0]
|
|
else:
|
|
item = dependencies
|
|
|
|
munkicommon.display_detail("%s modifies %s. Getting info on %s..." % (manifestitemname, item, item))
|
|
success = processInstall(item, cataloglist, installinfo)
|
|
if not success:
|
|
dependenciesMet = False
|
|
|
|
if not dependenciesMet:
|
|
munkicommon.display_info("Didn't attempt to install %s because could not resolve all dependencies." % manifestitemname)
|
|
return False
|
|
|
|
iteminfo = {}
|
|
iteminfo["name"] = pl.get('name', '')
|
|
iteminfo["manifestitem"] = manifestitemname
|
|
iteminfo["description"] = pl.get('description', '')
|
|
|
|
if not isInstalled(pl):
|
|
munkicommon.display_detail("Need to install %s" % manifestitemname)
|
|
# check to see if there is enough free space to download and install
|
|
if not enoughDiskSpace(pl):
|
|
iteminfo['installed'] = False
|
|
iteminfo['note'] = "Insufficient disk space to download and install"
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
return False
|
|
|
|
if 'installer_item_location' in pl:
|
|
location = pl['installer_item_location']
|
|
if download_installeritem(location):
|
|
filename = os.path.split(location)[1]
|
|
iteminfo['installer_item'] = filename
|
|
iteminfo['installed'] = False
|
|
iteminfo["version_to_install"] = pl.get('version',"UNKNOWN")
|
|
iteminfo['description'] = pl.get('description','')
|
|
iteminfo['display_name'] = pl.get('display_name','')
|
|
if 'installer_choices_xml' in pl:
|
|
iteminfo['installer_choices_xml'] = pl['installer_choices_xml']
|
|
if 'RestartAction' in pl:
|
|
iteminfo['RestartAction'] = pl['RestartAction']
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
return True
|
|
else:
|
|
iteminfo['installed'] = False
|
|
iteminfo['note'] = "Download failed"
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
return False
|
|
else:
|
|
munkicommon.display_info("Can't install %s because there's no download info for the installer item" % manifestitemname)
|
|
iteminfo['installed'] = False
|
|
iteminfo['note'] = "Download info missing"
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
return False
|
|
else:
|
|
iteminfo["installed"] = True
|
|
#iteminfo["installed_version"] = getInstalledVersion(pl)
|
|
installinfo['managed_installs'].append(iteminfo)
|
|
# remove included version number if any
|
|
(name, includedversion) = nameAndVersion(manifestitemname)
|
|
munkicommon.display_detail("%s version %s (or newer) is already installed." % (name, pl['version']))
|
|
return True
|
|
|
|
|
|
def processManifestForInstalls(manifestpath, installinfo, parentcatalogs=[]):
|
|
"""
|
|
Processes manifests to build a list of items to install.
|
|
Can be recursive if manifests inlcude other manifests.
|
|
Probably doesn't handle circular manifest references well...
|
|
"""
|
|
cataloglist = getManifestValueForKey(manifestpath, 'catalogs')
|
|
if cataloglist:
|
|
getCatalogs(cataloglist)
|
|
else:
|
|
cataloglist = parentcatalogs
|
|
|
|
nestedmanifests = getManifestValueForKey(manifestpath, "included_manifests")
|
|
if nestedmanifests:
|
|
for item in nestedmanifests:
|
|
nestedmanifestpath = getmanifest(item)
|
|
if munkicommon.stopRequested():
|
|
return {}
|
|
if nestedmanifestpath:
|
|
listofinstalls = processManifestForInstalls(nestedmanifestpath, installinfo, cataloglist)
|
|
|
|
installitems = getManifestValueForKey(manifestpath, "managed_installs")
|
|
if installitems:
|
|
for item in installitems:
|
|
if munkicommon.stopRequested():
|
|
return {}
|
|
result = processInstall(item, cataloglist, installinfo)
|
|
|
|
return installinfo
|
|
|
|
|
|
def getReceiptsToRemove(item):
|
|
name = item['name']
|
|
if name in pkgdata['receipts_for_name']:
|
|
return pkgdata['receipts_for_name'][name]
|
|
# now check aliases
|
|
if 'aliases' in item:
|
|
for alias in item['aliases']:
|
|
if alias in pkgdata['receipts_for_name']:
|
|
return pkgdata['receipts_for_name'][alias]
|
|
# found nothing
|
|
return []
|
|
|
|
|
|
def processRemoval(manifestitem, cataloglist, installinfo):
|
|
"""
|
|
Processes a manifest item; attempts to determine if it
|
|
needs to be removed, and if it can be removed.
|
|
|
|
Unlike installs, removals aren't really version-specific -
|
|
If we can figure out how to remove the currently installed
|
|
version, we do, unless the admin specifies a specific version
|
|
number in the manifest. In that case, we only attempt a
|
|
removal if the version installed matches the specific version
|
|
in the manifest.
|
|
|
|
Any items dependent on the given item need to be removed first.
|
|
Items to be removed are added to installinfo['removals'].
|
|
|
|
Calls itself recursively as it processes dependencies.
|
|
Returns a boolean; when processing dependencies, a false return
|
|
will stop the removal of a dependent item.
|
|
"""
|
|
global pkgdata
|
|
manifestitemname_withversion = os.path.split(manifestitem)[1]
|
|
|
|
munkicommon.display_detail("Processing manifest item %s..." % manifestitemname_withversion)
|
|
(manifestitemname, includedversion) = nameAndVersion(manifestitemname_withversion)
|
|
infoitems = []
|
|
if includedversion:
|
|
# a specific version was specified
|
|
pl = getItemDetail(manifestitemname, cataloglist, includedversion)
|
|
if pl:
|
|
infoitems.append(pl)
|
|
else:
|
|
# get all items matching the name provided
|
|
infoitems = getAllItemsWithName(manifestitemname,cataloglist)
|
|
|
|
if not infoitems:
|
|
munkicommon.display_info("Could not get information for %s" % manifestitemname_withversion)
|
|
return False
|
|
|
|
for item in infoitems:
|
|
# check to see if item is already in the installlist,
|
|
# if so, that's bad - it means it's scheduled to be installed
|
|
# _and_ removed. We'll warn, and do nothing with this item.
|
|
if isItemInInstallInfo(item, installinfo['managed_installs']):
|
|
munkicommon.display_info("Will not attempt to remove %s because some version of it is in the list of managed installs, or it is required by another managed install." % manifestitemname_withversion)
|
|
return False
|
|
|
|
for item in infoitems:
|
|
# check to see if item is already in the removallist:
|
|
if isItemInInstallInfo(item, installinfo['removals']):
|
|
munkicommon.display_debug1("%s has already been processed for removal." % manifestitemname_withversion)
|
|
return True
|
|
|
|
installEvidence = False
|
|
for item in infoitems:
|
|
if evidenceThisIsInstalled(item):
|
|
installEvidence = True
|
|
break
|
|
|
|
if not installEvidence:
|
|
munkicommon.display_detail("%s doesn't appear to be installed." % manifestitemname_withversion)
|
|
iteminfo = {}
|
|
iteminfo["manifestitem"] = manifestitemname_withversion
|
|
iteminfo["installed"] = False
|
|
installinfo['removals'].append(iteminfo)
|
|
return True
|
|
|
|
uninstall_item = None
|
|
packagesToRemove = []
|
|
for item in infoitems:
|
|
# check for uninstall info
|
|
# walk through the list of items (sorted newest first)
|
|
# and grab the first uninstall method we find.
|
|
if 'uninstallable' in item and 'uninstall_method' in item:
|
|
uninstallmethod = item['uninstall_method']
|
|
if uninstallmethod == 'removepackages':
|
|
packagesToRemove = getReceiptsToRemove(item)
|
|
if packagesToRemove:
|
|
uninstall_item = item
|
|
break
|
|
else:
|
|
# no matching packages found. Check next item
|
|
continue
|
|
else:
|
|
# uninstall_method is a local script.
|
|
# Check to see if it exists and is executable
|
|
if os.path.exists(uninstallmethod) and os.access(uninstallmethod, os.X_OK):
|
|
uninstall_item = item
|
|
break
|
|
|
|
if not uninstall_item:
|
|
# we didn't find an item that seems to match anything on disk.
|
|
munkicommon.display_info("Could not find uninstall info for %s." % manifestitemname_withversion)
|
|
return False
|
|
|
|
# if we got this far, we have enough info to attempt an uninstall.
|
|
# the pkginfo is in uninstall_item
|
|
# Now check for dependent items
|
|
#
|
|
# First, look through catalogs for items that are required by this item;
|
|
# if any are installed, we need to remove them as well
|
|
#
|
|
# still not sure how to handle references to specific versions --
|
|
# if another package says it requires SomePackage--1.0.0.0.0
|
|
# and we're supposed to remove SomePackage--1.0.1.0.0... what do we do?
|
|
#
|
|
dependentitemsremoved = True
|
|
ManagedInstallDir = munkicommon.ManagedInstallDir()
|
|
catalogsdir = os.path.join(ManagedInstallDir, 'catalogs')
|
|
|
|
# make a list of the name and aliases of the current uninstall_item
|
|
uninstall_item_names = []
|
|
uninstall_item_names.append(uninstall_item.get('name'))
|
|
uninstall_item_names.extend(uninstall_item.get('aliases',[]))
|
|
|
|
processednamesandaliases = []
|
|
for catalogname in cataloglist:
|
|
localcatalog = os.path.join(catalogsdir,catalogname)
|
|
catalog = FoundationPlist.readPlist(localcatalog)
|
|
for item_pl in catalog:
|
|
namesandaliases = []
|
|
namesandaliases.append(item_pl.get('name'))
|
|
namesandaliases.extend(item_pl.get('aliases',[]))
|
|
if not set(namesandaliases).intersection(processednamesandaliases):
|
|
if 'requires' in item_pl:
|
|
if set(item_pl['requires']).intersection(uninstall_item_names):
|
|
munkicommon.display_debug1("%s requires %s, checking to see if it's installed..." % (item_pl.get('name'), manifestitemname))
|
|
if evidenceThisIsInstalled(item_pl):
|
|
munkicommon.display_info("%s requires %s and must be removed as well." % (item_pl.get('name'), manifestitemname))
|
|
success = processRemoval(item_pl.get('name'), cataloglist, installinfo)
|
|
if not success:
|
|
dependentitemsremoved = False
|
|
break
|
|
# record these names so we don't process them again
|
|
processednamesandaliases.extend(namesandaliases)
|
|
|
|
# if this package modifies another one, and we're using removepackages,
|
|
# we must remove it as well
|
|
# if we're using another removal method, we just have to hope that
|
|
# the method is smart enough to get everything...
|
|
if 'modifies' in uninstall_item and uninstallmethod == 'removepackages':
|
|
modifies_value = uninstall_item['modifies']
|
|
if type(modifies_value) == list:
|
|
modifieditem = modifies_value[0]
|
|
else:
|
|
modifieditem = modifies_value
|
|
(modifieditemname, modifieditemversion) = nameAndVersion(modifieditem)
|
|
if not modifieditemname in uninstall_item_names:
|
|
success = processRemoval(modifieditem, cataloglist, installinfo)
|
|
if not success:
|
|
dependentitemsremoved = False
|
|
|
|
if not dependentitemsremoved:
|
|
munkicommon.display_info("Will not attempt to remove %s because could not remove all items dependent on it." % manifestitemname_withversion)
|
|
return False
|
|
|
|
# Finally! We can record the removal information!
|
|
iteminfo = {}
|
|
iteminfo["name"] = uninstall_item.get('name', '')
|
|
iteminfo["display_name"] = uninstall_item.get('display_name', '')
|
|
iteminfo["manifestitem"] = manifestitemname_withversion
|
|
iteminfo["description"] = "Will be removed."
|
|
if packagesToRemove:
|
|
# remove references for each package
|
|
packagesToReallyRemove = []
|
|
for pkg in packagesToRemove:
|
|
munkicommon.display_debug1("Considering %s for removal..." % pkg)
|
|
# find pkg in pkgdata['pkg_references'] and remove the reference so
|
|
# we only remove packages if we're the last reference to it
|
|
if pkg in pkgdata['pkg_references']:
|
|
munkicommon.display_debug1("%s references are: %s" % (pkg, pkgdata['pkg_references'][pkg]))
|
|
pkgdata['pkg_references'][pkg].remove(iteminfo["name"])
|
|
if len(pkgdata['pkg_references'][pkg]) == 0:
|
|
munkicommon.display_debug1("Adding %s to removal list." % pkg)
|
|
packagesToReallyRemove.append(pkg)
|
|
else:
|
|
# This shouldn't happen
|
|
munkicommon.display_error("WARNING: pkg id %s missing from pkgdata" % pkg)
|
|
iteminfo['packages'] = packagesToReallyRemove
|
|
iteminfo["uninstall_method"] = uninstallmethod
|
|
iteminfo["installed"] = True
|
|
iteminfo["installed_version"] = uninstall_item.get('version')
|
|
if 'RestartAction' in uninstall_item:
|
|
iteminfo['RestartAction'] = uninstall_item['RestartAction']
|
|
installinfo['removals'].append(iteminfo)
|
|
munkicommon.display_detail("Removal of %s added to ManagedInstaller tasks." % manifestitemname_withversion)
|
|
return True
|
|
|
|
|
|
def processManifestForRemovals(manifestpath, installinfo):
|
|
"""
|
|
Processes manifests for removals. Can be recursive if manifests include other manifests.
|
|
Probably doesn't handle circular manifest references well...
|
|
"""
|
|
global pkgdata
|
|
if pkgdata == {}:
|
|
# build our database of installed packages
|
|
analyzeInstalledPkgs()
|
|
|
|
cataloglist = getManifestValueForKey(manifestpath, 'catalogs')
|
|
|
|
nestedmanifests = getManifestValueForKey(manifestpath, "included_manifests")
|
|
if nestedmanifests:
|
|
for item in nestedmanifests:
|
|
if munkicommon.stopRequested():
|
|
return {}
|
|
nestedmanifestpath = getmanifest(item)
|
|
if nestedmanifestpath:
|
|
listofremovals = processManifestForRemovals(nestedmanifestpath, installinfo)
|
|
|
|
removalitems = getManifestValueForKey(manifestpath, "managed_uninstalls")
|
|
if removalitems:
|
|
for item in removalitems:
|
|
if munkicommon.stopRequested():
|
|
return {}
|
|
result = processRemoval(item, cataloglist, installinfo)
|
|
|
|
return installinfo
|
|
|
|
|
|
def getManifestValueForKey(manifestpath, keyname):
|
|
try:
|
|
pl = FoundationPlist.readPlist(manifestpath)
|
|
except:
|
|
munkicommon.display_error("Could not read plist %s" % manifestpath)
|
|
return None
|
|
if keyname in pl:
|
|
return pl[keyname]
|
|
else:
|
|
return None
|
|
|
|
|
|
def getCatalogs(cataloglist):
|
|
"""
|
|
Retreives the catalogs from the server and populates our catalogs dictionary
|
|
"""
|
|
global catalog
|
|
managedinstallprefs = munkicommon.prefs()
|
|
sw_repo_baseurl = managedinstallprefs['SoftwareRepoURL']
|
|
catalog_dir = os.path.join(managedinstallprefs['ManagedInstallDir'], "catalogs")
|
|
|
|
for catalogname in cataloglist:
|
|
if not catalogname in catalog:
|
|
catalogurl = sw_repo_baseurl + "/catalogs/" + urllib2.quote(catalogname)
|
|
catalogpath = os.path.join(catalog_dir, catalogname)
|
|
message = "Getting catalog %s from %s..." % (catalogname, catalogurl)
|
|
munkicommon.log(message)
|
|
message = "Retreiving catalog '%s'..." % catalogname
|
|
(newcatalog, err) = getHTTPfileIfNewerAtomically(catalogurl, catalogpath, message=message)
|
|
if newcatalog:
|
|
catalog[catalogname] = makeCatalogDB(FoundationPlist.readPlist(newcatalog))
|
|
else:
|
|
munkicommon.display_error("Could not retreive catalog %s from server." % catalog)
|
|
munkicommon.display_error(err)
|
|
|
|
|
|
def getmanifest(partialurl, suppress_errors=False):
|
|
"""
|
|
Gets a manifest from the server
|
|
"""
|
|
managedinstallprefs = munkicommon.prefs()
|
|
sw_repo_baseurl = managedinstallprefs['SoftwareRepoURL']
|
|
manifest_dir = os.path.join(managedinstallprefs['ManagedInstallDir'], "manifests")
|
|
|
|
if partialurl.startswith("http"):
|
|
# then it's really a request for the client's primary manifest
|
|
manifesturl = partialurl
|
|
manifestname = "client_manifest.plist"
|
|
else:
|
|
# request for nested manifest
|
|
manifestname = os.path.split(partialurl)[1]
|
|
manifesturl = sw_repo_baseurl + "/manifests/" + urllib2.quote(partialurl)
|
|
|
|
manifestpath = os.path.join(manifest_dir, manifestname)
|
|
message = "Getting manifest %s from %s..." % (manifestname, manifesturl)
|
|
munkicommon.log(message)
|
|
message = "Retreiving list of software for this machine..."
|
|
(newmanifest, err) = getHTTPfileIfNewerAtomically(manifesturl, manifestpath, message=message)
|
|
if not newmanifest and not suppress_errors:
|
|
munkicommon.display_error("Could not retreive manifest %s from the server." % partialurl)
|
|
munkicommon.display_error(err)
|
|
|
|
return newmanifest
|
|
|
|
|
|
def getPrimaryManifest(alternate_id):
|
|
"""
|
|
Gets the client manifest from the server
|
|
"""
|
|
global errors
|
|
manifest = ""
|
|
managedinstallprefs = munkicommon.prefs()
|
|
manifesturl = managedinstallprefs['ManifestURL']
|
|
clientidentifier = managedinstallprefs.get('ClientIdentifier','')
|
|
|
|
if not manifesturl.endswith('?') and not manifesturl.endswith('/'):
|
|
manifesturl = manifesturl + "/"
|
|
if alternate_id:
|
|
# use id passed in at command-line
|
|
manifesturl = manifesturl + urllib2.quote(alternate_id)
|
|
elif clientidentifier:
|
|
# use client_identfier from /Library/Preferences/ManagedInstalls.plist
|
|
manifesturl = manifesturl + urllib2.quote(clientidentifier)
|
|
else:
|
|
# no client identifier specified, so use the hostname
|
|
hostname = os.uname()[1]
|
|
munkicommon.display_detail("No client id specified. Requesting %s..." % (manifesturl + hostname))
|
|
manifest = getmanifest(manifesturl + hostname,suppress_errors=True)
|
|
if not manifest:
|
|
# try the short hostname
|
|
munkicommon.display_detail("Request failed. Trying %s..." % (manifesturl + hostname.split('.')[0]))
|
|
manifest = getmanifest(manifesturl + hostname.split('.')[0], suppress_errors=True)
|
|
if not manifest:
|
|
# last resort - try for the site_default manifest
|
|
munkicommon.display_detail("Request failed. Trying %s..." % (manifesturl + "site_default"))
|
|
manifesturl = manifesturl + "site_default"
|
|
|
|
if not manifest:
|
|
manifest = getmanifest(manifesturl)
|
|
if manifest:
|
|
# clear out any errors we got while trying to find
|
|
# the primary manifest
|
|
errors = ""
|
|
|
|
return manifest
|
|
|
|
|
|
def getInstallCount(installinfo):
|
|
count = 0
|
|
for item in installinfo.get('managed_installs',[]):
|
|
if 'installer_item' in item:
|
|
count +=1
|
|
return count
|
|
|
|
|
|
def getRemovalCount(installinfo):
|
|
count = 0
|
|
for item in installinfo.get('removals',[]):
|
|
if 'installed' in item:
|
|
if item['installed']:
|
|
count +=1
|
|
return count
|
|
|
|
|
|
def checkServer():
|
|
'''in progress'''
|
|
managedinstallprefs = munkicommon.prefs()
|
|
manifesturl = managedinstallprefs['ManifestURL']
|
|
# deconstruct URL so we can check availability
|
|
port = 80
|
|
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(manifesturl)
|
|
# get rid of any embedded username/password
|
|
netlocparts = netloc.split("@")
|
|
netloc = netlocparts[-1]
|
|
# split into host and port if present
|
|
netlocparts = netloc.split(":")
|
|
host = netlocparts[0]
|
|
if len(netlocparts) == 2:
|
|
port = netlocparts[1]
|
|
|
|
s = socket.socket()
|
|
#try:
|
|
s.connect((host, port))
|
|
s.close()
|
|
return True
|
|
#except:
|
|
#return False
|
|
|
|
|
|
# HTTP download functions
|
|
#
|
|
# Handles http downloads
|
|
#
|
|
# Supports Last-modified and If-modified-since headers so
|
|
# we download from the server only if we don't have it in the
|
|
# local cache, or the locally cached item is older than the
|
|
# one on the server.
|
|
#
|
|
# Possible failure mode: if client's main catalog gets pointed
|
|
# to a different, older, catalog, we'll fail to retreive it.
|
|
# Need to check content length as well, and if it changes, retreive
|
|
# it anyway.
|
|
#
|
|
# Should probably cleanup/unify
|
|
# httpDownload/getfilefromhttpurl/getHTTPfileIfNewerAtomically
|
|
#
|
|
|
|
# urllib2 has no handler for client certificates, so make one...
|
|
# Subclass HTTPSClientAuthHandler adapted from the following sources:
|
|
# http://www.osmonov.com/2009/04/client-certificates-with-urllib2.html
|
|
# http://www.threepillarsoftware.com/soap_client_auth
|
|
# http://bugs.python.org/issue3466
|
|
# bcw
|
|
class HTTPSClientAuthHandler(urllib2.HTTPSHandler):
|
|
def __init__(self, key, cert):
|
|
urllib2.HTTPSHandler.__init__(self)
|
|
self.key = key
|
|
self.cert = cert
|
|
def https_open(self, req):
|
|
# Rather than pass in a reference to a connection class, we pass in
|
|
# a reference to a function which, for all intents and purposes,
|
|
# will behave as a constructor
|
|
return self.do_open(self.getConnection, req)
|
|
def getConnection(self, host, timeout=300):
|
|
return httplib.HTTPSConnection(host, key_file=self.key, cert_file=self.cert)
|
|
|
|
# An empty subclass for identifying missing certs, bcw
|
|
# Maybe there is a better place for this?
|
|
class UseClientCertificateError(IOError):
|
|
pass
|
|
|
|
def httpDownload(url, filename, headers={}, postData=None, reporthook=None, message=None):
|
|
|
|
# The required name for combination certifcate and private key
|
|
# File must be PEM formatted and include the client's private key
|
|
# bcw
|
|
pemfile = 'munki.pem'
|
|
|
|
# Grab the prefs for UseClientCertificate and construct a loc for the cert, bcw
|
|
ManagedInstallDir = munkicommon.ManagedInstallDir()
|
|
UseClientCertificate = munkicommon.UseClientCertificate()
|
|
cert = os.path.join(ManagedInstallDir, 'certs', pemfile)
|
|
|
|
reqObj = urllib2.Request(url, postData, headers)
|
|
|
|
if UseClientCertificate == True:
|
|
# Check for the existence of the PEM file, bcw
|
|
if os.path.isfile(cert):
|
|
# Construct a secure urllib2 opener, bcw
|
|
secureopener = urllib2.build_opener(HTTPSClientAuthHandler(cert, cert))
|
|
fp = secureopener.open(reqObj)
|
|
else:
|
|
# No x509 cert so fail -0x509 (decimal -1289). So amusing. bcw
|
|
raise UseClientCertificateError(-1289, "PEM file missing, %s" % cert)
|
|
|
|
else:
|
|
fp = urllib2.urlopen(reqObj)
|
|
|
|
headers = fp.info()
|
|
|
|
if message:
|
|
# log always, display if verbose is 2 or more
|
|
munkicommon.display_detail(message)
|
|
if munkicommon.munkistatusoutput:
|
|
# send to detail field on MunkiStatus
|
|
munkistatus.detail(message)
|
|
|
|
#read & write fileObj to filename
|
|
tfp = open(filename, 'wb')
|
|
result = filename, headers
|
|
bs = 1024*8
|
|
size = -1
|
|
read = 0
|
|
blocknum = 0
|
|
|
|
if reporthook:
|
|
if "content-length" in headers:
|
|
size = int(headers["Content-Length"])
|
|
reporthook(blocknum, bs, size)
|
|
|
|
while 1:
|
|
block = fp.read(bs)
|
|
if block == "":
|
|
break
|
|
read += len(block)
|
|
tfp.write(block)
|
|
blocknum += 1
|
|
if reporthook:
|
|
reporthook(blocknum, bs, size)
|
|
|
|
fp.close()
|
|
tfp.close()
|
|
|
|
# raise exception if actual size does not match content-length header
|
|
if size >= 0 and read < size:
|
|
raise ContentTooShortError("retrieval incomplete: got only %i out "
|
|
"of %i bytes" % (read, size), result)
|
|
|
|
return result
|
|
|
|
|
|
def getfilefromhttpurl(url,filepath, ifmodifiedsince=None, message=None):
|
|
"""
|
|
gets a file from a url.
|
|
If 'ifmodifiedsince' is specified, this header is set
|
|
and the file is not retreived if it hasn't changed on the server.
|
|
Returns 0 if successful, or HTTP error code
|
|
"""
|
|
def reporthook(block_count, block_size, file_size):
|
|
if (file_size > 0):
|
|
max_blocks = file_size/block_size
|
|
munkicommon.display_percent_done(block_count, max_blocks)
|
|
|
|
try:
|
|
request_headers = {}
|
|
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, message=message)
|
|
if 'last-modified' in headers:
|
|
# set the modtime of the downloaded file to the modtime of the
|
|
# file on the server
|
|
modtimestr = headers['last-modified']
|
|
modtimetuple = time.strptime(modtimestr, "%a, %d %b %Y %H:%M:%S %Z")
|
|
modtimeint = calendar.timegm(modtimetuple)
|
|
os.utime(filepath, (time.time(), modtimeint))
|
|
|
|
except urllib2.HTTPError, err:
|
|
return err.code
|
|
# Uncommented the exception handler below and added str(err)
|
|
# This will catch missing or invalid certs/keys in getHTTPfileIfNewerAtomically
|
|
# bcw
|
|
except urllib2.URLError, err:
|
|
return str(err)
|
|
# This will catch missing certs in getHTTPfileIfNewerAtomically, bcw
|
|
except UseClientCertificateError, err:
|
|
return err
|
|
except IOError, err:
|
|
return err
|
|
except Exception, err:
|
|
return (-1, err)
|
|
|
|
return 0
|
|
|
|
|
|
def getHTTPfileIfNewerAtomically(url,destinationpath, message=None):
|
|
"""
|
|
Gets file from HTTP URL, only if newer on web server.
|
|
Replaces pre-existing file only on success. (thus 'Atomically')
|
|
"""
|
|
err = None
|
|
mytemppath = os.path.join(munkicommon.tmpdir,"TempDownload")
|
|
if os.path.exists(destinationpath):
|
|
modtime = os.stat(destinationpath).st_mtime
|
|
else:
|
|
modtime = None
|
|
result = getfilefromhttpurl(url, mytemppath, ifmodifiedsince=modtime, message=message)
|
|
if result == 0:
|
|
try:
|
|
os.rename(mytemppath, destinationpath)
|
|
return destinationpath, err
|
|
except:
|
|
err = "Could not write to %s" % destinationpath
|
|
destinationpath = None
|
|
elif result == 304:
|
|
# not modified, return existing file
|
|
munkicommon.display_debug1("%s already exists and is up-to-date." % destinationpath)
|
|
return destinationpath, err
|
|
# Added to catch private key errors when the opener is constructed, bcw
|
|
elif result == '<urlopen error SSL_CTX_use_PrivateKey_file error>':
|
|
err = "SSL_CTX_use_PrivateKey_file error: PrivateKey Invalid or Missing"
|
|
destinationpath = None
|
|
# Added to catch certificate errors when the opener is constructed, bcw
|
|
elif result == '<urlopen error SSL_CTX_use_certificate_chain_file error>':
|
|
err = "SSL_CTX_use_certificate_chain_file error: Certificate Invalid or Missing"
|
|
destinationpath = None
|
|
else:
|
|
err = "Error code: %s retreiving %s" % (result, url)
|
|
destinationpath = None
|
|
|
|
if os.path.exists(mytemppath):
|
|
os.remove(mytemppath)
|
|
|
|
return destinationpath, err
|
|
|
|
|
|
def getMachineFacts():
|
|
global machine
|
|
|
|
machine['arch'] = os.uname()[4]
|
|
cmd = ['/usr/bin/sw_vers', '-productVersion']
|
|
p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(output, err) = p.communicate()
|
|
# format version string like "10.5.8", so that "10.6" becomes "10.6.0"
|
|
machine['os_vers'] = munkicommon.padVersionString(output.rstrip("\n"),3)
|
|
|
|
|
|
# some globals
|
|
machine = {}
|
|
|
|
def check(id=''):
|
|
'''Checks for available new or updated managed software, downloading installer items
|
|
if needed. Returns 1 if there are available updates, 0 if there are no available updates,
|
|
and -1 if there were errors.'''
|
|
|
|
getMachineFacts()
|
|
|
|
ManagedInstallDir = munkicommon.ManagedInstallDir()
|
|
|
|
if munkicommon.munkistatusoutput:
|
|
munkistatus.activate()
|
|
munkistatus.message("Checking for available updates...")
|
|
munkistatus.percent("-1")
|
|
|
|
munkicommon.log("### Beginning managed software check ###")
|
|
|
|
mainmanifestpath = getPrimaryManifest(id)
|
|
if munkicommon.stopRequested():
|
|
return 0
|
|
|
|
installinfo = {}
|
|
|
|
if mainmanifestpath:
|
|
# initialize our installinfo record
|
|
installinfo['managed_installs'] = []
|
|
installinfo['removals'] = []
|
|
munkicommon.display_detail("**Checking for installs**")
|
|
installinfo = processManifestForInstalls(mainmanifestpath, installinfo)
|
|
if munkicommon.stopRequested():
|
|
return 0
|
|
|
|
# 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 installinfo['managed_installs']:
|
|
if "installer_item" in item:
|
|
installer_item_list.append(item["installer_item"])
|
|
|
|
cachedir = os.path.join(ManagedInstallDir, "Cache")
|
|
for item in os.listdir(cachedir):
|
|
if item not in installer_item_list:
|
|
munkicommon.display_detail("Removing %s from cache" % item)
|
|
os.unlink(os.path.join(cachedir, item))
|
|
|
|
if munkicommon.munkistatusoutput:
|
|
# reset progress indicator and detail field
|
|
munkistatus.percent("-1")
|
|
munkistatus.detail('')
|
|
|
|
# now generate a list of items to be uninstalled
|
|
munkicommon.display_detail("**Checking for removals**")
|
|
if munkicommon.stopRequested():
|
|
return 0
|
|
installinfo = processManifestForRemovals(mainmanifestpath, installinfo)
|
|
if munkicommon.munkistatusoutput:
|
|
munkistatus.disableStopButton()
|
|
|
|
# need to write out install list so the autoinstaller
|
|
# can use it to install things in the right order
|
|
installinfochanged = True
|
|
installinfopath = os.path.join(ManagedInstallDir, "InstallInfo.plist")
|
|
if os.path.exists(installinfopath):
|
|
oldinstallinfo = FoundationPlist.readPlist(installinfopath)
|
|
if oldinstallinfo == installinfo:
|
|
installinfochanged = False
|
|
munkicommon.display_detail("No change in InstallInfo.")
|
|
if installinfochanged:
|
|
FoundationPlist.writePlist(installinfo, os.path.join(ManagedInstallDir, "InstallInfo.plist"))
|
|
|
|
else:
|
|
# couldn't get a primary manifest. Check to see if we have a valid InstallList from
|
|
# an earlier run.
|
|
munkicommon.display_error("Could not retreive managed install primary manifest.")
|
|
installinfopath = os.path.join(ManagedInstallDir, "InstallInfo.plist")
|
|
if os.path.exists(installinfopath):
|
|
try:
|
|
installinfo = FoundationPlist.readPlist(installinfopath)
|
|
except:
|
|
installinfo = {}
|
|
|
|
|
|
installcount = getInstallCount(installinfo)
|
|
removalcount = getRemovalCount(installinfo)
|
|
|
|
if installcount:
|
|
munkicommon.display_info("The following items will be installed or upgraded:")
|
|
for item in installinfo['managed_installs']:
|
|
if item.get('installer_item'):
|
|
munkicommon.display_info(" + %s-%s" % (item.get('name',''), item.get('version_to_install','')))
|
|
if item.get('description'):
|
|
munkicommon.display_info(" %s" % item['description'])
|
|
if item.get('RestartAction') == 'RequireRestart':
|
|
munkicommon.display_info(" *Restart required")
|
|
if removalcount:
|
|
munkicommon.display_info("The following items will be removed:")
|
|
for item in installinfo['removals']:
|
|
if item.get('installed'):
|
|
munkicommon.display_info(" - %s" % item.get('name'))
|
|
if item.get('RestartAction') == 'RequireRestart':
|
|
munkicommon.display_info(" *Restart required")
|
|
|
|
if installcount == 0 and removalcount == 0:
|
|
munkicommon.display_info("No changes to managed software are available.")
|
|
|
|
munkicommon.log("### End managed software check ###")
|
|
|
|
if munkicommon.errors:
|
|
reporterrors()
|
|
|
|
if installcount or removalcount:
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
|
|
def main():
|
|
pass
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|