Adds SSL configuration params to the client

* Adds SSL configuration params to all client classes
* Adds unit test for bad SSL client configuration
* Refactors the registry methods to no longer need
  configuration options passed, and to configure the
  registry client once, on images.Controller.__init__
* Adds glance-api.conf options for SSL support in
  registry client connections.
* Adds SSL CA file socket wrappers via a client auth
  HTTPS connection class
* Adds server SSL support, but not functional tests
  for SSL support yet. Still need to research self-signed
  cert generation for tests...
* Updates documentation for bind and startup options
* Adds functional test case for secure communication
  with API server stood up with SSL supprt. Note it is
  not very DRY. There is some DRY cleanup to do in the
  future...

TODO: Integrate options with bin/glance CLI tool

Change-Id: Ie9fcd36337cc93fd5beeabb9186ad5e93ae2a0f0
This commit is contained in:
Jay Pipes 2011-08-09 16:08:50 -04:00 committed by Brian Waldon
parent 53db059772
commit eec5c1afa1
19 changed files with 1757 additions and 118 deletions

@ -980,8 +980,13 @@ def get_client(options):
auth_url=os.getenv('OS_AUTH_URL'),
strategy=os.getenv('OS_AUTH_STRATEGY', 'noauth'))
use_ssl = (options.host.find('https') != -1 or (
creds['auth_url'] is not None and
creds['auth_url'].find('https') != -1))
return glance_client.Client(host=options.host, port=options.port,
auth_tok=options.auth_token, creds=creds)
use_ssl=use_ssl, auth_tok=options.auth_token,
creds=creds)
def create_options(parser):

@ -63,7 +63,7 @@ if __name__ == '__main__':
conf, app = config.load_paste_app('glance-api', options, args)
server = wsgi.Server()
server.start(app, int(conf['bind_port']), conf['bind_host'])
server.start(app, int(conf['bind_port']), conf['bind_host'], conf)
server.wait()
except RuntimeError, e:
sys.exit("ERROR: %s" % e)

@ -63,7 +63,7 @@ if __name__ == '__main__':
conf, app = config.load_paste_app('glance-registry', options, args)
server = wsgi.Server()
server.start(app, int(conf['bind_port']), conf['bind_host'])
server.start(app, int(conf['bind_port']), conf['bind_host'], conf)
server.wait()
except RuntimeError, e:
sys.exit("ERROR: %s" % e)

@ -88,6 +88,80 @@ The filename that is searched for depends on the server application name. So,
if you are starting up the API server, ``glance-api.conf`` is searched for,
otherwise ``glance-registry.conf``.
Configuring Server Startup Options
----------------------------------
You can put the following options in the ``glance-api.conf`` and
``glance-registry.conf`` files, under the ``[DEFAULT]`` section. They enable
startup and binding behaviour for the API and registry servers, respectively.
* ``bind_host=ADDRESS``
The address of the host to bind to.
Optional. Default: ``0.0.0.0``
* ``bind_port=PORT``
The port the server should bind to.
Optional. Default: ``9191`` for the registry server, ``9292`` for the API server
* ``backlog=REQUESTS``
Number of backlog requests to configure the socket with.
Optional. Default: ``4096``
Configurating SSL Support
~~~~~~~~~~~~~~~~~~~~~~~~~
* ``cert_file=PATH``
Path to the the certificate file the server should use when binding to an
SSL-wrapped socket.
Optional. Default: not enabled.
* ``key_file=PATH``
Path to the the private key file the server should use when binding to an
SSL-wrapped socket.
Optional. Default: not enabled.
* ``registry_client_protocol=PROTOCOL``
If you run a secure Registry server, you need to set this value to ``https``
and also set ``registry_client_key_file`` and optionally
``registry_client_cert_file``.
Optional. Default: http
* ``registry_client_key_file=PATH``
The path to the key file to use in SSL connections to the
registry server, if any. Alternately, you may set the
``GLANCE_CLIENT_KEY_FILE`` environ variable to a filepath of the key file
Optional. Default: Not set.
* ``registry_client_cert_file=PATH``
Optional. Default: Not set.
The path to the cert file to use in SSL connections to the
registry server, if any. Alternately, you may set the
``GLANCE_CLIENT_CERT_FILE`` environ variable to a filepath of the cert file
* ``registry_client_ca_file=PATH``
Optional. Default: Not set.
The path to a Certifying Authority's cert file to use in SSL connections to the
registry server, if any. Alternately, you may set the
``GLANCE_CLIENT_CA_FILE`` environ variable to a filepath of the CA cert file
Configuring Logging in Glance
-----------------------------
@ -132,6 +206,12 @@ Defaults to ``%Y-%m-%d %H:%M:%S``. See the
`logging module <http://docs.python.org/library/logging.html>`_ documentation for
more information on setting this format string.
* ``log_use_syslog``
Use syslog logging functionality.
Defaults to False.
Configuring Glance Storage Backends
-----------------------------------

@ -16,12 +16,6 @@ bind_host = 0.0.0.0
# Port the bind the API server to
bind_port = 9292
# Address to find the registry server
registry_host = 0.0.0.0
# Port the registry server is listening on
registry_port = 9191
# Log to this file. Make sure you do not set the same log
# file for both the API and registry servers!
log_file = /var/log/glance/api.log
@ -29,6 +23,44 @@ log_file = /var/log/glance/api.log
# Send logs to syslog (/dev/log) instead of to file specified by `log_file`
use_syslog = False
# Backlog requests when creating socket
backlog = 4096
# ================= SSL Options ===============================
# Certificate file to use when starting API server securely
# cert_file = /path/to/certfile
# Private key file to use when starting API server securely
# key_file = /path/to/keyfile
# ============ Registry Options ===============================
# Address to find the registry server
registry_host = 0.0.0.0
# Port the registry server is listening on
registry_port = 9191
# What protocol to use when connecting to the registry server?
# Set to https for secure HTTP communication
registry_client_protocol = http
# The path to the key file to use in SSL connections to the
# registry server, if any. Alternately, you may set the
# GLANCE_CLIENT_KEY_FILE environ variable to a filepath of the key file
# registry_client_key_file = /path/to/key/file
# The path to the cert file to use in SSL connections to the
# registry server, if any. Alternately, you may set the
# GLANCE_CLIENT_CERT_FILE environ variable to a filepath of the cert file
# registry_client_cert_file = /path/to/cert/file
# The path to the certifying authority cert file to use in SSL connections
# to the registry server, if any. Alternately, you may set the
# GLANCE_CLIENT_CA_FILE environ variable to a filepath of the CA cert file
# registry_client_ca_file = /path/to/ca/file
# ============ Notification System Options =====================
# Notifications can be sent when images are create, updated or deleted.

@ -18,6 +18,9 @@ log_file = /var/log/glance/registry.log
# Send logs to syslog (/dev/log) instead of to file specified by `log_file`
use_syslog = False
# Backlog requests when creating socket
backlog = 4096
# SQLAlchemy connection string for the reference implementation
# registry server. Any valid SQLAlchemy connection string is fine.
# See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine
@ -40,6 +43,14 @@ api_limit_max = 1000
# default to `limit_param_default`
limit_param_default = 25
# ================= SSL Options ===============================
# Certificate file to use when starting registry server securely
# cert_file = /path/to/certfile
# Private key file to use when starting registry server securely
# key_file = /path/to/keyfile
[pipeline:glance-registry]
pipeline = context registryapp
# NOTE: use the following pipeline for keystone

@ -37,7 +37,7 @@ class BaseController(object):
"""
context = request.context
try:
return registry.get_image_metadata(self.options, context, image_id)
return registry.get_image_metadata(context, image_id)
except exception.NotFound:
msg = _("Image with identifier %s not found") % image_id
logger.debug(msg)

@ -82,6 +82,7 @@ class Controller(api.BaseController):
self.options = options
glance.store.create_stores(options)
self.notifier = notifier.Notifier(options)
registry.configure_registry_client(options)
def index(self, req):
"""
@ -108,8 +109,7 @@ class Controller(api.BaseController):
"""
params = self._get_query_params(req)
try:
images = registry.get_images_list(self.options, req.context,
**params)
images = registry.get_images_list(req.context, **params)
except exception.Invalid, e:
raise HTTPBadRequest(explanation="%s" % e)
@ -141,8 +141,7 @@ class Controller(api.BaseController):
"""
params = self._get_query_params(req)
try:
images = registry.get_images_detail(self.options, req.context,
**params)
images = registry.get_images_detail(req.context, **params)
# Strip out the Location attribute. Temporary fix for
# LP Bug #755916. This information is still coming back
# from the registry, since the API server still needs access
@ -304,9 +303,7 @@ class Controller(api.BaseController):
image_meta['size'] = image_meta.get('size', 0)
try:
image_meta = registry.add_image_metadata(self.options,
req.context,
image_meta)
image_meta = registry.add_image_metadata(req.context, image_meta)
return image_meta
except exception.Duplicate:
msg = (_("An image with identifier %s already exists")
@ -352,7 +349,7 @@ class Controller(api.BaseController):
image_id = image_meta['id']
logger.debug(_("Setting image %s to status 'saving'"), image_id)
registry.update_image_metadata(self.options, req.context, image_id,
registry.update_image_metadata(req.context, image_id,
{'status': 'saving'})
try:
logger.debug(_("Uploading image data for image %(image_id)s "
@ -387,8 +384,7 @@ class Controller(api.BaseController):
logger.debug(_("Updating image %(image_id)s data. "
"Checksum set to %(checksum)s, size set "
"to %(size)d"), locals())
registry.update_image_metadata(self.options, req.context,
image_id,
registry.update_image_metadata(req.context, image_id,
{'checksum': checksum,
'size': size})
self.notifier.info('image.upload', image_meta)
@ -435,10 +431,8 @@ class Controller(api.BaseController):
image_meta = {}
image_meta['location'] = location
image_meta['status'] = 'active'
return registry.update_image_metadata(self.options,
req.context,
image_id,
image_meta)
return registry.update_image_metadata(req.context, image_id,
image_meta)
def _kill(self, req, image_id):
"""
@ -447,9 +441,7 @@ class Controller(api.BaseController):
:param req: The WSGI/Webob Request object
:param image_id: Opaque image identifier
"""
registry.update_image_metadata(self.options,
req.context,
image_id,
registry.update_image_metadata(req.context, image_id,
{'status': 'killed'})
def _safe_kill(self, req, image_id):
@ -562,8 +554,7 @@ class Controller(api.BaseController):
raise HTTPConflict(_("Cannot upload to an unqueued image"))
try:
image_meta = registry.update_image_metadata(self.options,
req.context, id,
image_meta = registry.update_image_metadata(req.context, id,
image_meta, True)
if image_data is not None:
image_meta = self._upload_and_activate(req, image_meta)
@ -613,7 +604,7 @@ class Controller(api.BaseController):
if image['location']:
schedule_delete_from_backend(image['location'], self.options,
req.context, id)
registry.delete_image_metadata(self.options, req.context, id)
registry.delete_image_metadata(req.context, id)
except exception.NotFound, e:
msg = ("Failed to find image to delete: %(e)s" % locals())
for line in msg.split('\n'):

@ -32,8 +32,7 @@ class Controller(object):
]}
"""
try:
members = registry.get_image_members(self.options, req.context,
image_id)
members = registry.get_image_members(req.context, image_id)
except exception.NotFound:
msg = _("Image with identifier %s not found") % image_id
logger.debug(msg)
@ -54,7 +53,7 @@ class Controller(object):
raise webob.exc.HTTPUnauthorized(_("No authenticated user"))
try:
registry.delete_member(self.options, req.context, image_id, id)
registry.delete_member(req.context, image_id, id)
except exception.NotFound, e:
msg = "%s" % e
logger.debug(msg)
@ -93,8 +92,7 @@ class Controller(object):
if body and 'member' in body and 'can_share' in body['member']:
can_share = bool(body['member']['can_share'])
try:
registry.add_member(self.options, req.context, image_id, id,
can_share)
registry.add_member(req.context, image_id, id, can_share)
except exception.NotFound, e:
msg = "%s" % e
logger.debug(msg)
@ -122,8 +120,7 @@ class Controller(object):
raise webob.exc.HTTPUnauthorized(_("No authenticated user"))
try:
registry.replace_members(self.options, req.context,
image_id, body)
registry.replace_members(req.context, image_id, body)
except exception.NotFound, e:
msg = "%s" % e
logger.debug(msg)
@ -149,7 +146,7 @@ class Controller(object):
]}
"""
try:
members = registry.get_member_images(self.options, req.context, id)
members = registry.get_member_images(req.context, id)
except exception.NotFound, e:
msg = "%s" % e
logger.debug(msg)

@ -1,9 +1,32 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack, LLC
# All Rights Reserved.
#
# 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
#
# http://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.
# HTTPSClientAuthConnection code comes courtesy of ActiveState website:
# http://code.activestate.com/recipes/
# 577548-https-httplib-client-connection-with-certificate-v/
import httplib
import logging
import socket
import os
import urllib
import urlparse
from eventlet.green import socket, ssl
# See http://code.google.com/p/python-nose/issues/detail?id=373
# The code below enables glance.client standalone to work with i18n _() blocks
import __builtin__
@ -43,6 +66,48 @@ class ImageBodyIterator(object):
break
class HTTPSClientAuthConnection(httplib.HTTPSConnection):
"""
Class to make a HTTPS connection, with support for
full client-based SSL Authentication
:see http://code.activestate.com/recipes/
577548-https-httplib-client-connection-with-certificate-v/
"""
def __init__(self, host, port, key_file, cert_file,
ca_file, timeout=None):
httplib.HTTPSConnection.__init__(self, host, port, key_file=key_file,
cert_file=cert_file)
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
self.timeout = timeout
def connect(self):
"""
Connect to a host on a given (SSL) port.
If ca_file is pointing somewhere, use it to check Server Certificate.
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
ssl.wrap_socket(), which forces SSL to check server certificate against
our client certificate.
"""
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
self.sock = sock
self._tunnel()
# If there's no CA File, don't force Server Certificate Check
if self.ca_file:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
ca_certs=self.ca_file,
cert_reqs=ssl.CERT_REQUIRED)
else:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
cert_reqs=ssl.CERT_NONE)
class BaseClient(object):
"""A base client class"""
@ -52,7 +117,8 @@ class BaseClient(object):
DEFAULT_DOC_ROOT = None
def __init__(self, host, port=None, use_ssl=False, auth_tok=None,
creds=None, doc_root=None):
creds=None, doc_root=None,
key_file=None, cert_file=None, ca_file=None):
"""
Creates a new client to some service.
@ -62,6 +128,23 @@ class BaseClient(object):
:param auth_tok: The auth token to pass to the server
:param creds: The credentials to pass to the auth plugin
:param doc_root: Prefix for all URLs we request from host
:param key_file: Optional PEM-formatted file that contains the private
key.
If use_ssl is True, and this param is None (the
default), then an environ variable
GLANCE_CLIENT_KEY_FILE is looked for. If no such
environ variable is found, ClientConnectionError
will be raised.
:param cert_file: Optional PEM-formatted certificate chain file.
If use_ssl is True, and this param is None (the
default), then an environ variable
GLANCE_CLIENT_CERT_FILE is looked for. If no such
environ variable is found, ClientConnectionError
will be raised.
:param ca_file: Optional CA cert file to use in SSL connections
If use_ssl is True, and this param is None (the
default), then an environ variable
GLANCE_CLIENT_CA_FILE is looked for.
"""
self.host = host
self.port = port or self.DEFAULT_PORT
@ -69,8 +152,48 @@ class BaseClient(object):
self.auth_tok = auth_tok
self.creds = creds or {}
self.connection = None
self.doc_root = self.DEFAULT_DOC_ROOT if doc_root is None else doc_root
# doc_root can be a nullstring, which is valid, and why we
# cannot simply do doc_root or self.DEFAULT_DOC_ROOT below.
self.doc_root = (doc_root if doc_root is not None
else self.DEFAULT_DOC_ROOT)
self.auth_plugin = self.make_auth_plugin(self.creds)
self.connect_kwargs = {}
if use_ssl:
if not key_file:
if not os.environ.get('GLANCE_CLIENT_KEY_FILE'):
msg = _("You have selected to use SSL in connecting, "
"however you have failed to supply either a "
"key_file parameter or set the "
"GLANCE_CLIENT_KEY_FILE environ variable")
raise exception.ClientConnectionError(msg)
key_file = os.environ.get('GLANCE_CLIENT_KEY_FILE')
if not os.path.exists(key_file):
msg = _("The key file you specified %s does not "
"exist") % key_file
raise exception.ClientConnectionError(msg)
self.connect_kwargs['key_file'] = key_file
if not cert_file:
if not os.environ.get('GLANCE_CLIENT_CERT_FILE'):
msg = _("You have selected to use SSL in connecting, "
"however you have failed to supply either a "
"cert_file parameter or set the "
"GLANCE_CLIENT_CERT_FILE environ variable")
raise exception.ClientConnectionError(msg)
cert_file = os.environ.get('GLANCE_CLIENT_CERT_FILE')
if not os.path.exists(cert_file):
msg = _("The key file you specified %s does not "
"exist") % cert_file
raise exception.ClientConnectionError(msg)
self.connect_kwargs['cert_file'] = cert_file
if not ca_file:
ca_file = os.environ.get('GLANCE_CLIENT_CA_FILE')
self.connect_kwargs['ca_file'] = ca_file
def set_auth_token(self, auth_tok):
"""
@ -112,7 +235,7 @@ class BaseClient(object):
Returns the proper connection type
"""
if self.use_ssl:
return httplib.HTTPSConnection
return HTTPSClientAuthConnection
else:
return httplib.HTTPConnection
@ -186,7 +309,7 @@ class BaseClient(object):
if 'x-auth-token' not in headers and self.auth_tok:
headers['x-auth-token'] = self.auth_tok
c = connection_type(self.host, self.port)
c = connection_type(self.host, self.port, **self.connect_kwargs)
if self.doc_root:
action = '/'.join([self.doc_root, action.lstrip('/')])

@ -107,6 +107,11 @@ class InvalidContentType(GlanceException):
message = _("Invalid content type %(content_type)s")
class BadRegistryConnectionConfiguration(GlanceException):
message = _("Registry was not configured correctly on API server. "
"Reason: %(reason)s")
class BadStoreConfiguration(GlanceException):
message = _("Store %(store_name)s could not be configured correctly. "
"Reason: %(reason)s")
@ -122,4 +127,4 @@ class StoreAddDisabled(GlanceException):
class InvalidNotifierStrategy(GlanceException):
message = "'%(strategy)s' is not an available notifier strategy."
message = _("'%(strategy)s' is not an available notifier strategy.")

@ -21,14 +21,15 @@
Utility methods for working with WSGI servers
"""
import datetime
import json
import logging
import sys
import datetime
import time
import eventlet
from eventlet.green import socket, ssl
import eventlet.wsgi
eventlet.patcher.monkey_patch(all=False, socket=True)
import routes
import routes.middleware
import webob.dec
@ -48,10 +49,57 @@ class WritableLogger(object):
self.logger.log(self.level, msg.strip("\n"))
def run_server(application, port):
"""Run a WSGI server with the given application."""
sock = eventlet.listen(('0.0.0.0', port))
eventlet.wsgi.server(sock, application)
def get_socket(host, port, conf):
"""
Bind socket to bind ip:port in conf
note: Mostly comes from Swift with a few small changes...
:param host: Host to bind to
:param port: Port to bind to
:param conf: Configuration dict to read settings from
:returns : a socket object as returned from socket.listen or
ssl.wrap_socket if conf specifies cert_file
"""
bind_addr = (host, port)
# TODO(jaypipes): eventlet's greened socket module does not actually
# support IPv6 in getaddrinfo(). We need to get around this in the
# future or monitor upstream for a fix
address_family = [addr[0] for addr in socket.getaddrinfo(bind_addr[0],
bind_addr[1], socket.AF_UNSPEC, socket.SOCK_STREAM)
if addr[0] in (socket.AF_INET, socket.AF_INET6)][0]
backlog = int(conf.get('backlog', 4096))
cert_file = conf.get('cert_file')
key_file = conf.get('key_file')
use_ssl = cert_file or key_file
if use_ssl and (not cert_file or not key_file):
raise RuntimeError(_("When running server in SSL mode, you must "
"specify both a cert_file and key_file "
"option value in your configuration file"))
sock = None
retry_until = time.time() + 30
while not sock and time.time() < retry_until:
try:
sock = eventlet.listen(bind_addr, backlog=backlog,
family=address_family)
if use_ssl:
sock = ssl.wrap_socket(sock, certfile=cert_file,
keyfile=key_file)
except socket.error, err:
if err.args[0] != errno.EADDRINUSE:
raise
sleep(0.1)
if not sock:
raise RuntimeError(_("Could not bind to %s:%s after trying for 30 "
"seconds") % bind_addr)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# in my experience, sockets can hang around forever without keepalive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 600)
return sock
class Server(object):
@ -60,9 +108,17 @@ class Server(object):
def __init__(self, threads=1000):
self.pool = eventlet.GreenPool(threads)
def start(self, application, port, host='0.0.0.0', backlog=128):
"""Run a WSGI server with the given application."""
socket = eventlet.listen((host, port), backlog=backlog)
def start(self, application, port, host='0.0.0.0', conf=None):
"""
Run a WSGI server with the given application.
:param application: The application to run in the WSGI server
:param port: Port to bind to
:param host: Host to bind to
:param conf: Mapping of configuration options
"""
conf = conf or {}
socket = get_socket(host, port, conf)
self.pool.spawn_n(self._run, application, socket)
def wait(self):

@ -21,103 +21,111 @@ Registry API
import logging
from glance.common import config
from glance.common import exception
from glance.registry import client
logger = logging.getLogger('glance.registry')
def get_registry_client(options, context):
host = options['registry_host']
port = int(options['registry_port'])
return client.RegistryClient(host, port, auth_tok=context.auth_tok)
_CLIENT_HOST = None
_CLIENT_PORT = None
_CLIENT_KWARGS = {}
def get_images_list(options, context, **kwargs):
c = get_registry_client(options, context)
def configure_registry_client(options):
"""
Sets up a registry client for use in registry lookups
:param options: Configuration options coming from controller
"""
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT
try:
host = options['registry_host']
port = int(options['registry_port'])
except (TypeError, ValueError):
msg = _("Configuration option was not valid")
logger.error(msg)
raise exception.BadRegistryConnectionConfiguration(msg)
except IndexError:
msg = _("Could not find required configuration option")
logger.error(msg)
raise exception.BadRegistryConnectionConfiguration(msg)
use_ssl = config.get_option(options, 'registry_client_protocol',
default='http').lower() == 'https'
key_file = options.get('registry_client_key_file')
cert_file = options.get('registry_client_cert_file')
ca_file = options.get('registry_client_ca_file')
_CLIENT_HOST = host
_CLIENT_PORT = port
_CLIENT_KWARGS = {'use_ssl': use_ssl,
'key_file': key_file,
'cert_file': cert_file,
'ca_file': ca_file}
def get_registry_client(cxt):
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT
kwargs = _CLIENT_KWARGS.copy()
kwargs['auth_tok'] = cxt.auth_tok
return client.RegistryClient(_CLIENT_HOST, _CLIENT_PORT, **kwargs)
def get_images_list(context, **kwargs):
c = get_registry_client(context)
return c.get_images(**kwargs)
def get_images_detail(options, context, **kwargs):
c = get_registry_client(options, context)
def get_images_detail(context, **kwargs):
c = get_registry_client(context)
return c.get_images_detailed(**kwargs)
def get_image_metadata(options, context, image_id):
c = get_registry_client(options, context)
def get_image_metadata(context, image_id):
c = get_registry_client(context)
return c.get_image(image_id)
def add_image_metadata(options, context, image_meta):
if options['debug']:
logger.debug(_("Adding image metadata..."))
_debug_print_metadata(image_meta)
c = get_registry_client(options, context)
new_image_meta = c.add_image(image_meta)
if options['debug']:
logger.debug(_("Returned image metadata from call to "
"RegistryClient.add_image():"))
_debug_print_metadata(new_image_meta)
return new_image_meta
def add_image_metadata(context, image_meta):
logger.debug(_("Adding image metadata..."))
c = get_registry_client(context)
return c.add_image(image_meta)
def update_image_metadata(options, context, image_id, image_meta,
def update_image_metadata(context, image_id, image_meta,
purge_props=False):
if options['debug']:
logger.debug(_("Updating image metadata for image %s..."), image_id)
_debug_print_metadata(image_meta)
c = get_registry_client(options, context)
new_image_meta = c.update_image(image_id, image_meta, purge_props)
if options['debug']:
logger.debug(_("Returned image metadata from call to "
"RegistryClient.update_image():"))
_debug_print_metadata(new_image_meta)
return new_image_meta
logger.debug(_("Updating image metadata for image %s..."), image_id)
c = get_registry_client(context)
return c.update_image(image_id, image_meta, purge_props)
def delete_image_metadata(options, context, image_id):
def delete_image_metadata(context, image_id):
logger.debug(_("Deleting image metadata for image %s..."), image_id)
c = get_registry_client(options, context)
c = get_registry_client(context)
return c.delete_image(image_id)
def get_image_members(options, context, image_id):
c = get_registry_client(options, context)
def get_image_members(context, image_id):
c = get_registry_client(context)
return c.get_image_members(image_id)
def get_member_images(options, context, member_id):
c = get_registry_client(options, context)
def get_member_images(context, member_id):
c = get_registry_client(context)
return c.get_member_images(member_id)
def replace_members(options, context, image_id, member_data):
c = get_registry_client(options, context)
def replace_members(context, image_id, member_data):
c = get_registry_client(context)
return c.replace_members(image_id, member_data)
def add_member(options, context, image_id, member_id, can_share=None):
c = get_registry_client(options, context)
def add_member(context, image_id, member_id, can_share=None):
c = get_registry_client(context)
return c.add_member(image_id, member_id, can_share=can_share)
def delete_member(options, context, image_id, member_id):
c = get_registry_client(options, context)
def delete_member(context, image_id, member_id):
c = get_registry_client(context)
return c.delete_member(image_id, member_id)
def _debug_print_metadata(image_meta):
data = image_meta.copy()
properties = data.pop('properties', None)
for key, value in sorted(data.items()):
logger.debug(" %(key)20s: %(value)s" % locals())
if properties:
logger.debug(_(" %d custom properties..."),
len(properties))
for key, value in properties.items():
logger.debug(" %(key)20s: %(value)s" % locals())

@ -161,7 +161,7 @@ def schedule_delete_from_backend(uri, options, context, image_id, **kwargs):
use_delay = config.get_option(options, 'delayed_delete', type='bool',
default=False)
if not use_delay:
registry.update_image_metadata(options, context, image_id,
registry.update_image_metadata(context, image_id,
{'status': 'deleted'})
try:
return delete_from_backend(uri, **kwargs)
@ -186,5 +186,5 @@ def schedule_delete_from_backend(uri, options, context, image_id, **kwargs):
os.chmod(file_path, 0600)
os.utime(file_path, (delete_time, delete_time))
registry.update_image_metadata(options, context, image_id,
registry.update_image_metadata(context, image_id,
{'status': 'pending_delete'})

@ -146,6 +146,8 @@ class ApiServer(Server):
super(ApiServer, self).__init__(test_dir, port)
self.server_name = 'api'
self.default_store = 'file'
self.key_file = ""
self.cert_file = ""
self.image_dir = os.path.join(self.test_dir,
"images")
self.pid_file = os.path.join(self.test_dir,
@ -177,6 +179,8 @@ filesystem_store_datadir=%(image_dir)s
default_store = %(default_store)s
bind_host = 0.0.0.0
bind_port = %(bind_port)s
key_file = %(key_file)s
cert_file = %(cert_file)s
registry_host = 0.0.0.0
registry_port = %(registry_port)s
log_file = %(log_file)s
@ -306,6 +310,7 @@ class FunctionalTest(unittest.TestCase):
self.test_id = random.randint(0, 100000)
self.test_dir = os.path.join("/", "tmp", "test.%d" % self.test_id)
self.api_protocol = 'http'
self.api_port = get_unused_port()
self.registry_port = get_unused_port()

File diff suppressed because it is too large Load Diff

@ -19,6 +19,7 @@ import datetime
import json
import os
import StringIO
import tempfile
import unittest
import stubout
@ -48,6 +49,78 @@ class TestBadClients(unittest.TestCase):
c.get_image,
1)
def test_ssl_no_key_file(self):
"""
Test that when doing SSL connection, a key file is
required
"""
try:
c = client.Client("0.0.0.0", use_ssl=True)
except exception.ClientConnectionError:
return
self.fail("Did not raise ClientConnectionError")
def test_ssl_non_existing_key_file(self):
"""
Test that when doing SSL connection, a key file is
required to exist
"""
try:
c = client.Client("0.0.0.0", use_ssl=True,
key_file='nonexistingfile')
except exception.ClientConnectionError:
return
self.fail("Did not raise ClientConnectionError")
def test_ssl_no_cert_file(self):
"""
Test that when doing SSL connection, a cert file is
required
"""
try:
with tempfile.NamedTemporaryFile() as key_file:
key_file.write("bogus")
key_file.flush()
c = client.Client("0.0.0.0", use_ssl=True,
key_file=key_file.name)
except exception.ClientConnectionError:
return
self.fail("Did not raise ClientConnectionError")
def test_ssl_non_existing_cert_file(self):
"""
Test that when doing SSL connection, a cert file is
required to exist
"""
try:
with tempfile.NamedTemporaryFile() as key_file:
key_file.write("bogus")
key_file.flush()
c = client.Client("0.0.0.0", use_ssl=True,
key_file=key_file.name,
cert_file='nonexistingfile')
except exception.ClientConnectionError:
return
self.fail("Did not raise ClientConnectionError")
def test_ssl_optional_ca_file(self):
"""
Test that when doing SSL connection, a cert file and key file are
required to exist, but a CA file is optional.
"""
try:
with tempfile.NamedTemporaryFile() as key_file:
key_file.write("bogus")
key_file.flush()
with tempfile.NamedTemporaryFile() as cert_file:
cert_file.write("bogus")
cert_file.flush()
c = client.Client("0.0.0.0", use_ssl=True,
key_file=key_file.name,
cert_file=cert_file.name)
except exception.ClientConnectionError:
self.fail("Raised ClientConnectionError when it should not")
class TestRegistryClient(unittest.TestCase):

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC4DCCAcigAwIBAgIBATANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDEwhNeVRl
c3RDQTAeFw0xMTA3MjExNTA1NDZaFw0xMjA3MjAxNTA1NDZaMCMxEDAOBgNVBAMT
B2FobWFkcGMxDzANBgNVBAoTBnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAO9zpczf+W4DoK2z8oFbsZfbvz1y/yQOnrQYvb1zv1IieT+QA+Ti
N64N/sgR/cR7YEIXDnhij8yE1JTWMk1W6g4m7TGacUMXD/WAcsTM7kRol/FVksdn
F51qxCYqWUPQ3xiTfBg2SJWvJCUGowvz06xh8JeOEXLbALC5xrzrM3hclpdbrKYE
oe8kikI/K0TKpu52VJJrTBGPHMsw+eIqL2Ix5pWHh7DPfjBiiG7khsJxN7xSqLbX
LrhDi24nTM9pndaqABkmPYQ9qd11SoAUB82QAAGj8A7iR/DnAzAfJl1usvQp+Me6
sR3TPY27zifBbD04tiROi1swM/1xRH7qOpkCAwEAAaMvMC0wCQYDVR0TBAIwADAL
BgNVHQ8EBAMCBSAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEFBQAD
ggEBAIJvnQjkEDFvLT7NiyFrO938BuxdQH2mX2N7Fz86myZLcGpr5NCdLvT9tD9f
6KqrR8e839pYVPZY80cBpGTmRmzW3xLsmGCFHPHt4p1tkqSP1R5iLzKDe8jawHhD
sch8P9URRhW9ZgBzA4xiv9FnIxZ70uDr04uX/sR/j41HGBS8YW6dJvr9Y2SpGqSS
rR2btnNZ945dau6CPLRNd9Fls3Qjx03PnsmZ5ikSuV0pT1sPQmhhw7rBYV/b2ff+
z/4cRtZrR00NVc74IEXLoujIjUUpFC83in10PKQmAvKYTeTdXns48eC4Cwqe8eaM
N0YtxqQvSTsUo6vPM28NR99Fbow=
-----END CERTIFICATE-----

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA73OlzN/5bgOgrbPygVuxl9u/PXL/JA6etBi9vXO/UiJ5P5AD
5OI3rg3+yBH9xHtgQhcOeGKPzITUlNYyTVbqDibtMZpxQxcP9YByxMzuRGiX8VWS
x2cXnWrEJipZQ9DfGJN8GDZIla8kJQajC/PTrGHwl44RctsAsLnGvOszeFyWl1us
pgSh7ySKQj8rRMqm7nZUkmtMEY8cyzD54iovYjHmlYeHsM9+MGKIbuSGwnE3vFKo
ttcuuEOLbidMz2md1qoAGSY9hD2p3XVKgBQHzZAAAaPwDuJH8OcDMB8mXW6y9Cn4
x7qxHdM9jbvOJ8FsPTi2JE6LWzAz/XFEfuo6mQIDAQABAoIBAQC6BwvBbiQXH0Re
jtWRQA5p3zPk5olnluAfJLWMEPeLNPMjuZv83u7JD2BoSOnxErTGw6jfSBtVlcCd
3Qb5ZNOzqPRPvB/QMoOYhHElidx2UxfwSz4cInCLQJ4g1HfDIuuf6TzYhpu/hnC7
Pzu+lnBVlUVYSOwvYgtYQQwwSz4Se8Mwoh2OOOTgn4wvZDbiDrMvv2UUUL1nyvAB
FdaywbD/dW8TqbnPSoj8uipq0yugDOyzzNQDM6+rN69qNrD2/vYaAsSaWxISLDqs
fEI4M1+PeDmLigQeA7V3kEZWWDwHbS92LL8BxEmmeeHN5xwZyC8xqa1jt2A/S6Af
Q7gkpG6BAoGBAP+jFn7HCCi/Lc+YEZO0km7fvfR48M6QW3ar+b1aQywJWJhbtU9E
eoX1IcLxgce3+mUO05hGz3Rvz5JSDbmWXd6GTVsMRZqJeeCKbw9xirp5i4JjLzc8
Vu2oOJhqtAa88FgpZJ3iPIrT38UBpmnrvv1nb2ZNMdZnTNhtj5WByLFpAoGBAO/K
rVuvMq370P69Lo+iAr6+t4vwpF6pC/06B+OT5vldoiF57dOjFwndAKs9HCk9rS0/
jTvo0a1tS9yU20cFLXMN98zho3BYs4BlEKpNwVmpopxcfGV6dbwka7delAEVZzyN
TDW2P5Gyq9sYys+2ldvT2zTK8hHXZSh5JAp3V+mxAoGAC6G6Fk6sGl6IkReURSpE
N3NKy2LtYhjDcKTmmi0PPWO3ekdB+rdc89dxj9M5WoMOi6afDiC6s8uaoEfHhBhJ
cSSfRHNMf3md6A+keglqjI2XQXmN3m+KbQnoeVbxlhTmwrwvbderdY2qcuZeUhd9
+z3HndoJWH4eywJBNEZRgXECgYEAjtTeEDe6a1IMuj/7xQiOtAmsEQolDlGJV6vC
WTeXJEA2u9QB6sdBiNmAdX9wD8yyI7qwKNhUVQY+YsS0HIij+t1+FibtEJV1Tmxk
0dyA6CSYPKUGX/fiu0/CbbZDWKXkGXhcxb2p/eI8ZcRNwg4TE58M+lRMfn4bvlDy
O928mvECgYEA18MfGUZENZmC9ismsqrr9uVevfB08U5b+KRjSOyI2ZwOXnzcvbc3
zt9Tp35bcpQMAxPVT2B5htXeXqhUAJMkFEajpNZGDEKlCRB2XvMeA1Dn5fSk2dBB
ADeqQczoXT2+VgXLxRJJPucYCzi3kzo0OBUsHc9Z/HZNyr8LrUgd5lI=
-----END RSA PRIVATE KEY-----