- forced_installs and forced_uninstalls now work with items that are part of a dependency relationship

- makecatalogs now has no OS X-specific dependencies

- munkiimport fixes for repos hosted via SMB/CIFS.

git-svn-id: http://munki.googlecode.com/svn/trunk@1097 a4e17f2e-e282-11dd-95e1-755cbddbdd66
This commit is contained in:
Greg Neagle
2011-04-06 21:54:29 +00:00
parent b20b212431
commit 1c9e5784c1
7 changed files with 196 additions and 86 deletions
+40 -6
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
# encoding: utf-8
#
# Copyright 2009-2011 Greg Neagle.
@@ -30,17 +30,51 @@ User calling this needs to be able to write to repo/catalogs.
import sys
import os
import optparse
from munkilib import munkicommon
try:
from munkilib import FoundationPlist as plistlib
except ImportError:
try:
import FoundationPlist as plistlib
except ImportError:
# maybe we're not on an OS X machine...
print >> sys.stderr, ("WARNING: FoundationPlist is not available, "
"using plistlib instead.")
import plistlib
try:
from munkilib.munkicommon import listdir, get_version
except ImportError:
# munkilib is not available
def listdir(path):
"""OSX HFS+ string encoding safe listdir().
Args:
path: path to list contents of
Returns:
list of contents, items as str or unicode types
"""
# if os.listdir() is supplied a unicode object for the path,
# it will return unicode filenames instead of their raw fs-dependent
# version, which is decomposed utf-8 on OSX.
#
# we use this to our advantage here and have Python do the decoding
# work for us, instead of decoding each item in the output list.
#
# references:
# http://docs.python.org/howto/unicode.html#unicode-filenames
# http://developer.apple.com/library/mac/#qa/qa2001/qa1235.html
# http://lists.zerezo.com/git/msg643117.html
# http://unicode.org/reports/tr15/ section 1.2
if type(path) is str:
path = unicode(path, 'utf-8')
elif type(path) is not unicode:
path = unicode(path)
return os.listdir(path)
def get_version():
'''Placeholder if munkilib is not available'''
return 'UNKNOWN'
def makecatalogs(repopath):
'''Assembles all pkginfo files into catalogs.
@@ -54,7 +88,7 @@ def makecatalogs(repopath):
catalogs = {}
catalogs['all'] = []
for dirpath, dirnames, filenames in os.walk(pkgsinfopath):
subdir = dirpath[len(pkgsinfopath):]
#subdir = dirpath[len(pkgsinfopath):]
for name in filenames:
if name.startswith("._") or name == ".DS_Store":
# don't process these
@@ -102,7 +136,7 @@ def makecatalogs(repopath):
if not os.path.exists(catalogpath):
os.mkdir(catalogpath)
else:
for item in munkicommon.listdir(catalogpath):
for item in listdir(catalogpath):
itempath = os.path.join(catalogpath, item)
if os.path.isfile(itempath):
os.remove(itempath)
@@ -122,7 +156,7 @@ def main():
options, arguments = p.parse_args()
if options.version:
print munkicommon.get_version()
print get_version()
exit(0)
if len(arguments) == 0:
+1 -8
View File
@@ -157,10 +157,6 @@ def getCatalogInfoFromDmg(dmgpath, options):
cataloginfo = {}
cataloginfo['name'] = iteminfo.get('CFBundleName',
os.path.splitext(item)[0])
#cataloginfo['version'] = \
# munkicommon.padVersionString(
# iteminfo.get('CFBundleShortVersionString', "0")
# ,5)
cataloginfo['version'] = \
iteminfo.get('CFBundleShortVersionString', "0")
cataloginfo['installs'] = [iteminfo]
@@ -445,7 +441,7 @@ def main():
"To use it with munki, you should encapsulate it "
"in a disk image.\n") % item
# need to walk the dir and add it all up
for (path, dirs, files) in os.walk(item):
for (path, unused_dirs, files) in os.walk(item):
for name in files:
filename = os.path.join(path, name)
# use os.lstat so we don't follow symlinks
@@ -536,9 +532,6 @@ def main():
munkicommon.MunkiLooseVersion(minosversion)):
minosversion = thisminosversion
if 'CFBundleShortVersionString' in iteminfodict:
#thisitemversion = \
# munkicommon.padVersionString(
# iteminfodict['CFBundleShortVersionString'],5)
thisitemversion = \
iteminfodict['CFBundleShortVersionString']
if (munkicommon.MunkiLooseVersion(thisitemversion) >
+9 -10
View File
@@ -107,7 +107,13 @@ def mountRepoCLI():
return
os.mkdir(repo_path)
print 'Attempting to mount fileshare %s:' % repo_url
cmd = ['/sbin/mount_afp', '-i', repo_url, repo_path]
if repo_url.startswith('afp:'):
cmd = ['/sbin/mount_afp', '-i', repo_url, repo_path]
elif repo_url.startswith('smb:'):
cmd = ['/sbin/mount_smbfs', repo_url[4:], repo_path]
else:
print >> sys.stderr, 'Unsupported filesystem URL!'
return
retcode = subprocess.call(cmd)
if retcode:
os.rmdir(repo_path)
@@ -236,9 +242,7 @@ def promptForSubdirectory(subdirectory):
% subdirectory)
if newdir:
repo_path = pref('repo_path')
if not os.path.exists(repo_path):
mountRepoGUI()
if not os.path.exists(repo_path):
if not repoAvailable():
raise RepoCopyError('Could not connect to munki repo.')
destination_path = os.path.join(repo_path, 'pkgs', newdir)
if not os.path.exists(destination_path):
@@ -283,9 +287,6 @@ def makeCatalogDB():
if name == 'NO NAME' or vers == 'NO VERSION':
munkicommon.display_warning('Bad pkginfo: %s' % item)
# normalize the version number
#vers = munkicommon.padVersionString(vers, 5)
# add to hash table
if 'installer_item_hash' in item:
if not item['installer_item_hash'] in hash_table:
@@ -425,9 +426,7 @@ def makeCatalogs():
# didn't find it; assume the default install path
makecatalogs_path = '/usr/local/munki/makepkginfo'
repo_path = pref('repo_path')
if not os.path.exists(repo_path):
mountRepoGUI()
if not os.path.exists(repo_path):
if not repoAvailable():
raise RepoCopyError('Could not connect to munki repo.')
proc = subprocess.Popen([makecatalogs_path, repo_path],
bufsize=-1, stdout=subprocess.PIPE,
+1 -3
View File
@@ -21,7 +21,7 @@ using the CS3/CS4/CS5 Deployment Toolkits.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
#import sys
import os
import subprocess
import time
@@ -132,8 +132,6 @@ def getPayloadInfo(dirpath):
if propname == 'ProductName':
payloadinfo['display_name'] = propvalue
if propname == 'ProductVersion':
#payloadinfo['version'] = \
#munkicommon.padVersionString(propvalue,5)
payloadinfo['version'] = propvalue
installmetadata = \
+96 -4
View File
@@ -26,6 +26,7 @@ import subprocess
import adobeutils
import munkicommon
import munkistatus
import updatecheck
import FoundationPlist
from removepackages import removepackages
@@ -455,6 +456,51 @@ def removeCopiedItems(itemlist):
return retcode
def itemPrereqsInSkippedItems(item, skipped_items):
'''Looks for item prerequisites (requires and update_for) in the list
of skipped items. Returns a list of matches.'''
munkicommon.display_debug1(
'Checking for skipped prerequisites for %s-%s'
% (item['name'], item.get('version_to_install')))
# get list of prerequisites for this item
prerequisites = item.get('requires', [])
prerequisites.extend(item.get('update_for', []))
if not prerequisites:
munkicommon.display_debug1(
'%s-%s has no prerequisites.'
% (item['name'], item.get('version_to_install')))
return []
munkicommon.display_debug1('Prerequisites: %s' % ", ".join(prerequisites))
# build a dictionary of names and versions of skipped items
skipped_item_dict = {}
for skipped_item in skipped_items:
if skipped_item['name'] not in skipped_item_dict:
skipped_item_dict[skipped_item['name']] = []
normalized_version = updatecheck.trimVersionString(
skipped_item.get('version_to_install', '0.0'))
munkicommon.display_debug1('Adding skipped item: %s-%s'
% (skipped_item['name'], normalized_version))
skipped_item_dict[skipped_item['name']].append(normalized_version)
# now check prereqs against the skipped items
matched_prereqs = []
for prereq in prerequisites:
(name, version) = updatecheck.nameAndVersion(prereq)
munkicommon.display_debug1(
'Comparing %s-%s against skipped items' % (name, version))
if name in skipped_item_dict:
if version:
version = updatecheck.trimVersionString(version)
if version in skipped_item_dict[name]:
matched_prereqs.append(prereq)
else:
matched_prereqs.append(prereq)
return matched_prereqs
def installWithInfo(dirpath, installlist, only_forced=False, applesus=False):
"""
Uses the installlist to install items in the
@@ -470,13 +516,26 @@ def installWithInfo(dirpath, installlist, only_forced=False, applesus=False):
skipped_installs.append(item)
munkicommon.display_detail(
('Skipping install of %s because it\'s not flagged for '
'forced_install') % item['name'])
'forced_install.') % item['name'])
continue
elif blockingApplicationsRunning(item):
skipped_installs.append(item)
munkicommon.display_detail(
"Skipping forced install of %s" % item['name'])
'Skipping forced/background install of %s because '
'blocking application(s) running.'
% item['name'])
continue
skipped_prereqs = itemPrereqsInSkippedItems(
item, skipped_installs)
if skipped_prereqs:
# need to skip this too
skipped_installs.append(item)
munkicommon.display_detail(
'Skipping forced/background install of %s because these '
'prerequisites were skipped: %s'
% (item['name'], ", ".join(skipped_prereqs)))
continue
if munkicommon.stopRequested():
return restartflag, skipped_installs
@@ -491,7 +550,7 @@ def installWithInfo(dirpath, installlist, only_forced=False, applesus=False):
munkistatus.message("Installing %s (%s of %s)..." %
(display_name, itemindex,
len(installlist)))
munkistatus.detail("")
munkistatus.detail('')
munkistatus.percent(-1)
else:
munkicommon.display_status("Installing %s (%s of %s)" %
@@ -771,6 +830,27 @@ def runScript(itemname, path, scriptname):
return retcode
def skippedItemsThatRequireThisItem(item, skipped_items):
'''Looks for items in the skipped_items that require or are update_for
the current item. Returns a list of matches.'''
munkicommon.display_debug1(
'Checking for skipped items that require %s' % item['name'])
matched_skipped_items = []
for skipped_item in skipped_items:
# get list of prerequisites for this skipped_item
prerequisites = skipped_item.get('requires', [])
prerequisites.extend(skipped_item.get('update_for', []))
munkicommon.display_debug1(
'%s has these prerequisites: %s'
% (skipped_item['name'], ', '.join(prerequisites)))
for prereq in prerequisites:
(prereq_name, unused_version) = updatecheck.nameAndVersion(prereq)
if prereq_name == item['name']:
matched_skipped_items.append(skipped_item['name'])
return matched_skipped_items
def processRemovals(removallist, only_forced=False):
'''processes removals from the removal list'''
restartFlag = False
@@ -787,8 +867,20 @@ def processRemovals(removallist, only_forced=False):
elif blockingApplicationsRunning(item):
skipped_removals.append(item)
munkicommon.display_detail(
'Skipping forced removal of %s ' % item['name'])
'Skipping forced/background removal of %s because '
'blocking application(s) running.' % item['name'])
continue
dependent_skipped_items = skippedItemsThatRequireThisItem(
item, skipped_removals)
if dependent_skipped_items:
# need to skip this too
skipped_removals.append(item)
munkicommon.display_detail(
'Skipping forced/background removal of %s because these '
'skipped items required it: %s'
% (item['name'], ", ".join(dependent_skipped_items)))
continue
if munkicommon.stopRequested():
return restartFlag
if not item.get('installed'):
+1 -12
View File
@@ -846,7 +846,6 @@ def getExtendedVersion(bundlepath):
plist = FoundationPlist.readPlist(infoPlist)
versionstring = getVersionString(plist)
if versionstring:
#return padVersionString(versionstring, 5)
return versionstring
# no version number in Info.plist. Maybe old-style package?
@@ -865,7 +864,6 @@ def getExtendedVersion(bundlepath):
if len(parts) == 2:
label = parts[0]
if label == 'Version':
#return padVersionString(parts[1], 5)
return parts[1]
# didn't find a version number, so return 0...
@@ -885,8 +883,6 @@ def parsePkgRefs(filename):
pkginfo = {}
pkginfo['packageid'] = \
ref.attributes['id'].value.encode('UTF-8')
#pkginfo['version'] = padVersionString(
# ref.attributes['version'].value.encode('UTF-8'), 5)
pkginfo['version'] = \
ref.attributes['version'].value.encode('UTF-8')
if 'installKBytes' in keys:
@@ -904,9 +900,6 @@ def parsePkgRefs(filename):
pkginfo = {}
pkginfo['packageid'] = \
ref.attributes['identifier'].value.encode('UTF-8')
#pkginfo['version'] = \
# padVersionString(
# ref.attributes['version'].value.encode('UTF-8'),5)
pkginfo['version'] = \
ref.attributes['version'].value.encode('UTF-8')
payloads = ref.getElementsByTagName('payload')
@@ -1014,8 +1007,6 @@ def getOnePackageInfo(pkgpath):
if len(parts) == 2:
label = parts[0]
if label == 'Version':
#pkginfo['version'] = \
# padVersionString(parts[1], 5)
pkginfo['version'] = parts[1]
if label == 'Title':
pkginfo['name'] = parts[1]
@@ -1132,7 +1123,6 @@ def getInstalledPackageVersion(pkgid):
"""
# First check (Leopard and later) package database
proc = subprocess.Popen(['/usr/sbin/pkgutil',
'--pkg-info-plist', pkgid],
bufsize=1,
@@ -1151,8 +1141,7 @@ def getInstalledPackageVersion(pkgid):
if pkgid == foundbundleid:
display_debug2('\tThis machine has %s, version %s' %
(pkgid, foundvers))
#return padVersionString(foundvers, 5)
return foundvers
return foundvers
# If we got to this point, we haven't found the pkgid yet.
# Check /Library/Receipts
+48 -43
View File
@@ -59,7 +59,6 @@ def makeCatalogDB(catalogitems):
munkicommon.display_warning('Bad pkginfo: %s' % item)
# normalize the version number
#vers = munkicommon.padVersionString(vers, 5)
vers = trimVersionString(vers)
# build indexes for items by name and version
@@ -279,8 +278,6 @@ def compareVersions(thisvers, thatvers):
1 if thisvers is the same as thatvers
2 if thisvers is newer than thatvers
"""
#thisvers = munkicommon.padVersionString(thisvers, 5)
#thatvers = munkicommon.padVersionString(thatvers, 5)
if (munkicommon.MunkiLooseVersion(thisvers) <
munkicommon.MunkiLooseVersion(thatvers)):
return -1
@@ -851,9 +848,6 @@ def getItemDetail(name, cataloglist, vers=''):
if includedversion:
vers = includedversion
if vers:
# make sure version is in 1.0.0.0.0 format
#vers = munkicommon.padVersionString(vers, 5)
# normalize the version string
vers = trimVersionString(vers)
else:
vers = 'latest'
@@ -889,9 +883,6 @@ def getItemDetail(name, cataloglist, vers=''):
# 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)
min_os_vers = item['minimum_os_version']
munkicommon.display_debug1(
'Considering item %s, ' % item['name'] +
@@ -912,9 +903,6 @@ def getItemDetail(name, cataloglist, vers=''):
continue
if 'maximum_os_version' in item:
#max_os_vers = \
# munkicommon.padVersionString(
# item['maximum_os_version'],3)
max_os_vers = item['maximum_os_version']
munkicommon.display_debug1(
'Considering item %s, ' % item['name'] +
@@ -1521,12 +1509,15 @@ def processInstall(manifestitem, cataloglist, installinfo):
iteminfo['version_to_install'] = item_pl.get(
'version','UNKNOWN')
# currently we will ignore the forced_install key
# if the item is part of a dependency graph or needs a restart or
# logout...
if (not item_pl.get('requires') and not item_pl.get('update_for')
and not item_pl.get('RestartAction')):
if item_pl.get('forced_install'):
# we will ignore the forced_install key
# if the item needs a restart or logout...
if item_pl.get('forced_install'):
if item_pl.get('RestartAction'):
munkicommon.display_warning(
'Ignoring forced_install key for %s '
'because RestartAction is %s.'
% (item_pl['name'], item_pl.get('RestartAction')))
else:
iteminfo['forced_install'] = True
# optional keys
@@ -1539,6 +1530,8 @@ def processInstall(manifestitem, cataloglist, installinfo):
'package_path',
'blocking_applications',
'installs',
'requires',
'update_for',
'preinstall_script',
'postinstall_script',
'items_to_copy', # used w/ copy_from_dmg
@@ -1848,19 +1841,26 @@ def processRemoval(manifestitem, cataloglist, installinfo):
iteminfo['display_name'] = uninstall_item.get('display_name', '')
iteminfo['description'] = 'Will be removed.'
# currently we will ignore the forced_install and forced_uninstall key if
# the item is part of a dependency graph or needs a restart or logout...
if (not uninstall_item.get('requires')
and not uninstall_item.get('update_for')
and not uninstall_item.get('RestartAction')):
iteminfo['forced_uninstall'] = uninstall_item.get(
'forced_uninstall', False)
# we will ignore the forced_install and forced_uninstall key if
# the item needs a restart or logout...
if uninstall_item.get('forced_uninstall'):
if uninstall_item.get('RestartAction'):
munkicommon.display_warning(
'Ignoring forced_uninstall key for %s '
'because RestartAction is %s.'
% (uninstall_item['name'],
uninstall_item.get('RestartAction')))
else:
iteminfo['forced_uninstall'] = True
if 'blocking_applications' in uninstall_item:
iteminfo['blocking_applications'] = \
uninstall_item['blocking_applications']
if 'installs' in uninstall_item:
iteminfo['installs'] = uninstall_item['installs']
# some keys we'll copy if they exist
optionalKeys = ['blocking_applications',
'installs',
'requires',
'update_for']
for key in optionalKeys:
if key in uninstall_item:
iteminfo[key] = uninstall_item[key]
if packagesToRemove:
# remove references for each package
@@ -2692,9 +2692,6 @@ def getMachineFacts():
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, unused_err) = proc.communicate()
# format version string like '10.5.8', so that '10.6' becomes '10.6.0'
#MACHINE['os_vers'] = munkicommon.padVersionString(
# str(output).rstrip('\n'),3)
MACHINE['os_vers'] = str(output).rstrip('\n')
@@ -2757,7 +2754,8 @@ def check(client_id='', localmanifestpath=None):
cataloglist = getManifestValueForKey(mainmanifestpath, 'catalogs')
autoremovalitems = getAutoRemovalItems(installinfo, cataloglist)
if autoremovalitems:
munkicommon.display_detail('**Checking for implicit removals**')
munkicommon.display_detail(
'**Checking for implicit removals**')
for item in autoremovalitems:
if munkicommon.stopRequested():
return 0
@@ -2797,18 +2795,21 @@ def check(client_id='', localmanifestpath=None):
if os.path.exists(selfservemanifest):
# use catalogs from main manifest for self-serve manifest
cataloglist = getManifestValueForKey(mainmanifestpath, 'catalogs')
munkicommon.display_detail('**Processing self-serve choices**')
cataloglist = getManifestValueForKey(
mainmanifestpath, 'catalogs')
munkicommon.display_detail(
'**Processing self-serve choices**')
selfserveinstalls = getManifestValueForKey(selfservemanifest,
'managed_installs')
available_optional_installs = [item['name']
for item in installinfo.get('optional_installs',[])]
# filter the list, removing any items not in the current list of
# available self-serve installs
for item in installinfo.get('optional_installs',[])]
# filter the list, removing any items not in the current list
# of available self-serve installs
selfserveinstalls = [item for item in selfserveinstalls
if item in available_optional_installs]
for item in selfserveinstalls:
unused_result = processInstall(item, cataloglist, installinfo)
unused_result = processInstall(
item, cataloglist, installinfo)
# we don't need to filter uninstalls
processManifestForKey(selfservemanifest, 'managed_uninstalls',
installinfo, cataloglist)
@@ -2834,11 +2835,13 @@ def check(client_id='', localmanifestpath=None):
for item in installinfo['managed_installs']
if item.get('installed') == False and
not item.get('installer_item')]
# filter removals to get items already removed (or never installed)
# filter removals to get items already removed
# (or never installed)
removed_items = [item.get('name','')
for item in installinfo['removals']
if item.get('installed') == False]
if os.path.exists(selfservemanifest):
# for any item in the managed_uninstalls in the self-serve
# manifest that is not installed, we should remove it from
@@ -2914,10 +2917,12 @@ def check(client_id='', localmanifestpath=None):
# that need to be installed but are missing
# the installer_item; these might be partial
# downloads. So if we have no problem items, it's
# OK to get rid of any partial downloads hanging around.
# OK to get rid of any partial downloads hanging
# around.
os.unlink(os.path.join(cachedir, item))
elif item not in cache_list:
munkicommon.display_detail('Removing %s from cache' % item)
munkicommon.display_detail(
'Removing %s from cache' % item)
os.unlink(os.path.join(cachedir, item))
# write out install list so our installer