Files
munki/code/client/manifestutil
Greg Neagle b9f9fffccc Revert "munki: rename "/usr/local/munki/python" symlink to "munki-python" (#997)"
This change is still a good future goal, but is causing problems that are too difficult to work around right now and is delaying the vital release of Munki 5.1 for Big Sur compatibility.

This reverts commit 3bb91cabca.
2020-09-15 09:04:47 -07:00

1150 lines
40 KiB
Python
Executable File

#!/usr/local/munki/python
# encoding: utf-8
#
# Copyright 2011-2020 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.
"""
manifestutil
Created by Greg Neagle on 2011-03-04.
"""
from __future__ import absolute_import, print_function
# TODO: add support for delete-manifest
import fnmatch
import optparse
import os
import readline
import shlex
import sys
# our libs
from munkilib.cliutils import ConfigurationSaveError
from munkilib.cliutils import configure as _configure
from munkilib.cliutils import libedit
from munkilib.cliutils import get_version, pref, path2url
from munkilib.wrappers import (get_input,
readPlistFromString, writePlistToString,
PlistReadError, PlistWriteError)
from munkilib import munkirepo
def get_installer_item_names(repo, catalog_limit_list):
'''Returns a list of unique installer item (pkg) names
from the given list of catalogs'''
item_list = []
try:
catalogs_list = repo.itemlist('catalogs')
except munkirepo.RepoError as err:
print((
u'Could not retrieve catalogs: %s' % err), file=sys.stderr)
return []
for catalog_name in catalogs_list:
if catalog_name in catalog_limit_list:
try:
data = repo.get(os.path.join('catalogs', catalog_name))
catalog = readPlistFromString(data)
except munkirepo.RepoError as err:
print((
u'Could not retrieve catalog %s: %s'
% (catalog_name, err)), file=sys.stderr)
except (IOError, OSError, PlistReadError):
# skip items that aren't valid plists
# or that we can't read
pass
else:
item_list.extend([item['name']
for item in catalog
if not item.get('update_for')])
item_list = list(set(item_list))
item_list.sort()
return item_list
def get_manifest_names(repo):
'''Returns a list of available manifests'''
try:
manifest_names = repo.itemlist('manifests')
except munkirepo.RepoError as err:
print((
u'Could not retrieve manifests: %s' % err), file=sys.stderr)
manifest_names = []
manifest_names.sort()
return manifest_names
def get_catalogs(repo):
'''Returns a list of available catalogs'''
try:
catalog_names = repo.itemlist('catalogs')
except munkirepo.RepoError as err:
print((
u'Could not retrieve catalogs: %s' % err), file=sys.stderr)
catalog_names = []
catalog_names.sort()
return catalog_names
def get_manifest_pkg_sections():
'''Returns a list of manifest sections that can contain pkg names'''
return ['managed_installs',
'managed_uninstalls',
'managed_updates',
'optional_installs']
def printplistitem(label, value, indent=0):
"""Prints a plist item in an 'attractive' way"""
indentspace = ' '
if value is None:
print(indentspace*indent, '%s: !NONE!' % label)
elif isinstance(value, list) or type(value).__name__ == 'NSCFArray':
if label:
print(indentspace*indent, '%s:' % label)
for item in value:
printplistitem('', item, indent+1)
elif isinstance(value, dict) or type(value).__name__ == 'NSCFDictionary':
if label:
print(indentspace*indent, '%s:' % label)
keys = list(value.keys())
keys.sort()
for subkey in keys:
printplistitem(subkey, value[subkey], indent+1)
else:
if label:
print(indentspace*indent, '%s: %s' % (label, value))
else:
print(indentspace*indent, '%s' % value)
def printplist(plistdict):
"""Prints plist dictionary in a pretty(?) way"""
keys = list(plistdict.keys())
keys.sort()
for key in keys:
printplistitem(key, plistdict[key])
def get_manifest(repo, manifest_name):
'''Gets the contents of a manifest'''
manifest_ref = os.path.join('manifests', manifest_name)
try:
data = repo.get(manifest_ref)
return readPlistFromString(data)
except munkirepo.RepoError as err:
print(u'Could not retrieve manifest %s: %s'
% (manifest_name, err), file=sys.stderr)
return None
except (IOError, OSError, PlistReadError) as err:
print(u'Could not read manifest %s: %s' % (manifest_name, err),
file=sys.stderr)
return None
def save_manifest(repo, manifest_dict, manifest_name, overwrite_existing=False):
'''Saves a manifest to disk'''
existing_manifest_names = get_manifest_names(repo)
if not overwrite_existing:
if manifest_name in existing_manifest_names:
print('%s already exists!' % manifest_name, file=sys.stderr)
return False
manifest_ref = os.path.join('manifests', manifest_name)
try:
data = writePlistToString(manifest_dict)
repo.put(manifest_ref, data)
return True
except (IOError, OSError, PlistWriteError, munkirepo.RepoError) as err:
print(u'Saving %s failed: %s' % (manifest_name, err),
file=sys.stderr)
return False
def manifest_rename(repo, source_manifest_name, dest_manifest_name,
overwrite_existing=False):
'''Renames an existing manifest. Since the repo can be an API-based repo,
we really have to save a copy under the new name and then delete the old
one.'''
source_manifest_ref = os.path.join('manifests', source_manifest_name)
dest_manifest_ref = os.path.join('manifests', dest_manifest_name)
existing_manifest_names = get_manifest_names(repo)
if not overwrite_existing:
if dest_manifest_name in existing_manifest_names:
print(u'%s already exists!' % dest_manifest_name, file=sys.stderr)
return False
try:
source_data = repo.get(source_manifest_ref)
repo.put(dest_manifest_ref, source_data)
repo.delete(source_manifest_ref)
return True
except munkirepo.RepoError as err:
print(u'Renaming %s to %s failed: %s'
% (source_manifest_name, dest_manifest_name, err),
file=sys.stderr)
return False
def cleanup_and_exit(exitcode):
"""Give the user the chance to unmount the repo when we exit"""
result = 0
# TODO: handle mounted repo
#if repo and repo.mounted and repo.WE_MOUNTED_THE_REPO:
# answer = raw_input('Unmount the repo fileshare? [y/n] ')
# if answer.lower().startswith('y'):
# result = repo.unmount()
exit(exitcode or result)
def update_cached_manifest_list(repo):
'''Updates our cached list of available manifests so our completer
will return all the available manifests.'''
CMD_ARG_DICT['manifests'] = get_manifest_names(repo)
##### subcommand functions #####
class MyOptParseError(Exception):
'''Exception for our custom option parser'''
pass
class MyOptParseExit(Exception):
'''Raised when optparse calls self.exit() so we can handle it instead
of exiting'''
pass
class MyOptionParser(optparse.OptionParser):
'''Custom option parser that overrides the error handler and
the exit handler so printing help doesn't exit the interactive
pseudo-shell'''
def error(self, msg):
"""error(msg : string)
"""
self.print_usage(sys.stderr)
raise MyOptParseError('option error: %s' % msg)
def exit(self, status=0, msg=None):
if msg:
sys.stderr.write(msg)
raise MyOptParseExit
def version(repo, args):
'''Prints version number'''
# we ignore repo arg but subcommand dispatcher sends it to all
# subcommands
if len(args) != 0:
print('Usage: version', file=sys.stderr)
return 22 # Invalid argument
print(get_version())
return 0
def configure(repo, args):
'''Allow user to configure tool options'''
# we ignore the repo arg, but all the other subcommands require it
if len(args):
print('Usage: configure', file=sys.stderr)
return 22 # Invalid argument
prompt_list = [
('repo_url', 'Repo URL (example: afp://munki.example.com/repo)'),
('plugin', 'Munki repo plugin (defaults to FileRepo)')
]
try:
_configure(prompt_list)
return 0
except ConfigurationSaveError:
return 1 # Operation not permitted
def list_catalogs(repo, args):
'''Prints the names of the available catalogs'''
parser = MyOptionParser()
parser.set_usage('''list-catalogs
Prints the names of the available catalogs''')
try:
_, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if len(arguments) != 0:
parser.print_usage(sys.stderr)
return 22 # Invalid argument
for item in get_catalogs(repo):
print(item)
return 0
def list_catalog_items(repo, args):
'''Lists items in the given catalogs'''
parser = MyOptionParser()
parser.set_usage('''list-catalog-items CATALOG_NAME [CATALOG_NAME ...]
Lists items in the given catalogs''')
try:
_, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if len(arguments) == 0:
parser.print_usage(sys.stderr)
return 1 # Operation not permitted
available_catalogs = get_catalogs(repo)
for catalog in arguments:
if catalog not in available_catalogs:
print('%s: no such catalog!' % catalog, file=sys.stderr)
return 2 # No such file or directory
for pkg in get_installer_item_names(repo, arguments):
print(pkg)
return 0
def list_manifests(repo, args):
'''Prints names of available manifests, filtering on arg[0]
similar to Unix file globbing'''
parser = MyOptionParser()
parser.set_usage('''list-manifests [FILE_NAME_MATCH_STRING]
Prints names of available manifests.''')
try:
_, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if len(arguments) == 1:
list_filter = arguments[0]
elif len(arguments) == 0:
list_filter = '*'
else:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
for item in get_manifest_names(repo):
if fnmatch.fnmatch(item, list_filter):
print(item)
return 0
def find(repo, args):
'''Find text in manifests, optionally searching just a specific manifest
section specified by keyname'''
parser = MyOptionParser()
parser.set_usage('''find FIND_TEXT [--section SECTION_NAME]
Find text in manifests, optionally searching a specific manifest
section''')
parser.add_option('--section',
metavar='SECTION_NAME',
help=('(Optional) Section of the manifest to search for '
'FIND_TEXT'))
try:
options, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if not options.section and len(arguments) == 2:
options.section = arguments[1]
del arguments[1]
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
findtext = arguments[0]
keyname = options.section
count = 0
for name in get_manifest_names(repo):
manifest_ref = os.path.join('manifests', name)
try:
data = repo.get(manifest_ref)
manifest = readPlistFromString(data)
except (IOError, OSError, PlistReadError, munkirepo.RepoError) as err:
print(u'Error reading %s: %s' % (manifest_ref, err),
file=sys.stderr)
continue
if keyname:
if keyname in manifest:
value = manifest[keyname]
if type(value) == list or type(value).__name__ == 'NSCFArray':
for item in value:
try:
if findtext.upper() in item.upper():
print('%s: %s' % (name, item))
count += 1
except AttributeError:
pass
elif findtext.upper() in value.upper():
print('%s: %s' % (name, value))
count += 1
else:
for key in manifest.keys():
value = manifest[key]
if type(value) == list or type(value).__name__ == 'NSCFArray':
for item in value:
try:
if findtext.upper() in item.upper():
print('%s (%s): %s' % (name, key, item))
count += 1
except AttributeError:
pass
elif findtext.upper() in value.upper():
print('%s (%s): %s' % (name, key, value))
count += 1
print('%s matches found.' % count)
return 0
def display_manifest(repo, args):
'''Prints contents of a given manifest'''
parser = MyOptionParser()
parser.set_usage('''display-manifest MANIFESTNAME
Prints the contents of the specified manifest''')
try:
_, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
manifestname = arguments[0]
manifest = get_manifest(repo, manifestname)
if manifest:
printplist(manifest)
return 0
else:
return 2 # No such file or directory
def expand_included_manifests(repo, args):
'''Prints a manifest, expanding any included manifests.'''
def manifest_recurser(repo, manifest):
'''Recursive expansion of included manifests'''
# No infinite loop checking! Be wary!
if 'included_manifests' in manifest:
for (index, item) in enumerate(manifest['included_manifests']):
included_manifest = get_manifest(repo, item)
if included_manifest:
included_manifest = manifest_recurser(repo, included_manifest)
manifest['included_manifests'][index] = {
item: included_manifest
}
return manifest
parser = MyOptionParser()
parser.set_usage('''expand-included-manifest MANIFESTNAME
Prints included manifests in the specified manifest''')
try:
_, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
manifestname = arguments[0]
manifest = get_manifest(repo, manifestname)
if manifest:
printplist(manifest_recurser(repo, manifest))
else:
return 2 # No such file or directory
def new_manifest(repo, args):
'''Creates a new, empty manifest'''
parser = MyOptionParser()
parser.set_usage('''new-manifest MANIFESTNAME
Creates a new empty manifest''')
try:
_, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
manifest_name = arguments[0]
manifest = {'catalogs': [],
'included_manifests': [],
'managed_installs': [],
'managed_uninstalls': []}
if save_manifest(repo, manifest, manifest_name):
update_cached_manifest_list(repo)
return 0
else:
return 1 # Operation not permitted
def copy_manifest(repo, args):
'''Copies one manifest to another'''
parser = MyOptionParser()
parser.set_usage(
'''copy-manifest SOURCE_MANIFEST DESTINATION_MANIFEST
Copies the contents of one manifest to another''')
try:
_, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if len(arguments) != 2:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
source_manifest = arguments[0]
dest_manifest = arguments[1]
manifest = get_manifest(repo, source_manifest)
if manifest and save_manifest(repo, manifest, dest_manifest):
update_cached_manifest_list(repo)
return 0
else:
return 1 # Operation not permitted
def rename_manifest(repo, args):
'''Renames a manifest'''
parser = MyOptionParser()
parser.set_usage(
'''rename-manifest SOURCE_MANIFEST DESTINATION_MANIFEST
Renames the manifest''')
try:
_, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if len(arguments) != 2:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
source_manifest = arguments[0]
dest_manifest = arguments[1]
if manifest_rename(repo, source_manifest, dest_manifest):
print ('Renamed manifest %s to %s.'
% (source_manifest, dest_manifest))
update_cached_manifest_list(repo)
return 0
else:
return 1 # Operation not permitted
def add_pkg(repo, args):
'''Adds a package to a manifest.'''
parser = MyOptionParser()
parser.set_usage(
'''add-pkg PKGNAME --manifest MANIFESTNAME [--section SECTIONNAME]
Adds a package to a manifest. Package is added to managed_installs
unless a different manifest section is specified with the
--section option''')
parser.add_option('--manifest',
metavar='MANIFESTNAME',
help='name of manifest on which to operate')
parser.add_option('--section', default='managed_installs',
metavar='SECTIONNAME',
help=('manifest section to which to add the package. '
'Defaults to managed_installs.'))
try:
options, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if not options.manifest:
if len(arguments) == 2:
options.manifest = arguments[1]
del arguments[1]
else:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
pkgname = arguments[0]
manifest = get_manifest(repo, options.manifest)
if not manifest:
return 2 # No such file or directory
for section in get_manifest_pkg_sections():
if pkgname in manifest.get(section, []):
print((
'Package %s is already in section %s of manifest %s.'
% (pkgname, section, options.manifest)), file=sys.stderr)
return 1 # Operation not permitted
manifest_catalogs = manifest.get('catalogs', [])
available_pkgnames = get_installer_item_names(repo, manifest_catalogs)
if pkgname not in available_pkgnames:
print((
'WARNING: Package %s is not available in catalogs %s '
'of manifest %s.'
% (pkgname, manifest_catalogs, options.manifest)), file=sys.stderr)
if not options.section in manifest:
manifest[options.section] = [pkgname]
else:
manifest[options.section].append(pkgname)
if save_manifest(repo, manifest, options.manifest, overwrite_existing=True):
print ('Added %s to section %s of manifest %s.'
% (pkgname, options.section, options.manifest))
return 0
else:
return 1 # Operation not permitted
def move_install_to_uninstall(repo, args):
'''Moves a package from managed_installs to managed_uninstalls in a manifest.'''
parser = MyOptionParser()
parser.set_usage(
'''move-install-to-uninstall PKGNAME --manifest MANIFESTNAME
Moves a package from managed_installs to managed_uninstalls in a manifest.''')
parser.add_option('--manifest',
metavar='MANIFESTNAME',
help='''name of manifest on which to operate''')
try:
options, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if not options.manifest:
if len(arguments) == 2:
options.manifest = arguments[1]
del arguments[1]
else:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
pkgname = arguments[0]
manifest = get_manifest(repo, options.manifest)
if not manifest:
return 2 # No such file or directory
else:
if pkgname in manifest['managed_installs']:
manifest['managed_installs'].remove(pkgname)
print ('Removed %s from section %s of manifest %s'
% (pkgname, 'managed_installs', options.manifest))
else:
print('WARNING: Package %s is not in section %s '
'of manifest %s. No changes made.'
% (pkgname, 'managed_installs', options.manifest),
file=sys.stderr)
return 1 # Operation not permitted
if pkgname in manifest['managed_uninstalls']:
print ('%s is already in section managed_uninstalls of manifest %s.'
% (pkgname, options.manifest))
else:
manifest['managed_uninstalls'].append(pkgname)
print ('Added %s to section managed_uninstalls of manifest %s.'
% (pkgname, options.manifest))
if save_manifest(repo, manifest, options.manifest,
overwrite_existing=True):
return 0
else:
return 1 # Operation not permitted
def remove_pkg(repo, args):
'''Removes a package from a manifest.'''
parser = MyOptionParser()
parser.set_usage(
'''remove-pkg PKGNAME --manifest MANIFESTNAME [--section SECTIONNAME]
Removes a package from a manifest. Package is removed from
managed_installs unless a different manifest section is specified with
the --section option''')
parser.add_option('--manifest',
metavar='MANIFESTNAME',
help='''name of manifest on which to operate''')
parser.add_option('--section', default='managed_installs',
metavar='SECTIONNAME',
help=('manifest section from which to remove the '
'package. Defaults to managed_installs.'))
try:
options, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if not options.manifest:
if len(arguments) == 2:
options.manifest = arguments[1]
del arguments[1]
else:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
pkgname = arguments[0]
manifest = get_manifest(repo, options.manifest)
if not manifest:
return 2 # No such file or directory
if not options.section in manifest:
print('Section %s is not in manifest %s.'
% (options.section, options.manifest), file=sys.stderr)
return 1 # Operation not permitted
if pkgname not in manifest[options.section]:
print('Package %s is not in section %s of manifest %s.'
% (pkgname, options.section, options.manifest), file=sys.stderr)
return 1 # Operation not permitted
else:
manifest[options.section].remove(pkgname)
if save_manifest(repo, manifest, options.manifest,
overwrite_existing=True):
print ('Removed %s from section %s of manifest %s.'
% (pkgname, options.section, options.manifest))
return 0
else:
return 1 # Operation not permitted
def add_catalog(repo, args):
'''Adds a catalog to a manifest.'''
parser = MyOptionParser()
parser.set_usage('''add-catalog CATALOGNAME --manifest MANIFESTNAME
Adds a catalog to a manifest''')
parser.add_option('--manifest',
metavar='MANIFESTNAME',
help='name of manifest on which to operate')
try:
options, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if not options.manifest:
if len(arguments) == 2:
options.manifest = arguments[1]
del arguments[1]
else:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
catalogname = arguments[0]
available_catalogs = get_catalogs(repo)
if catalogname not in available_catalogs:
print('Unknown catalog name: %s.' % catalogname, file=sys.stderr)
return 2 # no such file or directory
manifest = get_manifest(repo, options.manifest)
if not manifest:
return 2 # no such file or directory
if not 'catalogs' in manifest:
manifest['catalogs'] = []
if catalogname in manifest['catalogs']:
print((
'Catalog %s is already in manifest %s.'
% (catalogname, options.manifest)), file=sys.stderr)
return 1 # Operation not permitted
else:
# put it at the front of the catalog list as that is usually
# what is wanted...
manifest['catalogs'].insert(0, catalogname)
if save_manifest(repo, manifest, options.manifest,
overwrite_existing=True):
print ('Added %s to catalogs of manifest %s.'
% (catalogname, options.manifest))
return 0
else:
return 1 # Operation not permitted
def remove_catalog(repo, args):
'''Removes a catalog from a manifest.'''
parser = MyOptionParser()
parser.set_usage('''remove-catalog CATALOGNAME --manifest MANIFESTNAME
Removes a catalog from a manifest''')
parser.add_option('--manifest',
metavar='MANIFESTNAME',
help='name of manifest on which to operate')
try:
options, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if not options.manifest:
if len(arguments) == 2:
options.manifest = arguments[1]
del arguments[1]
else:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
catalogname = arguments[0]
manifest = get_manifest(repo, options.manifest)
if not manifest:
return 2 # no such file or directory
if catalogname not in manifest.get('catalogs', []):
print((
'Catalog %s is not in manifest %s.'
% (catalogname, options.manifest)), file=sys.stderr)
return 1 # Operation not permitted
else:
manifest['catalogs'].remove(catalogname)
if save_manifest(repo, manifest, options.manifest,
overwrite_existing=True):
print ('Removed %s from catalogs of manifest %s.'
% (catalogname, options.manifest))
return 0
else:
return 1 # Operation not permitted
def add_included_manifest(repo, args):
'''Adds an included manifest to a manifest.'''
parser = MyOptionParser()
parser.set_usage(
'''add-included-manifest MANIFEST_TO_INCLUDE --manifest TARGET_MANIFEST
Adds a manifest to the included_manifests of the TARGET_MANIFEST''')
parser.add_option('--manifest',
metavar='TARGET_MANIFEST',
help='name of manifest on which to operate')
try:
options, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if not options.manifest:
if len(arguments) == 2:
options.manifest = arguments[1]
del arguments[1]
else:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
manifest_to_include = arguments[0]
available_manifests = get_manifest_names(repo)
if manifest_to_include not in available_manifests:
print('Unknown manifest name: %s.' % manifest_to_include,
file=sys.stderr)
return 2 # no such file or directory
if manifest_to_include == options.manifest:
print('Can\'t include %s in itself!.' % manifest_to_include,
file=sys.stderr)
return 1 # Operation not permitted
manifest = get_manifest(repo, options.manifest)
if not manifest:
return 2 # no such file or directory
if not 'included_manifests' in manifest:
manifest['included_manifests'] = []
if manifest_to_include in manifest['included_manifests']:
print('Manifest %s is already included in manifest %s.'
% (manifest_to_include, options.manifest), file=sys.stderr)
return 1 # Operation not permitted
else:
manifest['included_manifests'].append(manifest_to_include)
if save_manifest(repo, manifest, options.manifest,
overwrite_existing=True):
print ('Added %s to included_manifests of manifest %s.'
% (manifest_to_include, options.manifest))
return 0
else:
return 1 # Operation not permitted
def remove_included_manifest(repo, args):
'''Removes an included manifest from a manifest.'''
parser = MyOptionParser()
parser.set_usage(
'''remove-included_manifest INCLUDED_MANIFEST --manifest TARGET_MANIFEST
Removes a manifest from the included_manifests of TARGET_MANIFEST''')
parser.add_option('--manifest',
metavar='TARGET_MANIFEST',
help='name of manifest on which to operate')
try:
options, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if not options.manifest:
if len(arguments) == 2:
options.manifest = arguments[1]
del arguments[1]
else:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
if len(arguments) != 1:
parser.print_usage(sys.stderr)
return 7 # Argument list too long
included_manifest = arguments[0]
manifest = get_manifest(repo, options.manifest)
if not manifest:
return 2 # no such file or directory
if included_manifest not in manifest.get('included_manifests', []):
print('Manifest %s is not included in manifest %s.'
% (included_manifest, options.manifest), file=sys.stderr)
return 1 # Operation not permitted
else:
manifest['included_manifests'].remove(included_manifest)
if save_manifest(repo, manifest, options.manifest,
overwrite_existing=True):
print('Removed %s from included_manifests of manifest %s.'
% (included_manifest, options.manifest))
return 0
else:
return 1 # Operation not permitted
def refresh_cache(repo, args):
'''Refreshes the repo data if changes were made while manifestutil was
running. Updates manifests, catalogs, and packages.'''
parser = MyOptionParser()
parser.set_usage('''refresh-cache
Refreshes the repo data''')
try:
_, arguments = parser.parse_args(args)
except MyOptParseError as errmsg:
print(str(errmsg), file=sys.stderr)
return 22 # Invalid argument
except MyOptParseExit:
return 0
if len(arguments) != 0:
parser.print_usage(sys.stderr)
return 22 # Invalid argument
CMD_ARG_DICT['manifests'] = get_manifest_names(repo)
CMD_ARG_DICT['catalogs'] = get_catalogs(repo)
CMD_ARG_DICT['pkgs'] = get_installer_item_names(
repo, CMD_ARG_DICT['catalogs'])
##### end subcommand functions
def show_help():
'''Prints available subcommands'''
print("Available sub-commands:")
subcommands = list(CMD_ARG_DICT['cmds'].keys())
subcommands.sort()
for item in subcommands:
print('\t%s' % item)
return 0
def tab_completer(text, state):
'''Called by the readline lib to calculate possible completions'''
array_to_match = None
if readline.get_begidx() == 0:
# since we are at the start of the line
# we are matching commands
array_to_match = 'cmds'
match_list = list(CMD_ARG_DICT.get('cmds', {}).keys())
else:
# we are matching args
cmd_line = readline.get_line_buffer()[0:readline.get_begidx()]
cmd = shlex.split(cmd_line)[-1]
array_to_match = CMD_ARG_DICT.get('cmds', {}).get(cmd)
if array_to_match:
match_list = CMD_ARG_DICT[array_to_match]
else:
array_to_match = CMD_ARG_DICT.get('options', {}).get(cmd)
if array_to_match:
match_list = CMD_ARG_DICT[array_to_match]
else:
array_to_match = 'options'
match_list = list(CMD_ARG_DICT.get('options', {}).keys())
matches = [item for item in match_list
if item.upper().startswith(text.upper())]
try:
return matches[state]
except IndexError:
return None
def set_up_tab_completer():
'''Starts our tab-completer when running interactively'''
readline.set_completer(tab_completer)
if libedit:
# readline module was compiled against libedit
readline.parse_and_bind("bind ^I rl_complete")
else:
readline.parse_and_bind("tab: complete")
def handle_subcommand(repo, args):
'''Does all our subcommands'''
# check if any arguments are passed.
# if not, list subcommands.
if len(args) < 1:
show_help()
return 2
# strip leading hyphens and
# replace embedded hyphens with underscores
# so '--add-pkg' becomes 'add_pkg'
# and 'new-manifest' becomes 'new_manifest'
subcommand = args[0].lstrip('-').replace('-', '_')
# special case the exit command
if subcommand == 'exit':
cleanup_and_exit(0)
if (subcommand not in ['version', 'configure', 'help']
and '-h' not in args and '--help' not in args):
if not repo:
repo = connect_to_repo()
try:
# find function to call by looking in the global name table
# for a function with a name matching the subcommand
subcommand_function = globals()[subcommand]
except MyOptParseExit:
return 0
except (TypeError, KeyError):
print('Unknown subcommand: %s' % args[0], file=sys.stderr)
show_help()
return 2
else:
return subcommand_function(repo, args[1:])
def connect_to_repo():
'''Connects to the Munki repo'''
repo_url = pref('repo_url')
repo_path = pref('repo_path')
repo_plugin = pref('plugin')
if not repo_url and repo_path:
repo_url = path2url(repo_path)
if not repo_url:
print((
u'No repo URL defined. Run %s --configure to define one.'
% os.path.basename(__file__)), file=sys.stderr)
exit(-1)
try:
repo = munkirepo.connect(repo_url, repo_plugin)
except munkirepo.RepoError as err:
print(u'Repo error: %s' % err, file=sys.stderr)
exit(-1)
return repo
CMD_ARG_DICT = {}
def main():
'''Our main routine'''
repo = None
cmds = {'add-pkg': 'pkgs',
'add-catalog': 'catalogs',
'add-included-manifest': 'manifests',
'remove-pkg': 'pkgs',
'move-install-to-uninstall': 'pkgs',
'remove-catalog': 'catalogs',
'remove-included-manifest': 'manifests',
'list-manifests': 'manifests',
'list-catalogs': 'default',
'list-catalog-items': 'catalogs',
'display-manifest': 'manifests',
'expand-included-manifests': 'manifests',
'find': 'default',
'new-manifest': 'default',
'copy-manifest': 'manifests',
'rename-manifest': 'manifests',
'refresh-cache': 'default',
'exit': 'default',
'help': 'default',
'configure': 'default',
'version': 'default'
}
CMD_ARG_DICT['cmds'] = cmds
if len(sys.argv) > 1:
# some commands or options were passed at the command line
cmd = sys.argv[1].lstrip('-')
retcode = handle_subcommand(repo, sys.argv[1:])
cleanup_and_exit(retcode)
else:
# if we get here, no options or commands,
# so let's enter interactive mode
# must have an available repo for interactive mode
repo = connect_to_repo()
# build the rest of our dict to enable tab completion
CMD_ARG_DICT['options'] = {'--manifest': 'manifests',
'--section': 'sections'}
CMD_ARG_DICT['default'] = []
CMD_ARG_DICT['sections'] = get_manifest_pkg_sections()
CMD_ARG_DICT['manifests'] = get_manifest_names(repo)
CMD_ARG_DICT['catalogs'] = get_catalogs(repo)
CMD_ARG_DICT['pkgs'] = get_installer_item_names(
repo, CMD_ARG_DICT['catalogs'])
set_up_tab_completer()
print('Entering interactive mode... (type "help" for commands)')
while 1:
try:
cmd = get_input('> ')
except (KeyboardInterrupt, EOFError):
# React to Control-C and Control-D
print() # so we finish off the raw_input line
cleanup_and_exit(0)
args = shlex.split(cmd)
handle_subcommand(repo, args)
if __name__ == '__main__':
main()