Merge "Adds SSL configuration params to the client"
This commit is contained in:
commit
d3a31b372e
@ -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…
Reference in New Issue
Block a user