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:
parent
53db059772
commit
eec5c1afa1
bin
doc/source
etc
glance
api
common
registry
store
tests
@ -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()
|
||||
|
||||
|
1208
glance/tests/functional/test_ssl.py
Normal file
1208
glance/tests/functional/test_ssl.py
Normal file
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):
|
||||
|
||||
|
18
glance/tests/var/certificate.crt
Normal file
18
glance/tests/var/certificate.crt
Normal 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-----
|
27
glance/tests/var/privatekey.key
Normal file
27
glance/tests/var/privatekey.key
Normal 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-----
|
Loading…
x
Reference in New Issue
Block a user