mirror of
https://github.com/munki/munki.git
synced 2026-01-23 23:49:22 -06:00
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.
1150 lines
40 KiB
Python
Executable File
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()
|