pkgrepo.py revision 2028
5a580c3a38ced62d4bcc95b8ac7c4f2935b5d294Timo Sirainen#!/usr/bin/python2.6
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch#
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# CDDL HEADER START
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch#
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# The contents of this file are subject to the terms of the
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# Common Development and Distribution License (the "License").
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# You may not use this file except in compliance with the License.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch#
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# or http://www.opensolaris.org/os/licensing.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# See the License for the specific language governing permissions
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# and limitations under the License.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch#
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# When distributing Covered Code, include this CDDL HEADER in each
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# If applicable, add the following below this CDDL HEADER, with the
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# fields enclosed by brackets "[]" replaced with your own identifying
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# information: Portions Copyright [yyyy] [name of copyright owner]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch#
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# CDDL HEADER END
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch#
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch#
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# Copyright (c) 2010 Oracle and/or its affiliates. All rights reserved.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch#
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan BoschPKG_CLIENT_NAME = "pkgrepo"
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# pkgrepo exit codes
7384b4e78eaab44693c985192276e31322155e32Stephan BoschEXIT_OK = 0
7384b4e78eaab44693c985192276e31322155e32Stephan BoschEXIT_OOPS = 1
7384b4e78eaab44693c985192276e31322155e32Stephan BoschEXIT_BADOPT = 2
7384b4e78eaab44693c985192276e31322155e32Stephan BoschEXIT_PARTIAL = 3
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# listing constants
7384b4e78eaab44693c985192276e31322155e32Stephan BoschLISTING_FORMATS = ("tsv", )
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch# globals
7384b4e78eaab44693c985192276e31322155e32Stephan Boschtmpdirs = []
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport atexit
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport copy
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport errno
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport getopt
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport gettext
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport locale
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport logging
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport os
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport pipes
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport shlex
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport shutil
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport sys
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport tempfile
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport traceback
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport warnings
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Boschfrom pkg.client import global_settings
7384b4e78eaab44693c985192276e31322155e32Stephan Boschfrom pkg.misc import msg, PipeError
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport pkg
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport pkg.catalog
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport pkg.client.api_errors as apx
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport pkg.client.publisher as publisher
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport pkg.client.progress as progress
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport pkg.client.transport.transport as transport
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport pkg.misc as misc
7384b4e78eaab44693c985192276e31322155e32Stephan Boschimport pkg.server.repository as sr
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Boschlogger = global_settings.logger
7384b4e78eaab44693c985192276e31322155e32Stephan Boschorig_cwd = None
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch@atexit.register
7384b4e78eaab44693c985192276e31322155e32Stephan Boschdef cleanup():
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch """To be called at program finish."""
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch for d in tmpdirs:
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch shutil.rmtree(d, True)
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Boschdef error(text, cmd=None):
eb325a5a90c1d2655e74972bde0de6a699d2c864Stephan Bosch """Emit an error message prefixed by the command name """
eb325a5a90c1d2655e74972bde0de6a699d2c864Stephan Bosch
eb325a5a90c1d2655e74972bde0de6a699d2c864Stephan Bosch if cmd:
eb325a5a90c1d2655e74972bde0de6a699d2c864Stephan Bosch text = "%s: %s" % (cmd, text)
eb325a5a90c1d2655e74972bde0de6a699d2c864Stephan Bosch pkg_cmd = "pkgrepo "
eb325a5a90c1d2655e74972bde0de6a699d2c864Stephan Bosch else:
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkg_cmd = "pkgrepo: "
56d1345c43bbd28c36b7faa85e4163bd9e874290Timo Sirainen
30d917bcd48d70af0371baf27571cc198d621a62Timo Sirainen # If we get passed something like an Exception, we can convert
9d0aee99a8c80d71137aa9b8c216cc203bec7a9aTimo Sirainen # it down to a string.
9d0aee99a8c80d71137aa9b8c216cc203bec7a9aTimo Sirainen text = str(text)
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch # If the message starts with whitespace, assume that it should come
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch # *before* the command-name prefix.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch text_nows = text.lstrip()
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch ws = text[:len(text) - len(text_nows)]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch # This has to be a constant value as we can't reliably get our actual
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch # program name on all platforms.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch logger.error(ws + pkg_cmd + text_nows)
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Boschdef usage(usage_error=None, cmd=None, retcode=2, full=False):
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch """Emit a usage message and optionally prefix it with a more
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch specific error message. Causes program to exit.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch """
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch if usage_error:
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch error(usage_error, cmd=cmd)
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch if not full:
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch # The full usage message isn't desired.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch logger.error(_("Try `pkgrepo --help or -?' for more "
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch "information."))
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch sys.exit(retcode)
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
9dc01e0d10a61cab14867b26bf0d2d1dcf8ad978Timo Sirainen msg(_("""\
9dc01e0d10a61cab14867b26bf0d2d1dcf8ad978Timo SirainenUsage:
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo [options] command [cmd_options] [operands]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan BoschSubcommands:
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo create [--version] uri_or_path
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
aacf2a69acc59e9382578d6f4e030788abc79706Timo Sirainen pkgrepo add-signing-ca-cert [-p publisher ...]
aacf2a69acc59e9382578d6f4e030788abc79706Timo Sirainen [-s repo_uri_or_path] path ...
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo add-signing-intermediate-cert [-p publisher ...]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch [-s repo_uri_or_path] path ...
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo get [-p publisher ...] [-s repo_uri_or_path]
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen [section/property ...]
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen pkgrepo info [-F format] [-H] [-p publisher ...]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch [-s repo_uri_or_path]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo rebuild [-s repo_uri_or_path] [--no-catalog]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch [--no-index]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo refresh [-s repo_uri_or_path] [--no-catalog]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch [--no-index]
636d0f43138468f8efe685a681326b123f660e49Timo Sirainen
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo remove-signing-ca-cert [-p publisher ...]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch [-s repo_uri_or_path] hash ...
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo remove-signing-intermediate-cert [-p publisher ...]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch [-s repo_uri_or_path] hash ...
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo set [-p publisher ...] [-s repo_uri_or_path]
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch section/property[+|-]=[value] ... or
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch section/property[+|-]=([value]) ...
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo help
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pkgrepo version
636d0f43138468f8efe685a681326b123f660e49Timo Sirainen
636d0f43138468f8efe685a681326b123f660e49Timo SirainenOptions:
636d0f43138468f8efe685a681326b123f660e49Timo Sirainen --help or -?
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch Displays a usage message."""))
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch sys.exit(retcode)
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Boschclass OptionError(Exception):
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch """Option exception. """
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch def __init__(self, *args):
fb1be3de0159d6a10e916ad992e2bc53be64c6d5Timo Sirainen Exception.__init__(self, *args)
fb1be3de0159d6a10e916ad992e2bc53be64c6d5Timo Sirainen
fb1be3de0159d6a10e916ad992e2bc53be64c6d5Timo Sirainen
7384b4e78eaab44693c985192276e31322155e32Stephan Boschdef parse_uri(uri):
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch """Parse the repository location provided and attempt to transform it
1a9a35a6b307f8d5b25345af55e40a99162b4072Timo Sirainen into a valid repository URI.
1a9a35a6b307f8d5b25345af55e40a99162b4072Timo Sirainen """
1a9a35a6b307f8d5b25345af55e40a99162b4072Timo Sirainen
1a9a35a6b307f8d5b25345af55e40a99162b4072Timo Sirainen return publisher.RepositoryURI(misc.parse_uri(uri))
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Boschdef _add_certs(conf, subcommand, args, ca):
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch opts, pargs = getopt.getopt(args, "p:s:")
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pubs = set()
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch for opt, arg in opts:
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch if opt == "-p":
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch pubs.add(arg)
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch elif opt == "-s":
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch conf["repo_uri"] = parse_uri(arg)
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch # Get repository object.
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch if not conf.get("repo_uri", None):
7384b4e78eaab44693c985192276e31322155e32Stephan Bosch usage(_("A package repository location must be provided "
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen "using -s."), cmd=subcommand)
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen repo = get_repo(conf, read_only=False, subcommand=subcommand)
ba1c847d0af4afe4787ed470d0c818e948e184e2Timo Sirainen
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen if len(pargs) < 1:
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen usage(_("At least one path to a certificate must be provided."))
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen failed = []
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen def add_certs(pfx=None):
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen if orig_cwd:
56d1345c43bbd28c36b7faa85e4163bd9e874290Timo Sirainen certs = [os.path.join(orig_cwd, f) for f in pargs]
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen else:
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen certs = [os.path.abspath(f) for f in pargs]
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen
d47b9f1bd7274c7b2d9049c2e1718d1cf89cc572Timo Sirainen try:
d47b9f1bd7274c7b2d9049c2e1718d1cf89cc572Timo Sirainen repo.add_signing_certs(certs, ca=ca, pub=pfx)
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen except (apx.ApiException, sr.RepositoryError), e:
ba1c847d0af4afe4787ed470d0c818e948e184e2Timo Sirainen failed.append((pfx, e))
ba1c847d0af4afe4787ed470d0c818e948e184e2Timo Sirainen
ba1c847d0af4afe4787ed470d0c818e948e184e2Timo Sirainen if "all" in pubs:
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen # Default to list of all publishers.
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen pubs = repo.publishers
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen
415e16c3dc185578695b7d88e561a52de6c8b1b1Timo Sirainen if not pubs:
# Assume default publisher or older repository.
add_certs()
else:
# Add for each publisher specified.
map(add_certs, pubs)
return pubs, failed
def subcmd_add_signing_ca_cert(conf, args):
"""Add the provided signing ca certificates to the repository for
the given publisher."""
subcommand = "add-signing-ca-cert"
pubs, failed = _add_certs(conf, subcommand, args, True)
if failed:
for pfx, details in failed:
error(_("Unable to add signing ca certificates for "
"publisher '%(pfx)s':\n%(details)s") % locals(),
cmd=subcommand)
if len(failed) < len(pubs):
return EXIT_PARTIAL
return EXIT_OOPS
return EXIT_OK
def subcmd_add_signing_intermediate_cert(conf, args):
subcommand = "add-signing-intermediate-cert"
pubs, failed = _add_certs(conf, subcommand, args, True)
if failed:
for pfx, details in failed:
if pfx:
error(_("Unable to add signing intermediate "
"certificates for publisher '%(pfx)s':\n"
"%(details)s") % locals(), cmd=subcommand)
else:
error(_("Unable to add signing intermediate "
"certificates:\n%(details)s") % locals(),
cmd=subcommand)
if len(failed) < len(pubs):
return EXIT_PARTIAL
return EXIT_OOPS
return EXIT_OK
def _remove_certs(conf, subcommand, args, ca):
opts, pargs = getopt.getopt(args, "p:s:")
pubs = set()
for opt, arg in opts:
if opt == "-p":
pubs.add(arg)
elif opt == "-s":
conf["repo_uri"] = parse_uri(arg)
# Get repository object.
if not conf.get("repo_uri", None):
usage(_("A package repository location must be provided "
"using -s."), cmd=subcommand)
repo = get_repo(conf, read_only=False, subcommand=subcommand)
if len(pargs) < 1:
usage(_("At least one certificate hash must be provided."))
failed = []
def remove_certs(pfx=None):
try:
repo.remove_signing_certs(pargs, ca=True, pub=pfx)
except (apx.ApiException, sr.RepositoryError), e:
failed.append((pfx, e))
if "all" in pubs:
# Default to list of all publishers.
pubs = repo.publishers
if not pubs:
# Assume default publisher or older repository.
remove_certs()
else:
# Add for each publisher specified.
map(remove_certs, pubs)
return pubs, failed
def subcmd_remove_signing_ca_cert(conf, args):
subcommand = "remove-signing-ca-cert"
pubs, failed = _remove_certs(conf, subcommand, args, True)
if failed:
for pfx, details in failed:
error(_("Unable to remove signing ca certificates for "
"publisher '%(pfx)s':\n%(details)s") % locals(),
cmd=subcommand)
if len(failed) < len(pubs):
return EXIT_PARTIAL
return EXIT_OOPS
return EXIT_OK
def subcmd_remove_signing_intermediate_cert(conf, args):
subcommand = "remove-signing-intermediate-cert"
pubs, failed = _remove_certs(conf, subcommand, args, True)
if failed:
for pfx, details in failed:
if pfx:
error(_("Unable to remove signing intermediate "
"certificates for publisher '%(pfx)s':\n"
"%(details)s") % locals(), cmd=subcommand)
else:
error(_("Unable to remove signing intermediate "
"certificates:\n%(details)s") % locals(),
cmd=subcommand)
if len(failed) < len(pubs):
return EXIT_PARTIAL
return EXIT_OOPS
return EXIT_OK
def print_col_listing(desired_field_order, field_data, field_values, out_format,
def_fmt, omit_headers):
"""Print a columnar listing defined by provided values."""
# Custom sort function for preserving field ordering
def sort_fields(one, two):
return desired_field_order.index(get_header(one)) - \
desired_field_order.index(get_header(two))
# Functions for manipulating field_data records
def filter_default(record):
return "default" in record[0]
def filter_tsv(record):
return "tsv" in record[0]
def get_header(record):
return record[1]
def get_value(record):
return record[2]
def quote_value(val):
if out_format == "tsv":
# Expand tabs if tsv output requested.
val = val.replace("\t", " " * 8)
nval = val
# Escape bourne shell metacharacters.
for c in ("\\", " ", "\t", "\n", "'", "`", ";", "&", "(", ")",
"|", "^", "<", ">"):
nval = nval.replace(c, "\\" + c)
return nval
def set_value(entry):
val = entry[1]
multi_value = False
if isinstance(val, (list, set)):
multi_value = True
elif val == "":
entry[0][2] = '""'
return
elif val is None:
entry[0][2] = ''
return
else:
val = [val]
nval = []
for v in val:
if v == "":
# Indicate empty string value using "".
nval.append('""')
elif v is None:
# Indicate no value using empty string.
nval.append('')
else:
# Otherwise, escape the value to be displayed.
nval.append(quote_value(str(v)))
val = " ".join(nval)
nval = None
if multi_value:
val = "(%s)" % val
entry[0][2] = val
if out_format == "default":
# Create a formatting string for the default output
# format.
fmt = def_fmt
filter_func = filter_default
elif out_format == "tsv":
# Create a formatting string for the tsv output
# format.
num_fields = len(field_data.keys())
fmt = "\t".join('%s' for x in xrange(num_fields))
filter_func = filter_tsv
# Extract the list of headers from the field_data dictionary. Ensure
# they are extracted in the desired order by using the custom sort
# function.
hdrs = map(get_header, sorted(filter(filter_func, field_data.values()),
sort_fields))
# Output a header if desired.
if not omit_headers:
msg(fmt % tuple(hdrs))
for entry in field_values:
map(set_value, (
(field_data[f], v)
for f, v in entry.iteritems()
))
values = map(get_value, sorted(filter(filter_func,
field_data.values()), sort_fields))
msg(fmt % tuple(values))
def get_repo(conf, read_only=True, subcommand=None):
"""Return the repository object for current program configuration."""
repo_uri = conf["repo_uri"]
if repo_uri.scheme != "file":
usage(_("Network repositories are not currently supported "
"for this operation."), cmd=subcommand)
path = repo_uri.get_pathname()
if not path:
# Bad URI?
raise sr.RepositoryInvalidError(str(repo_uri))
return sr.Repository(read_only=read_only, root=path)
def setup_transport(conf):
repo_uri = conf.get("repo_uri", None)
if not repo_uri:
usage(_("No repository location specified."), cmd=subcommand)
temp_root = misc.config_temp_root()
tmp_dir = tempfile.mkdtemp(dir=temp_root)
tmpdirs.append(tmp_dir)
incoming_dir = tempfile.mkdtemp(dir=temp_root)
tmpdirs.append(incoming_dir)
cache_dir = tempfile.mkdtemp(dir=temp_root)
tmpdirs.append(cache_dir)
# Create transport and transport config.
xport, xport_cfg = transport.setup_transport()
xport_cfg.cached_download_dir = cache_dir
xport_cfg.incoming_download_dir = incoming_dir
# Configure target publisher.
src_pub = transport.setup_publisher(str(repo_uri), "target", xport,
xport_cfg, remote_prefix=True)
return xport, src_pub, tmp_dir
def subcmd_create(conf, args):
"""Create a package repository at the given location."""
subcommand = "create"
opts, pargs = getopt.getopt(args, "s:", ["version="])
version = None
for opt, arg in opts:
if opt == "-s":
conf["repo_uri"] = parse_uri(arg)
elif opt == "--version":
# This option is currently private and allows creating a
# repository with a specific format based on version.
try:
version = int(arg)
except ValueError:
usage(_("Version must be an integer value."),
cmd=subcommand)
if len(pargs) > 1:
usage(_("Only one repository location may be specified."),
cmd=subcommand)
elif pargs:
conf["repo_uri"] = parse_uri(pargs[0])
repo_uri = conf.get("repo_uri", None)
if not repo_uri:
usage(_("No repository location specified."), cmd=subcommand)
if repo_uri.scheme != "file":
usage(_("Network repositories are not currently supported "
"for this operation."), cmd=subcommand)
# Attempt to create a repository at the specified location. Allow
# whatever exceptions are raised to bubble up.
sr.repository_create(repo_uri, version=version)
return EXIT_OK
def subcmd_get(conf, args):
"""Display repository properties."""
subcommand = "get"
omit_headers = False
out_format = "default"
pubs = set()
opts, pargs = getopt.getopt(args, "F:Hp:s:")
for opt, arg in opts:
if opt == "-F":
out_format = arg
if out_format not in LISTING_FORMATS:
usage(_("Unrecognized format %(format)s."
" Supported formats: %(valid)s") % \
{ "format": out_format,
"valid": LISTING_FORMATS }, cmd="get")
return EXIT_OOPS
elif opt == "-H":
omit_headers = True
elif opt == "-p":
pubs.add(arg)
elif opt == "-s":
conf["repo_uri"] = parse_uri(arg)
# Setup transport so configuration can be retrieved.
if not conf.get("repo_uri", None):
usage(_("A package repository location must be provided "
"using -s."), cmd=subcommand)
xport, xpub, tmp_dir = setup_transport(conf)
# Get properties.
if pubs:
return _get_pub(conf, subcommand, xport, xpub, omit_headers,
out_format, pubs, pargs)
return _get_repo(conf, subcommand, xport, xpub, omit_headers,
out_format, pargs)
def _get_repo(conf, subcommand, xport, xpub, omit_headers, out_format, pargs):
"""Display repository properties."""
# Configuration index is indexed by section name and property name.
# Retrieve and flatten it to simplify listing process.
stat_idx = xport.get_status(xpub)
cfg_idx = stat_idx.get("repository", {}).get("configuration", {})
props = set()
# Set minimum widths for section and property name columns by using the
# length of the column headers.
max_sname_len = len(_("SECTION"))
max_pname_len = len(_("PROPERTY"))
for sname in cfg_idx:
max_sname_len = max(max_sname_len, len(sname))
for pname in cfg_idx[sname]:
max_pname_len = max(max_pname_len, len(pname))
props.add("/".join((sname, pname)))
req_props = set(pargs)
if len(req_props) >= 1:
found = props & req_props
notfound = req_props - found
del props
else:
found = props
notfound = set()
def gen_listing():
for prop in sorted(found):
sname, pname = prop.rsplit("/", 1)
sval = cfg_idx[sname][pname]
yield {
"section": sname,
"property": pname,
"value": sval,
}
# SECTION PROPERTY VALUE
# <sec_1> <prop_1> <prop_1_value>
# <sec_2> <prop_2> <prop_2_value>
# ...
field_data = {
"section" : [("default", "tsv"), _("SECTION"), ""],
"property" : [("default", "tsv"), _("PROPERTY"), ""],
"value" : [("default", "tsv"), _("VALUE"), ""],
}
desired_field_order = ((_("SECTION"), _("PROPERTY"), _("VALUE")))
# Default output formatting.
def_fmt = "%-" + str(max_sname_len) + "s %-" + str(max_pname_len) + \
"s %s"
if found or (not req_props and out_format == "default"):
print_col_listing(desired_field_order, field_data,
gen_listing(), out_format, def_fmt, omit_headers)
if found and notfound:
return EXIT_PARTIAL
if req_props and not found:
if out_format == "default":
# Don't pollute other output formats.
error(_("no matching properties found"),
cmd=subcommand)
return EXIT_OOPS
return EXIT_OK
def _get_pub(conf, subcommand, xport, xpub, omit_headers, out_format, pubs,
pargs):
"""Display publisher properties."""
# Retrieve publisher information.
pub_data = xport.get_publisherdata(xpub)
known_pubs = set(p.prefix for p in pub_data)
if len(pubs) > 0 and "all" not in pubs:
found = known_pubs & pubs
notfound = pubs - found
else:
found = known_pubs
notfound = set()
# Establish initial return value and perform early exit if appropriate.
rval = EXIT_OK
if found and notfound:
rval = EXIT_PARTIAL
elif pubs and not found:
if out_format == "default":
# Don't pollute other output formats.
error(_("no matching publishers found"),
cmd=subcommand)
return EXIT_OOPS
# Set minimum widths for section and property name columns by using the
# length of the column headers and data.
max_pubname_len = str(max(
[len(_("PUBLISHER"))] + [len(p) for p in found]
))
max_sname_len = len(_("SECTION"))
max_pname_len = len(_("PROPERTY"))
# For each requested publisher, retrieve the requested property data.
failed = set()
pub_idx = {}
for pub in pub_data:
if pub.prefix not in found:
continue
pub_idx[pub.prefix] = {
"publisher": {
"alias": pub.alias,
"prefix": pub.prefix,
},
}
pub_repo = pub.selected_repository
if pub_repo:
pub_idx[pub.prefix]["repository"] = {
"collection-type": pub_repo.collection_type,
"description": pub_repo.description,
"legal-uris": pub_repo.legal_uris,
"mirrors": pub_repo.mirrors,
"name": pub_repo.name,
"origins": pub_repo.origins,
"refresh-seconds": pub_repo.refresh_seconds,
"registration-uri": pub_repo.registration_uri,
"related-uris": pub_repo.related_uris,
}
else:
pub_idx[pub.prefix]["repository"] = {
"collection-type": "core",
"description": "",
"legal-uris": [],
"mirrors": [],
"name": "",
"origins": [],
"refresh-seconds": "",
"registration-uri": "",
"related-uris": [],
}
# Determine possible set of properties and lengths.
props = set()
for pub in pub_idx:
for sname in pub_idx[pub]:
max_sname_len = max(max_sname_len, len(sname))
for pname in pub_idx[pub][sname]:
max_pname_len = max(max_pname_len, len(pname))
props.add("/".join((sname, pname)))
# Determine properties to display.
req_props = set(pargs)
if len(req_props) >= 1:
found = props & req_props
notfound = req_props - found
del props
else:
found = props
notfound = set()
def gen_listing():
for pub in sorted(pub_idx.keys()):
for prop in sorted(found):
sname, pname = prop.rsplit("/", 1)
sval = pub_idx[pub][sname][pname]
yield {
"publisher": pub,
"section": sname,
"property": pname,
"value": sval,
}
# PUBLISHER SECTION PROPERTY VALUE
# <pub_1> <sec_1> <prop_1> <prop_1_value>
# <pub_1> <sec_2> <prop_2> <prop_2_value>
# ...
field_data = {
"publisher" : [("default", "tsv"), _("PUBLISHER"), ""],
"section" : [("default", "tsv"), _("SECTION"), ""],
"property" : [("default", "tsv"), _("PROPERTY"), ""],
"value" : [("default", "tsv"), _("VALUE"), ""],
}
desired_field_order = (_("PUBLISHER"), _("SECTION"), _("PROPERTY"),
_("VALUE"))
# Default output formatting.
def_fmt = "%-" + str(max_pubname_len) + "s %-" + str(max_sname_len) + \
"s %-" + str(max_pname_len) + "s %s"
if found or (not req_props and out_format == "default"):
print_col_listing(desired_field_order, field_data,
gen_listing(), out_format, def_fmt, omit_headers)
if found and notfound:
rval = EXIT_PARTIAL
if req_props and not found:
if out_format == "default":
# Don't pollute other output formats.
error(_("no matching properties found"),
cmd=subcommand)
rval = EXIT_OOPS
return rval
def subcmd_info(conf, args):
"""Display a list of known publishers and a summary of known packages
and when the package data for the given publisher was last updated.
"""
subcommand = "info"
omit_headers = False
out_format = "default"
pubs = set()
opts, pargs = getopt.getopt(args, "F:Hp:s:")
for opt, arg in opts:
if opt == "-F":
if arg not in LISTING_FORMATS:
usage(_("Unrecognized format %(format)s."
" Supported formats: %(valid)s") % \
{ "format": arg,
"valid": LISTING_FORMATS }, cmd="publisher")
return EXIT_OOPS
out_format = arg
elif opt == "-H":
omit_headers = True
elif opt == "-p":
pubs.add(arg)
elif opt == "-s":
conf["repo_uri"] = parse_uri(arg)
if pargs:
usage(_("command does not take operands"), cmd=subcommand)
# Setup transport so status can be retrieved.
if not conf.get("repo_uri", None):
usage(_("A package repository location must be provided "
"using -s."), cmd=subcommand)
xport, xpub, tmp_dir = setup_transport(conf)
# Retrieve repository status information.
stat_idx = xport.get_status(xpub)
pub_idx = stat_idx.get("repository", {}).get("publishers", {})
if len(pubs) > 0 and "all" not in pubs:
found = set(pub_idx.keys()) & pubs
notfound = pubs - found
else:
found = set(pub_idx.keys())
notfound = set()
def gen_listing():
for pfx in found:
pdata = pub_idx[pfx]
pkg_count = pdata.get("package-count", 0)
last_update = pdata.get("last-catalog-update", "")
if last_update:
# Reformat the date into something more user
# friendly (and locale specific).
last_update = pkg.catalog.basic_ts_to_datetime(
last_update)
last_update = "%sZ" % pkg.catalog.datetime_to_ts(
last_update)
rstatus = _(pub_idx[pfx].get("status", "online"))
yield {
"publisher": pfx,
"packages": pkg_count,
"status": rstatus,
"updated": last_update,
}
# PUBLISHER PACKAGES STATUS UPDATED
# <pub_1> <num_uniq_pkgs> <status> <cat_last_modified>
# <pub_2> <num_uniq_pkgs> <status> <cat_last_modified>
# ...
field_data = {
"publisher" : [("default", "tsv"), _("PUBLISHER"), ""],
"packages" : [("default", "tsv"), _("PACKAGES"), ""],
"status" : [("default", "tsv"), _("STATUS"), ""],
"updated" : [("default", "tsv"), _("UPDATED"), ""],
}
desired_field_order = (_("PUBLISHER"), "", _("PACKAGES"), _("STATUS"),
_("UPDATED"))
# Default output formatting.
pub_len = str(max(
[len(desired_field_order[0])] + [len(p) for p in found]
))
def_fmt = "%-" + pub_len + "s %-8s %-16s %s"
if found or (not pubs and out_format == "default"):
print_col_listing(desired_field_order, field_data,
gen_listing(), out_format, def_fmt, omit_headers)
if found and notfound:
return EXIT_PARTIAL
if pubs and not found:
if out_format == "default":
# Don't pollute other output formats.
error(_("no matching publishers found"),
cmd=subcommand)
return EXIT_OOPS
return EXIT_OK
def subcmd_rebuild(conf, args):
"""Rebuild the repository's catalog and index data (as permitted)."""
subcommand = "rebuild"
build_catalog = True
build_index = True
opts, pargs = getopt.getopt(args, "s:", ["no-catalog", "no-index"])
for opt, arg in opts:
if opt == "-s":
conf["repo_uri"] = parse_uri(arg)
elif opt == "--no-catalog":
build_catalog = False
elif opt == "--no-index":
build_index = False
if pargs:
usage(_("command does not take operands"), cmd=subcommand)
if not build_catalog and not build_index:
# Why? Who knows; but do what was requested--nothing!
return EXIT_OK
# Setup transport so operation can be performed.
if not conf.get("repo_uri", None):
usage(_("A package repository location must be provided "
"using -s."), cmd=subcommand)
xport, src_pub, tmp_dir = setup_transport(conf)
logger.info("Repository rebuild initiated.")
if build_catalog and build_index:
xport.publish_rebuild(src_pub)
elif build_catalog:
xport.publish_rebuild_packages(src_pub)
elif build_index:
xport.publish_rebuild_indexes(src_pub)
return EXIT_OK
def subcmd_refresh(conf, args):
"""Refresh the repository's catalog and index data (as permitted)."""
subcommand = "refresh"
add_content = True
refresh_index = True
opts, pargs = getopt.getopt(args, "s:", ["no-catalog", "no-index"])
for opt, arg in opts:
if opt == "-s":
conf["repo_uri"] = parse_uri(arg)
elif opt == "--no-catalog":
add_content = False
elif opt == "--no-index":
refresh_index = False
if pargs:
usage(_("command does not take operands"), cmd=subcommand)
if not add_content and not refresh_index:
# Why? Who knows; but do what was requested--nothing!
return EXIT_OK
# Setup transport so operation can be performed.
if not conf.get("repo_uri", None):
usage(_("A package repository location must be provided "
"using -s."), cmd=subcommand)
xport, src_pub, tmp_dir = setup_transport(conf)
logger.info("Repository refresh initiated.")
if add_content and refresh_index:
xport.publish_refresh(src_pub)
elif add_content:
xport.publish_refresh_packages(src_pub)
elif refresh_index:
xport.publish_refresh_indexes(src_pub)
return EXIT_OK
def subcmd_set(conf, args):
"""Set repository properties."""
subcommand = "set"
omit_headers = False
pubs = set()
opts, pargs = getopt.getopt(args, "p:s:")
for opt, arg in opts:
if opt == "-p":
pubs.add(arg)
elif opt == "-s":
conf["repo_uri"] = parse_uri(arg)
bad_args = False
props = {}
if not pargs:
bad_args = True
else:
for arg in pargs:
try:
# Attempt to parse property into components.
prop, val = arg.split("=", 1)
sname, pname = prop.rsplit("/", 1)
# Store property values by section.
props.setdefault(sname, {})
# Parse the property value into a list if
# necessary, otherwise append it to the list
# of values for the property.
if len(val) > 0 and val[0] == "(" and \
val[-1] == ")":
val = shlex.split(val.strip("()"))
if sname in props and pname in props[sname]:
# Determine if previous value is already
# a list, and if not, convert and append
# the value.
pval = props[sname][pname]
if not isinstance(pval, list):
pval = [pval]
if isinstance(val, list):
pval.extend(val)
else:
pval.append(val)
props[sname][pname] = pval
else:
# Otherwise, just store the value.
props[sname][pname] = val
except ValueError:
bad_args = True
break
if bad_args:
usage(_("a property name and value must be provided in the "
"form <section/property>=<value> or "
"<section/property>=([\"<value>\" ...])"))
# Get repository object.
if not conf.get("repo_uri", None):
usage(_("A package repository location must be provided "
"using -s."), cmd=subcommand)
repo = get_repo(conf, read_only=False, subcommand=subcommand)
# Set properties.
if pubs:
return _set_pub(conf, subcommand, omit_headers, props, pubs,
repo)
return _set_repo(conf, subcommand, omit_headers, props, repo)
def _set_pub(conf, subcommand, omit_headers, props, pubs, repo):
"""Set publisher properties."""
for sname, sprops in props.iteritems():
if sname not in ("publisher", "repository"):
usage(_("unknown property section "
"'%s'") % sname, cmd=subcommand)
for pname in sprops:
if sname == "publisher" and pname =="prefix":
usage(_("'%s' may not be set using "
"this command" % pname))
attrname = pname.replace("-", "_")
if not hasattr(publisher.Publisher, attrname) and \
not hasattr(publisher.Repository, attrname):
usage(_("unknown property '%s'") %
pname, cmd=subcommand)
if "all" in pubs:
# Default to list of all publishers.
pubs = repo.publishers
if not pubs:
# If there are still no known publishers, this
# operation cannot succeed, so fail now.
usage(_("One or more publishers must be specified to "
"create and set properties for as none exist yet."),
cmd=subcommand)
# Get publishers and update properties.
failed = []
new_pub = False
for pfx in pubs:
try:
# Get a copy of the existing publisher.
pub = copy.copy(repo.get_publisher(pfx))
except sr.RepositoryUnknownPublisher, e:
pub = publisher.Publisher(pfx)
new_pub = True
except sr.RepositoryError, e:
failed.append((pfx, e))
continue
try:
# Set/update the publisher's properties.
for sname, sprops in props.iteritems():
if sname == "publisher":
target = pub
elif sname == "repository":
target = pub.selected_repository
if not target:
target = publisher.Repository()
pub.repositories.append(target)
for pname, val in sprops.iteritems():
attrname = pname.replace("-", "_")
pval = getattr(target, attrname)
if isinstance(pval, list) and \
not isinstance(val, list):
# If the target property expects
# a list, transform the provided
# value into one if it isn't
# already.
if val == "":
val = []
else:
val = [val]
setattr(target, attrname, val)
except apx.ApiException, e:
failed.append((pfx, e))
continue
if new_pub:
repo.add_publisher(pub)
else:
repo.update_publisher(pub)
if failed:
for pfx, details in failed:
error(_("Unable to set properties for publisher "
"'%(pfx)s':\n%(details)s") % locals())
if len(failed) < len(pubs):
return EXIT_PARTIAL
return EXIT_OOPS
return EXIT_OK
def _set_repo(conf, subcommand, omit_headers, props, repo):
"""Set repository properties."""
# Set properties.
for sname, props in props.iteritems():
for pname, val in props.iteritems():
repo.cfg.set_property(sname, pname, val)
repo.write_config()
return EXIT_OK
def subcmd_version(conf, args):
"""Display the version of the pkg(5) API."""
subcommand = "version"
if args:
usage(_("command does not take operands"), cmd=subcommand)
msg(pkg.VERSION)
return EXIT_OK
def main_func():
global_settings.client_name = PKG_CLIENT_NAME
global orig_cwd
try:
orig_cwd = os.getcwd()
except OSError, e:
try:
orig_cwd = os.environ["PWD"]
if not orig_cwd or orig_cwd[0] != "/":
orig_cwd = None
except KeyError:
orig_cwd = None
try:
opts, pargs = getopt.getopt(sys.argv[1:], "s:?",
["help"])
except getopt.GetoptError, e:
usage(_("illegal global option -- %s") % e.opt)
conf = {}
show_usage = False
for opt, arg in opts:
if opt == "-s":
conf["repo_uri"] = parse_uri(arg)
elif opt in ("--help", "-?"):
show_usage = True
subcommand = None
if pargs:
subcommand = pargs.pop(0)
if subcommand == "help":
show_usage = True
if show_usage:
usage(retcode=0, full=True)
elif not subcommand:
usage(_("no subcommand specified"))
subcommand = subcommand.replace("-", "_")
func = globals().get("subcmd_%s" % subcommand, None)
if not func:
subcommand = subcommand.replace("_", "-")
usage(_("unknown subcommand '%s'") % subcommand)
try:
return func(conf, pargs)
except getopt.GetoptError, e:
if e.opt in ("help", "?"):
usage(full=True)
usage(_("illegal option -- %s") % e.opt, cmd=subcommand)
#
# Establish a specific exit status which means: "python barfed an exception"
# so that we can more easily detect these in testing of the CLI commands.
#
def handle_errors(func, *args, **kwargs):
"""Catch exceptions raised by the main program function and then print
a message and/or exit with an appropriate return code.
"""
traceback_str = _("\n\nThis is an internal error. Please let the "
"developers know about this\nproblem by filing a bug at "
"http://defect.opensolaris.org and including the\nabove "
"traceback and this message. The version of pkg(5) is "
"'%s'.") % pkg.VERSION
try:
# Out of memory errors can be raised as EnvironmentErrors with
# an errno of ENOMEM, so in order to handle those exceptions
# with other errnos, we nest this try block and have the outer
# one handle the other instances.
try:
__ret = func(*args, **kwargs)
except (MemoryError, EnvironmentError), __e:
if isinstance(__e, EnvironmentError) and \
__e.errno != errno.ENOMEM:
raise
error("\n" + misc.out_of_memory())
__ret = EXIT_OOPS
except SystemExit, __e:
raise __e
except (PipeError, KeyboardInterrupt):
# Don't display any messages here to prevent possible further
# broken pipe (EPIPE) errors.
__ret = EXIT_OOPS
except apx.VersionException, __e:
error(_("The pkgrepo command appears out of sync with the "
"libraries provided\nby pkg:/package/pkg. The client "
"version is %(client)s while the library\nAPI version is "
"%(api)s.") % {'client': __e.received_version,
'api': __e.expected_version
})
__ret = EXIT_OOPS
except apx.BadRepositoryURI, __e:
error(str(__e))
__ret = EXIT_BADOPT
except (apx.ApiException, sr.RepositoryError), __e:
error(str(__e))
__ret = EXIT_OOPS
except:
traceback.print_exc()
error(traceback_str)
__ret = 99
return __ret
if __name__ == "__main__":
misc.setlocale(locale.LC_ALL, "", error)
gettext.install("pkg", "/usr/share/locale")
# Make all warnings be errors.
warnings.simplefilter('error')
__retval = handle_errors(main_func)
try:
logging.shutdown()
except IOError:
# Ignore python's spurious pipe problems.
pass
sys.exit(__retval)