Merge "Adds SSL configuration params to the client"

This commit is contained in:
Jenkins 2011-10-18 14:48:01 +00:00 committed by Gerrit Code Review
commit d3a31b372e
19 changed files with 1757 additions and 118 deletions

View File

@ -980,8 +980,13 @@ def get_client(options):
auth_url=os.getenv('OS_AUTH_URL'), auth_url=os.getenv('OS_AUTH_URL'),
strategy=os.getenv('OS_AUTH_STRATEGY', 'noauth')) 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, 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): def create_options(parser):

View File

@ -63,7 +63,7 @@ if __name__ == '__main__':
conf, app = config.load_paste_app('glance-api', options, args) conf, app = config.load_paste_app('glance-api', options, args)
server = wsgi.Server() 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() server.wait()
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)

View File

@ -63,7 +63,7 @@ if __name__ == '__main__':
conf, app = config.load_paste_app('glance-registry', options, args) conf, app = config.load_paste_app('glance-registry', options, args)
server = wsgi.Server() 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() server.wait()
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)

View File

@ -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, if you are starting up the API server, ``glance-api.conf`` is searched for,
otherwise ``glance-registry.conf``. 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 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 `logging module <http://docs.python.org/library/logging.html>`_ documentation for
more information on setting this format string. more information on setting this format string.
* ``log_use_syslog``
Use syslog logging functionality.
Defaults to False.
Configuring Glance Storage Backends Configuring Glance Storage Backends
----------------------------------- -----------------------------------

View File

@ -16,12 +16,6 @@ bind_host = 0.0.0.0
# Port the bind the API server to # Port the bind the API server to
bind_port = 9292 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 # Log to this file. Make sure you do not set the same log
# file for both the API and registry servers! # file for both the API and registry servers!
log_file = /var/log/glance/api.log 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` # Send logs to syslog (/dev/log) instead of to file specified by `log_file`
use_syslog = False 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 ===================== # ============ Notification System Options =====================
# Notifications can be sent when images are create, updated or deleted. # Notifications can be sent when images are create, updated or deleted.

View File

@ -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` # Send logs to syslog (/dev/log) instead of to file specified by `log_file`
use_syslog = False use_syslog = False
# Backlog requests when creating socket
backlog = 4096
# SQLAlchemy connection string for the reference implementation # SQLAlchemy connection string for the reference implementation
# registry server. Any valid SQLAlchemy connection string is fine. # registry server. Any valid SQLAlchemy connection string is fine.
# See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine # 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` # default to `limit_param_default`
limit_param_default = 25 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:glance-registry]
pipeline = context registryapp pipeline = context registryapp
# NOTE: use the following pipeline for keystone # NOTE: use the following pipeline for keystone

View File

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

View File

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

View File

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

View File

@ -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 httplib
import logging import logging
import socket import os
import urllib import urllib
import urlparse import urlparse
from eventlet.green import socket, ssl
# See http://code.google.com/p/python-nose/issues/detail?id=373 # See http://code.google.com/p/python-nose/issues/detail?id=373
# The code below enables glance.client standalone to work with i18n _() blocks # The code below enables glance.client standalone to work with i18n _() blocks
import __builtin__ import __builtin__
@ -43,6 +66,48 @@ class ImageBodyIterator(object):
break 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): class BaseClient(object):
"""A base client class""" """A base client class"""
@ -52,7 +117,8 @@ class BaseClient(object):
DEFAULT_DOC_ROOT = None DEFAULT_DOC_ROOT = None
def __init__(self, host, port=None, use_ssl=False, auth_tok=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. 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 auth_tok: The auth token to pass to the server
:param creds: The credentials to pass to the auth plugin :param creds: The credentials to pass to the auth plugin
:param doc_root: Prefix for all URLs we request from host :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.host = host
self.port = port or self.DEFAULT_PORT self.port = port or self.DEFAULT_PORT
@ -69,8 +152,48 @@ class BaseClient(object):
self.auth_tok = auth_tok self.auth_tok = auth_tok
self.creds = creds or {} self.creds = creds or {}
self.connection = None 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.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): def set_auth_token(self, auth_tok):
""" """
@ -112,7 +235,7 @@ class BaseClient(object):
Returns the proper connection type Returns the proper connection type
""" """
if self.use_ssl: if self.use_ssl:
return httplib.HTTPSConnection return HTTPSClientAuthConnection
else: else:
return httplib.HTTPConnection return httplib.HTTPConnection
@ -186,7 +309,7 @@ class BaseClient(object):
if 'x-auth-token' not in headers and self.auth_tok: if 'x-auth-token' not in headers and self.auth_tok:
headers['x-auth-token'] = 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: if self.doc_root:
action = '/'.join([self.doc_root, action.lstrip('/')]) action = '/'.join([self.doc_root, action.lstrip('/')])

View File

@ -107,6 +107,11 @@ class InvalidContentType(GlanceException):
message = _("Invalid content type %(content_type)s") 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): class BadStoreConfiguration(GlanceException):
message = _("Store %(store_name)s could not be configured correctly. " message = _("Store %(store_name)s could not be configured correctly. "
"Reason: %(reason)s") "Reason: %(reason)s")
@ -122,4 +127,4 @@ class StoreAddDisabled(GlanceException):
class InvalidNotifierStrategy(GlanceException): class InvalidNotifierStrategy(GlanceException):
message = "'%(strategy)s' is not an available notifier strategy." message = _("'%(strategy)s' is not an available notifier strategy.")

View File

@ -21,14 +21,15 @@
Utility methods for working with WSGI servers Utility methods for working with WSGI servers
""" """
import datetime
import json import json
import logging import logging
import sys import sys
import datetime import time
import eventlet import eventlet
from eventlet.green import socket, ssl
import eventlet.wsgi import eventlet.wsgi
eventlet.patcher.monkey_patch(all=False, socket=True)
import routes import routes
import routes.middleware import routes.middleware
import webob.dec import webob.dec
@ -48,10 +49,57 @@ class WritableLogger(object):
self.logger.log(self.level, msg.strip("\n")) self.logger.log(self.level, msg.strip("\n"))
def run_server(application, port): def get_socket(host, port, conf):
"""Run a WSGI server with the given application.""" """
sock = eventlet.listen(('0.0.0.0', port)) Bind socket to bind ip:port in conf
eventlet.wsgi.server(sock, application)
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): class Server(object):
@ -60,9 +108,17 @@ class Server(object):
def __init__(self, threads=1000): def __init__(self, threads=1000):
self.pool = eventlet.GreenPool(threads) self.pool = eventlet.GreenPool(threads)
def start(self, application, port, host='0.0.0.0', backlog=128): def start(self, application, port, host='0.0.0.0', conf=None):
"""Run a WSGI server with the given application.""" """
socket = eventlet.listen((host, port), backlog=backlog) 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) self.pool.spawn_n(self._run, application, socket)
def wait(self): def wait(self):

View File

@ -21,103 +21,111 @@ Registry API
import logging import logging
from glance.common import config
from glance.common import exception
from glance.registry import client from glance.registry import client
logger = logging.getLogger('glance.registry') logger = logging.getLogger('glance.registry')
_CLIENT_HOST = None
def get_registry_client(options, context): _CLIENT_PORT = None
host = options['registry_host'] _CLIENT_KWARGS = {}
port = int(options['registry_port'])
return client.RegistryClient(host, port, auth_tok=context.auth_tok)
def get_images_list(options, context, **kwargs): def configure_registry_client(options):
c = get_registry_client(options, context) """
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) return c.get_images(**kwargs)
def get_images_detail(options, context, **kwargs): def get_images_detail(context, **kwargs):
c = get_registry_client(options, context) c = get_registry_client(context)
return c.get_images_detailed(**kwargs) return c.get_images_detailed(**kwargs)
def get_image_metadata(options, context, image_id): def get_image_metadata(context, image_id):
c = get_registry_client(options, context) c = get_registry_client(context)
return c.get_image(image_id) return c.get_image(image_id)
def add_image_metadata(options, context, image_meta): def add_image_metadata(context, image_meta):
if options['debug']: logger.debug(_("Adding image metadata..."))
logger.debug(_("Adding image metadata...")) c = get_registry_client(context)
_debug_print_metadata(image_meta) return c.add_image(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 update_image_metadata(options, context, image_id, image_meta, def update_image_metadata(context, image_id, image_meta,
purge_props=False): purge_props=False):
if options['debug']: logger.debug(_("Updating image metadata for image %s..."), image_id)
logger.debug(_("Updating image metadata for image %s..."), image_id) c = get_registry_client(context)
_debug_print_metadata(image_meta) return c.update_image(image_id, image_meta, purge_props)
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
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) 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) return c.delete_image(image_id)
def get_image_members(options, context, image_id): def get_image_members(context, image_id):
c = get_registry_client(options, context) c = get_registry_client(context)
return c.get_image_members(image_id) return c.get_image_members(image_id)
def get_member_images(options, context, member_id): def get_member_images(context, member_id):
c = get_registry_client(options, context) c = get_registry_client(context)
return c.get_member_images(member_id) return c.get_member_images(member_id)
def replace_members(options, context, image_id, member_data): def replace_members(context, image_id, member_data):
c = get_registry_client(options, context) c = get_registry_client(context)
return c.replace_members(image_id, member_data) return c.replace_members(image_id, member_data)
def add_member(options, context, image_id, member_id, can_share=None): def add_member(context, image_id, member_id, can_share=None):
c = get_registry_client(options, context) c = get_registry_client(context)
return c.add_member(image_id, member_id, can_share=can_share) return c.add_member(image_id, member_id, can_share=can_share)
def delete_member(options, context, image_id, member_id): def delete_member(context, image_id, member_id):
c = get_registry_client(options, context) c = get_registry_client(context)
return c.delete_member(image_id, member_id) 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())

View File

@ -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', use_delay = config.get_option(options, 'delayed_delete', type='bool',
default=False) default=False)
if not use_delay: if not use_delay:
registry.update_image_metadata(options, context, image_id, registry.update_image_metadata(context, image_id,
{'status': 'deleted'}) {'status': 'deleted'})
try: try:
return delete_from_backend(uri, **kwargs) 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.chmod(file_path, 0600)
os.utime(file_path, (delete_time, delete_time)) 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'}) {'status': 'pending_delete'})

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@ import datetime
import json import json
import os import os
import StringIO import StringIO
import tempfile
import unittest import unittest
import stubout import stubout
@ -48,6 +49,78 @@ class TestBadClients(unittest.TestCase):
c.get_image, c.get_image,
1) 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): class TestRegistryClient(unittest.TestCase):

View File

@ -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-----

View File

@ -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-----