#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
#
import errno
import getopt
import gettext
import locale
import logging
import os
import re
import shutil
import six
import socket
import sys
import traceback
import warnings
import pkg
# exit codes
# static string with our versions response
DEPOT_FRAGMENT_VERSIONS_STR = """\
pkg-server {0}
publisher 0 1
versions 0
catalog 1
file 1
manifest 0
status 0
# versions response used when we provide search capability
DEPOT_VERSIONS_STR = """{0}admin 0
search 0 1
pass
"""Emit an error message prefixed by the command name """
if cmd:
pkg_cmd = "pkg.depot-config "
else:
pkg_cmd = "pkg.depot-config: "
# If we get passed something like an Exception, we can convert
# it down to a string.
# If the message starts with whitespace, assume that it should come
# *before* the command-name prefix.
# This has to be a constant value as we can't reliably get our actual
# program name on all platforms.
"""Emit a usage message and optionally prefix it with a more
specific error message. Causes program to exit.
"""
if usage_error:
msg(_("""\
Usage:
pkg.depot-config ( -d repository_dir | -S ) -r runtime_dir
[-c cache_dir] [-s cache_size] [-p port] [-h hostname]
[-l logs_dir] [-T template_dir] [-A]
[-t server_type] ( ( [-F] [-P server_prefix] ) | [--https
( ( --cert server_cert_file --key server_key_file
[--cert-chain ssl_cert_chain_file] ) |
--cert-key-dir cert_key_directory ) [ (--ca-cert ca_cert_file
--ca-key ca_key_file ) ]
[--smf-fmri smf_pkg_depot_fmri] ] )
"""))
"""Sets ownership for the given directory to pkg5srv:pkg5srv"""
try:
raise DepotException(_("Unable to chown {dir} to "
"{user}:{group}: {err}").format(
"""Given a repository root, return the list of available publishers,
try:
# we don't set writable_root, as we don't want to take the hit
# on potentially building an index here.
raise DepotException(
_("pkg.depot-config only supports v4 repositories"))
except Exception as e:
raise DepotException(e)
try:
except cfg.UnknownPropertyError:
default_pub = None
"""Writes the webserver configuration for the depot.
pubs repository and publisher information, a list in the form
[(publisher_prefix, repo_dir, repo_prefix,
writable_root), ... ]
default_pubs default publishers, per repository, a list in the form
[(default_publisher_prefix, repo_dir, repo_prefix) ... ]
runtime_dir where we write httpd.conf files
log_dir where Apache should write its log files
template_dir where we find our Mako templates
cache_dir where Apache should write its cache and wsgi search idx
cache_size how large our cache can grow
host our hostname, needed to set ServerName properly
port the port on which Apache should listen
sroot the prefix into the server namespace,
ignored if fragment==False
fragment True if we should only write a file to drop into conf.d/
(i.e. a partial server configuration)
allow_refresh True if we allow the 'refresh' or 'refresh-indexes'
admin/0 operations
The URI namespace we create on the web server looks like this:
<sroot>/<repo_prefix>/<publisher>/<file, catalog etc.>/<version>/
<sroot>/<repo_prefix>/<file, catalog etc.>/<version>/
'sroot' is only used when the Apache server is serving other content
and we want to separate pkg(7) resources from the other resources
provided.
'repo_prefix' exists so that we can disambiguate between multiple
repositories that provide the same publisher.
'ssl_cert_file' the location of the server certificate file.
'ssl_key_file' the location of the server key file.
'ssl_cert_chain_file' the location of the certificate chain file if the
the server certificate is not signed by the top level CA.
"""
try:
# check our hostname
# Apache needs IPv6 addresses wrapped in square brackets
if ":" in host:
# check our directories
dirs = [runtime_dir]
if not fragment:
if cache_dir:
raise DepotException(
# check our port
if not fragment:
try:
raise DepotException(
except ValueError:
raise DepotException(
# check our cache size
try:
if num < 0:
raise DepotException(_("invalid cache size: "
except ValueError:
raise DepotException(
# we're disabling unicode here because we want Mako to
# passthrough any filesystem path names, whatever the
# original encoding.
if fragment:
else:
ipv6_addr="::1",
)
# socket.getaddrinfo raise UnicodeDecodeError in Python 3
# for some input, such as '.'
raise DepotException(
_("Unable to write Apache configuration: {host}: "
raise DepotException(
"""Writes a static versions/0 response for the Apache depot."""
try:
raise DepotException(
"""Writes a static publisher/0 response for the depot."""
try:
# convert our list of strings to a list of Publishers
# write individual reponses for the publishers
# write a response that contains all publishers
raise DepotException(
"""Writes a status status/0 response for the depot."""
try:
raise DepotException(
"""Generate a certificate given a certificate request.
'serial' is the serial number for the certificate
'CN' is the subject common name of the certificate.
'starttime' is the timestamp when the certificate starts
being valid. 0 means now.
'endtime' is the timestamp when the certificate stops being
valid
'dump_cert_path' is the file the generated certificate gets dumped.
'dump_key_path' is the file the generated key gets dumped.
'issuerCert' is the certificate object of the issuer.
'issuerKey' is the key object of the issuer.
'key_type' is the key type. allowed value: TYPE_RSA and TYPE_DSA.
'key_bits' is number of bits to use in the key.
'digest' is the digestion method to use for signing.
"""
# If a issuer is specified, set the issuer. otherwise set cert
# itself as a issuer.
if issuerCert:
else:
# If there is a issuer key, sign with that key. Otherwise,
# create a self-signed cert.
# Cert requires bytes.
if issuerKey:
b"CA:FALSE")])
else:
b"CA:TRUE")])
output_dir="/tmp"):
""" Generate certificate and key files for https service."""
raise DepotException(
else:
# If the cert and key files do not exist, then generate one.
# Used as a factor to easily specify a year.
# If user specifies ca_cert_file and ca_key_file, just load
# the files. Otherwise, generate new ca_cert and ca_key.
if not ca_cert_file or not ca_key_file:
else:
raise DepotException(_("Cannot find user "
"provided CA certificate file: {0}").format(
raise DepotException(_("Cannot find user "
"provided CA key file: {0}").format(
"""Destroy any existing "htdocs" directory."""
try:
raise DepotException(
_("Unable to remove an existing 'htdocs' directory "
ssl_cert_chain_file=""):
"""Creates a new configuration for the depot."""
try:
if not repo_info:
raise DepotException(_("no repositories found"))
# pubs and default_pubs are lists of tuples of the form:
# (publisher prefix, repository root dir, repository prefix,
# writable_root)
pubs = []
default_pubs = []
errors = []
# Query each repository for its publisher information.
try:
for pub in publishers:
# The writable root must exist and must be
# owned by pkg5srv:pkg5srv
if writable_root:
except DepotException as err:
if errors:
raise DepotException(_("Unable to write configuration: "
# Write the publisher/0 response for each repository
pubs_by_repo = {}
for repo_prefix in pubs_by_repo:
# If we're writing a configuration fragment, then the web server
# is probably not running as DEPOT_USER:DEPOT_GROUP
if not fragment:
else:
return ret
def get_smf_repo_info():
which are marked as pkg/standalone = False and pkg/readonly = True."""
repo_info = []
for fmri in smf_instances:
writable_root = None
if (state == "online" and
readonly == "true" and
standalone == "false"):
if not repo_info:
raise DepotException(_(
"No online, readonly, non-standalone instances of "
return repo_info
"""Determine whether the repository root, and supplied prefixes are
unique. The prefixes allow two or more repositories that both contain
the same publisher to be differentiated in the Apache configuration, so
that requests are routed to the correct repository."""
writable_roots = set()
errors = []
"once in a given depot configuration").format(
prefix))
"than once in a given depot configuration").format(
root))
"than once in a given depot configuration").format(
if errors:
return True
if "/" in str:
raise DepotException(_("cannot use '/' chars in prefixes"))
# An RE that matches valid SMF instance names works for prefixes
str):
raise DepotException(_("%s is not a valid prefix"))
"""Update the smf props after the new prop values are generated."""
for fmri in smf_instances:
if refresh:
def main_func():
# some sensible defaults
host = "0.0.0.0"
# the port we listen on
port = None
# a list of (repo_dir, repo_prefix) tuples
repo_info = []
# the path where we store disk caches
cache_dir = None
# our maximum cache size, in megabytes
cache_size = 0
# whether we're writing a full httpd.conf, or just a fragment
# Whether we support https service.
# The location of server certificate file.
ssl_cert_file = ""
# The location of server key file.
ssl_key_file = ""
# The location of the server ca certificate file.
ssl_ca_cert_file = ""
# The location of the server ca key file.
ssl_ca_key_file = ""
# Directory for storing generated certificates and keys
cert_key_dir = ""
# SSL certificate chain file path if the server certificate is not
# signed by the top level CA.
ssl_cert_chain_file = ""
smf_fmri = ""
# an optional url-prefix, used to separate pkg5 services from the rest
# of the webserver url namespace, only used when running in fragment
# mode, otherwise we assume we're the only service running on this
# web server instance, and control the entire server URL namespace.
sroot = ""
# the path where our Mako templates and wsgi scripts are stored
template_dir = "/etc/pkg/depot"
# a volatile directory used at runtime for storing state
runtime_dir = None
# where logs are written
# whether we should pull configuration from
# svc:/application/pkg/server instances
# whether we allow admin/0 operations to rebuild the index
# the current server_type
server_type = "apache2"
try:
"Ac:d:Fh:l:P:p:r:Ss:t:T:?", ["help", "debug=", "https",
"cert=", "key=", "ca-cert=", "ca-key=", "cert-chain=",
"cert-key-dir=", "smf-fmri="])
if opt == "--help":
usage()
elif opt == "-h":
elif opt == "-c":
elif opt == "-s":
elif opt == "-l":
elif opt == "-p":
elif opt == "-r":
elif opt == "-T":
elif opt == "-t":
elif opt == "-d":
if "=" not in arg:
usage(_("-d arguments must be in the "
"form <prefix>=<repo path>"
"[=writable root]"))
writable_root = None
elif opt == "-P":
elif opt == "-F":
elif opt == "-S":
elif opt == "-A":
elif opt == "--https":
elif opt == "--cert":
elif opt == "--key":
elif opt == "--ca-cert":
elif opt == "--ca-key":
elif opt == "--cert-chain":
elif opt == "--cert-key-dir":
elif opt == "--smf-fmri":
elif opt == "--debug":
try:
except (AttributeError, ValueError):
_("{opt} takes argument of form "
"name=value, not {arg}").format(
else:
except getopt.GetoptError as e:
if not runtime_dir:
usage(_("required runtime dir option -r missing."))
# we need a cache_dir to store the SSLSessionCache
usage(_("cache_dir option -c is required if -F is not used."))
usage(_("required port option -p missing."))
if not use_smf_instances and not repo_info:
usage(_("at least one -d option is required if -S is "
"not used."))
if repo_info and use_smf_instances:
usage(_("cannot use -d and -S together."))
if https:
if fragment:
usage(_("https configuration is not supported in "
"fragment mode."))
usage(_("certificate and key files must be presented "
"at the same time."))
elif not ssl_cert_file and not ssl_key_file:
if not cert_key_dir:
usage(_("cert-key-dir option is require to "
"store the generated certificates and keys"))
usage(_("Cannot use --cert-chain without "
"--cert and --key"))
usage(_("server CA certificate and key files "
"must be presented at the same time."))
# record the proporty values for updating.
if smf_fmri:
try:
ssl_key_file = \
if ssl_ca_cert_file:
msg(_("Server CA certificate is "
"located at {0}. Please deploy it "
"each client.").format(
except (DepotException, EnvironmentError) as e:
error(e)
return EXIT_OOPS
# anything changes.
if smf_fmri:
prop_list = ["config/ssl_ca_cert_file",
try:
except (smf.NonzeroExitException,
RuntimeError) as e:
error(e)
return EXIT_OOPS
else:
error(_("User provided server certificate "
"file {0} does not exist.").format(
return EXIT_OOPS
error(_("User provided server key file {0} "
return EXIT_OOPS
error(_("User provided certificate chain file "
"{0} does not exist.").format(
return EXIT_OOPS
else:
usage(_("certificate or key files are given before "
"https service is turned on. Use --https to turn "
"on the service."))
if smf_fmri:
usage(_("cannot use --smf-fmri without --https."))
# We can't support httpd.conf fragments with writable root, because
# we don't have the mod_wsgi app that can build the index or serve
# search requests everywhere the fragments might be used. (eg. on
# non-Solaris systems)
if writable_root_set and fragment:
usage(_("cannot use -d with writable roots and -F together."))
usage(_("cannot use -F and -p together."))
if fragment and allow_refresh:
usage(_("cannot use -F and -A together."))
usage(_("cannot use -P without -F."))
try:
except DepotException as e:
error(e)
# We can produce configuration for different HTTP servers.
# For now, We support "apache2" and "apache22".
if server_type not in KNOWN_SERVER_TYPES:
usage(_("unknown server type {type}. "
"Known types are: {known}").format(
# Generate Apache 2.2 configurations.
template_dir = "/etc/pkg/depot/apache22"
try:
except DepotException as e:
error(e)
return ret
#
# 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.
#
"""Catch exceptions raised by the main program function and then print
"""
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:
raise
except SystemExit as __e:
raise __e
except (PipeError, KeyboardInterrupt):
# Don't display any messages here to prevent possible further
# broken pipe (EPIPE) errors.
except:
__ret = 99
return __ret
if __name__ == "__main__":
# Make all warnings be errors.
# disable ResourceWarning: unclosed file
try:
except IOError:
# Ignore python's spurious pipe problems.
pass