Files
munki/code/client/makecatalogs
Yoann Gini 04b33642b3 An option to silently skip pkg check (#795)
Add --skip-pkg-check (-s) option
This PR add a manual option to skip the pkg_check process in addition to PackageCompleteURL & PackageURL based detection.
Manual request for this option is usefull when a unique server contain data and metadata but both are push from different sources. (Like GIT + rsync)
The difference with the force option rely in the warning message that are avoided here and the check done on the metadata part aren't skiped.
2017-08-23 15:34:43 -07:00

367 lines
14 KiB
Python
Executable File

#!/usr/bin/env python
# encoding: utf-8
#
# Copyright 2009-2017 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
#
# https://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.
Recursively scans a directory, looking for installer item info files.
Builds a repo catalog from these files.
Assumes a pkgsinfo directory under repopath.
User calling this needs to be able to write to repo/catalogs.
"""
import hashlib
import os
import optparse
import plistlib
import sys
from munkilib.cliutils import pref, path2url, print_utf8, print_err_utf8
from munkilib.cliutils import get_version
from munkilib import munkirepo
def list_items_of_kind(repo, kind):
'''Returns a list of items of kind. Relative pathnames are prepended with
kind. (example: ['pkgsinfo/apps/Bar.plist', 'pkgsinfo/apps/Foo.plist'])'''
return [os.path.join(kind, item) for item in repo.itemlist(kind)]
def hash_icons(repo):
'''Builds a dictionary containing hashes for all our repo icons'''
errors = []
icons = {}
print_utf8("Getting list of icons...")
icon_list = repo.itemlist('icons')
# Don't hash the hashes, they aren't icons.
if '_icon_hashes.plist' in icon_list:
icon_list.remove('_icon_hashes.plist')
for icon_ref in icon_list:
print_utf8("Hashing %s..." % (icon_ref))
# Try to read the icon file
try:
icondata = repo.get('icons/' + icon_ref)
icons[icon_ref] = hashlib.sha256(icondata).hexdigest()
except munkirepo.RepoError, err:
errors.append(u'RepoError for %s: %s' % (icon_ref, unicode(err)))
except IOError, err:
errors.append(u'IO error for %s: %s' % (icon_ref, err))
except BaseException, err:
errors.append(u'Unexpected error for %s: %s' % (icon_ref, err))
return icons, errors
def verify_pkginfo(pkginfo_ref, pkginfo, pkgs_list, errors):
'''Returns True if referenced installer items are present,
False otherwise. Adds errors/warnings to the errors list'''
installer_type = pkginfo.get('installer_type')
if installer_type in ['nopkg', 'apple_update_metadata']:
# no associated installer item (pkg) for these types
return True
if pkginfo.get('PackageCompleteURL') or pkginfo.get('PackageURL'):
# installer item may be on a different server
return True
if not 'installer_item_location' in pkginfo:
errors.append("WARNING: %s is missing installer_item_location"
% pkginfo_ref)
return False
# Try to form a path and fail if the
# installer_item_location is not a valid type
try:
installeritempath = os.path.join(
"pkgs", pkginfo['installer_item_location'])
except TypeError:
errors.append("WARNING: invalid installer_item_location in %s"
% pkginfo_ref)
return False
# Check if the installer item actually exists
if not installeritempath in pkgs_list:
# do a case-insenstive comparison
found_caseinsensitive_match = False
for repo_pkg in pkgs_list:
if installeritempath.lower() == repo_pkg.lower():
errors.append(
"WARNING: %s refers to installer item: %s. "
"The pathname of the item in the repo has "
"different case: %s. This may cause issues "
"depending on the case-sensitivity of the "
"underlying filesystem."
% (pkginfo_ref,
pkginfo['installer_item_location'], repo_pkg))
found_caseinsensitive_match = True
break
if not found_caseinsensitive_match:
errors.append(
"WARNING: %s refers to missing installer item: %s"
% (pkginfo_ref, pkginfo['installer_item_location']))
return False
#uninstaller sanity checking
uninstaller_type = pkginfo.get('uninstall_method')
if uninstaller_type in ['AdobeCCPUninstaller']:
# uninstaller_item_location is required
if not 'uninstaller_item_location' in pkginfo:
errors.append(
"WARNING: %s is missing uninstaller_item_location"
% pkginfo_ref)
return False
# if an uninstaller_item_location is specified, sanity-check it
if 'uninstaller_item_location' in pkginfo:
try:
uninstalleritempath = os.path.join(
"pkgs", pkginfo['uninstaller_item_location'])
except TypeError:
errors.append("WARNING: invalid uninstaller_item_location "
"in %s" % pkginfo_ref)
return False
# Check if the uninstaller item actually exists
if not uninstalleritempath in pkgs_list:
# do a case-insenstive comparison
found_caseinsensitive_match = False
for repo_pkg in pkgs_list:
if uninstalleritempath.lower() == repo_pkg.lower():
errors.append(
"WARNING: %s refers to uninstaller item: %s. "
"The pathname of the item in the repo has "
"different case: %s. This may cause issues "
"depending on the case-sensitivity of the "
"underlying filesystem."
% (pkginfo_ref,
pkginfo['uninstaller_item_location'], repo_pkg))
found_caseinsensitive_match = True
break
if not found_caseinsensitive_match:
errors.append(
"WARNING: %s refers to missing uninstaller item: %s"
% (pkginfo_ref, pkginfo['uninstaller_item_location']))
return False
# if we get here we passed all the checks
return True
def process_pkgsinfo(repo, icons, force=False, skip_payload_check=False):
'''Processes pkginfo files and returns a dictionary of catalogs'''
errors = []
catalogs = {}
# get a list of pkgsinfo items
print_utf8("Getting list of pkgsinfo...")
try:
pkgsinfo_list = list_items_of_kind(repo, 'pkgsinfo')
except munkirepo.RepoError, err:
print_err_utf8(
"Error getting list of pkgsinfo items: %s" % unicode(err))
exit(-1)
# get a list of pkgs items
print_utf8("Getting list of pkgs...")
try:
pkgs_list = list_items_of_kind(repo, 'pkgs')
except munkirepo.RepoError, err:
print_err_utf8("Error getting list of pkgs items: %s" % unicode(err))
exit(-1)
# start with empty catalogs dict
catalogs = {}
catalogs['all'] = []
# Walk through the pkginfo files
for pkginfo_ref in pkgsinfo_list:
# Try to read the pkginfo file
try:
data = repo.get(pkginfo_ref)
pkginfo = plistlib.readPlistFromString(data)
except IOError, err:
errors.append("IO error for %s: %s" % (pkginfo_ref, err))
continue
except BaseException, err:
errors.append("Unexpected error for %s: %s" % (pkginfo_ref, err))
continue
if not 'name' in pkginfo:
errors.append("WARNING: %s is missing name" % pkginfo_ref)
continue
# don't copy admin notes to catalogs.
if pkginfo.get('notes'):
del pkginfo['notes']
# strip out any keys that start with "_"
# (example: pkginfo _metadata)
for key in pkginfo.keys():
if key.startswith('_'):
del pkginfo[key]
#sanity checking
if not skip_payload_check:
verified = verify_pkginfo(pkginfo_ref, pkginfo, pkgs_list, errors)
if not verified and not force:
# Skip this pkginfo unless we're running with force flag
continue
# append the pkginfo to the relevant catalogs
catalogs['all'].append(pkginfo)
for catalogname in pkginfo.get("catalogs", []):
if not catalogname:
errors.append("WARNING: %s has an empty catalogs array!"
% pkginfo_ref)
continue
if not catalogname in catalogs:
catalogs[catalogname] = []
catalogs[catalogname].append(pkginfo)
print_utf8("Adding %s to %s..." % (pkginfo_ref, catalogname))
# look for catalog names that differ only in case
duplicate_catalogs = []
for key in catalogs:
if key.lower() in [item.lower() for item in catalogs if item != key]:
duplicate_catalogs.append(key)
if duplicate_catalogs:
errors.append("WARNING: There are catalogs with names that differ only "
"by case. This may cause issues depending on the case-"
"sensitivity of the underlying filesystem: %s"
% duplicate_catalogs)
return catalogs, errors
def makecatalogs(repo, options):
'''Assembles all pkginfo files into catalogs.
Assumes a pkgsinfo directory under repopath.
User calling this needs to be able to write to the repo/catalogs
directory.'''
exit_code = 0
icons, errors = hash_icons(repo)
catalogs, catalog_errors = process_pkgsinfo(
repo, icons, force=options.force, skip_payload_check=options.skip_payload_check)
errors.extend(catalog_errors)
if errors:
# group all errors at the end for better visibility
print
for error in errors:
print_err_utf8(error)
exit_code = -1
# clear out old catalogs
try:
catalog_list = repo.itemlist('catalogs')
except munkirepo.RepoError:
catalog_list = []
for catalog_name in catalog_list:
if catalog_name not in catalogs.keys():
catalog_ref = os.path.join('catalogs', catalog_name)
try:
repo.delete(catalog_ref)
except munkirepo.RepoError:
print_err_utf8('Could not delete catalog %s' % catalog_name)
# write the new catalogs
print
for key in catalogs:
catalogpath = os.path.join("catalogs", key)
if len(catalogs[key]):
catalog_data = plistlib.writePlistToString(catalogs[key])
try:
repo.put(catalogpath, catalog_data)
print "Created %s..." % (catalogpath)
except munkirepo.RepoError, err:
print_err_utf8(
u'Failed to create catalog %s: %s' % (key, unicode(err)))
else:
print_err_utf8(
"WARNING: Did not create catalog %s because it is empty" % key)
exit_code = -1
if icons:
icon_hashes_plist = os.path.join("icons", "_icon_hashes.plist")
icon_hashes = plistlib.writePlistToString(icons)
try:
repo.put(icon_hashes_plist, icon_hashes)
print "Created %s..." % (icon_hashes_plist)
except munkirepo.RepoError, err:
print_err_utf8(
u'Failed to create %s: %s' % (icon_hashes_plist, unicode(err)))
# Exit with "exit_code" if we got this far. This will be -1 if there were
# any errors that prevented the catalogs to be written.
exit(exit_code)
def main():
'''Main'''
usage = "usage: %prog [options] [/path/to/repo_root]"
parser = optparse.OptionParser(usage=usage)
parser.add_option('--version', '-V', action='store_true',
help='Print the version of the munki tools and exit.')
parser.add_option('--force', '-f', action='store_true', dest='force',
help='Disable sanity checks.')
parser.add_option('--skip-pkg-check', '-s', action='store_true', dest='skip_payload_check',
help='Skip checking of pkg existence. Useful'
'when pkgs aren\'t on the same server'
'as pkginfo, catalogs and manifests.')
parser.add_option('--repo_url', '--repo-url',
help='Optional repo URL that takes precedence '
'over the default repo_url specified via '
'--configure.')
parser.add_option('--plugin',
help='Specify a custom plugin to connect to repo.')
parser.set_defaults(force=False, skip_payload_check=False)
options, arguments = parser.parse_args()
if options.version:
print get_version()
exit(0)
# backwards compatibility
if not options.repo_url and not options.plugin:
if arguments:
options.repo_url = path2url(arguments[0])
elif pref('repo_url'):
options.repo_url = pref('repo_url')
options.plugin = pref('plugin')
elif pref('repo_path'):
options.repo_url = path2url(pref('repo_path'))
if not options.repo_url:
parser.print_usage()
exit(-1)
# Connect to the repo
try:
repo = munkirepo.connect(options.repo_url, options.plugin)
except munkirepo.RepoError, err:
print >> sys.stderr, (u'Could not connect to munki repo: %s'
% unicode(err))
exit(-1)
# Make the catalogs
makecatalogs(repo, options)
if __name__ == '__main__':
main()