Merge "Move SynchronousAPIBase to a generalized location"

This commit is contained in:
Zuul 2020-08-29 01:45:04 +00:00 committed by Gerrit Code Review
commit 5e9a14aee1
2 changed files with 215 additions and 134 deletions

View File

@ -34,9 +34,13 @@ import socket
import subprocess
import sys
import tempfile
import textwrap
import time
from unittest import mock
import uuid
import fixtures
import glance_store
from os_win import utilsfactory as os_win_utilsfactory
from oslo_config import cfg
from oslo_serialization import jsonutils
@ -44,8 +48,11 @@ from oslo_serialization import jsonutils
from six.moves import range
import six.moves.urllib.parse as urlparse
import testtools
import webob
from glance.common import config
from glance.common import utils
from glance.common import wsgi
from glance.db.sqlalchemy import api as db_api
from glance import tests as glance_tests
from glance.tests import utils as test_utils
@ -1467,3 +1474,209 @@ class MultipleBackendFunctionalTest(test_utils.BaseTestCase):
self._attached_server_logs.append(s.log_file)
self.addDetail(
s.server_name, testtools.content.text_content(s.dump_log()))
class SynchronousAPIBase(test_utils.BaseTestCase):
"""A base class that provides synchronous calling into the API.
This provides a way to directly call into the API WSGI stack
without starting a separate server, and with a simple paste
pipeline. Configured with multi-store and a real database.
This differs from the FunctionalTest lineage above in that they
start a full copy of the API server as a separate process, whereas
this calls directly into the WSGI stack. This test base is
appropriate for situations where you need to be able to mock the
state of the world (i.e. warp time, or inject errors) but should
not be used for happy-path testing where FunctionalTest provides
more isolation.
To use this, inherit and run start_server() before you are ready
to make API calls (either in your setUp() or per-test if you need
to change config or mocking).
Once started, use the api_get(), api_put(), api_post(), and
api_delete() methods to make calls to the API.
"""
TENANT = str(uuid.uuid4())
@mock.patch('oslo_db.sqlalchemy.enginefacade.writer.get_engine')
def setup_database(self, mock_get_engine):
"""Configure and prepare a fresh sqlite database."""
db_file = 'sqlite:///%s/test.db' % self.test_dir
self.config(connection=db_file, group='database')
# NOTE(danms): Make sure that we clear the current global
# database configuration, provision a temporary database file,
# and run migrations with our configuration to define the
# schema there.
db_api.clear_db_env()
engine = db_api.get_engine()
mock_get_engine.return_value = engine
with mock.patch('logging.config'):
# NOTE(danms): The alembic config in the env module will break our
# BaseTestCase logging setup. So mock that out to prevent it while
# we db_sync.
test_utils.db_sync(engine=engine)
def setup_simple_paste(self):
"""Setup a very simple no-auth paste pipeline.
This configures the API to be very direct, including only the
middleware absolutely required for consistent API calls.
"""
self.paste_config = os.path.join(self.test_dir, 'glance-api-paste.ini')
with open(self.paste_config, 'w') as f:
f.write(textwrap.dedent("""
[filter:context]
paste.filter_factory = glance.api.middleware.context:\
ContextMiddleware.factory
[filter:fakeauth]
paste.filter_factory = glance.tests.utils:\
FakeAuthMiddleware.factory
[pipeline:glance-api]
pipeline = context rootapp
[composite:rootapp]
paste.composite_factory = glance.api:root_app_factory
/v2: apiv2app
[app:apiv2app]
paste.app_factory = glance.api.v2.router:API.factory
"""))
def _store_dir(self, store):
return os.path.join(self.test_dir, store)
def setup_stores(self):
"""Configures multiple backend stores.
This configures the API with two file-backed stores (store1
and store2) as well as a os_glance_staging_store for imports.
"""
self.config(enabled_backends={'store1': 'file', 'store2': 'file'})
glance_store.register_store_opts(CONF,
reserved_stores=wsgi.RESERVED_STORES)
self.config(default_backend='store1',
group='glance_store')
self.config(filesystem_store_datadir=self._store_dir('store1'),
group='store1')
self.config(filesystem_store_datadir=self._store_dir('store2'),
group='store2')
self.config(filesystem_store_datadir=self._store_dir('staging'),
group='os_glance_staging_store')
glance_store.create_multi_stores(CONF,
reserved_stores=wsgi.RESERVED_STORES)
glance_store.verify_store()
def setUp(self):
super(SynchronousAPIBase, self).setUp()
self.setup_database()
self.setup_simple_paste()
self.setup_stores()
def start_server(self):
"""Builds and "starts" the API server.
Note that this doesn't actually "start" anything like
FunctionalTest does above, but that terminology is used here
to make it seem like the same sort of pattern.
"""
config.set_config_defaults()
self.api = config.load_paste_app('glance-api',
conf_file=self.paste_config)
def _headers(self, custom_headers=None):
base_headers = {
'X-Identity-Status': 'Confirmed',
'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
'X-Tenant-Id': self.TENANT,
'Content-Type': 'application/json',
'X-Roles': 'admin',
}
base_headers.update(custom_headers or {})
return base_headers
def api_request(self, method, url, headers=None, data=None,
json=None, body_file=None):
"""Perform a request against the API.
NOTE: Most code should use api_get(), api_post(), api_put(),
or api_delete() instead!
:param method: The HTTP method to use (i.e. GET, POST, etc)
:param url: The *path* part of the URL to call (i.e. /v2/images)
:param headers: Optional updates to the default set of headers
:param data: Optional bytes data payload to send (overrides @json)
:param json: Optional dict structure to be jsonified and sent as
the payload (mutually exclusive with @data)
:param body_file: Optional io.IOBase to provide as the input data
stream for the request (overrides @data)
:returns: A webob.Response object
"""
headers = self._headers(headers)
req = webob.Request.blank(url, method=method,
headers=headers)
if json and not data:
data = jsonutils.dumps(json).encode()
if data and not body_file:
req.body = data
elif body_file:
req.body_file = body_file
return self.api(req)
def api_get(self, url, headers=None):
"""Perform a GET request against the API.
:param url: The *path* part of the URL to call (i.e. /v2/images)
:param headers: Optional updates to the default set of headers
:returns: A webob.Response object
"""
return self.api_request('GET', url, headers=headers)
def api_post(self, url, headers=None, data=None, json=None,
body_file=None):
"""Perform a POST request against the API.
:param url: The *path* part of the URL to call (i.e. /v2/images)
:param headers: Optional updates to the default set of headers
:param data: Optional bytes data payload to send (overrides @json)
:param json: Optional dict structure to be jsonified and sent as
the payload (mutually exclusive with @data)
:param body_file: Optional io.IOBase to provide as the input data
stream for the request (overrides @data)
:returns: A webob.Response object
"""
return self.api_request('POST', url, headers=headers,
data=data, json=json,
body_file=body_file)
def api_put(self, url, headers=None, data=None, json=None, body_file=None):
"""Perform a PUT request against the API.
:param url: The *path* part of the URL to call (i.e. /v2/images)
:param headers: Optional updates to the default set of headers
:param data: Optional bytes data payload to send (overrides @json,
mutually exclusive with body_file)
:param json: Optional dict structure to be jsonified and sent as
the payload (mutually exclusive with @data)
:param body_file: Optional io.IOBase to provide as the input data
stream for the request (overrides @data)
:returns: A webob.Response object
"""
return self.api_request('PUT', url, headers=headers,
data=data, json=json,
body_file=body_file)
def api_delete(self, url, headers=None):
"""Perform a DELETE request against the API.
:param url: The *path* part of the URL to call (i.e. /v2/images)
:param headers: Optional updates to the default set of headers
:returns: A webob.Response object
"""
return self.api_request('DELETE', url, heaers=headers)

View File

@ -14,151 +14,19 @@
# under the License.
import datetime
import os
from testtools import content as ttc
import textwrap
import time
from unittest import mock
import uuid
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import fixture as time_fixture
from oslo_utils import units
import webob
from glance.common import config
from glance.common import wsgi
import glance.db.sqlalchemy.api
from glance.tests import functional
from glance.tests import utils as test_utils
import glance_store
LOG = logging.getLogger(__name__)
TENANT1 = str(uuid.uuid4())
CONF = cfg.CONF
class SynchronousAPIBase(test_utils.BaseTestCase):
"""A test base class that provides synchronous calling into the API
without starting a separate server, and with a simple paste
pipeline. Configured with multi-store and a real database.
"""
@mock.patch('oslo_db.sqlalchemy.enginefacade.writer.get_engine')
def setup_database(self, mock_get_engine):
db_file = 'sqlite:///%s/test-%s.db' % (self.test_dir,
uuid.uuid4())
self.config(connection=db_file, group='database')
# NOTE(danms): Make sure that we clear the current global
# database configuration, provision a temporary database file,
# and run migrations with our configuration to define the
# schema there.
glance.db.sqlalchemy.api.clear_db_env()
engine = glance.db.sqlalchemy.api.get_engine()
mock_get_engine.return_value = engine
with mock.patch('logging.config'):
# NOTE(danms): The alembic config in the env module will break our
# BaseTestCase logging setup. So mock that out to prevent it while
# we db_sync.
test_utils.db_sync(engine=engine)
def setup_simple_paste(self):
self.paste_config = os.path.join(self.test_dir, 'glance-api-paste.ini')
with open(self.paste_config, 'w') as f:
f.write(textwrap.dedent("""
[filter:context]
paste.filter_factory = glance.api.middleware.context:\
ContextMiddleware.factory
[filter:fakeauth]
paste.filter_factory = glance.tests.utils:\
FakeAuthMiddleware.factory
[pipeline:glance-api]
pipeline = context rootapp
[composite:rootapp]
paste.composite_factory = glance.api:root_app_factory
/v2: apiv2app
[app:apiv2app]
paste.app_factory = glance.api.v2.router:API.factory
"""))
def _store_dir(self, store):
return os.path.join(self.test_dir, store)
def setup_stores(self):
self.config(enabled_backends={'store1': 'file', 'store2': 'file'})
glance_store.register_store_opts(CONF,
reserved_stores=wsgi.RESERVED_STORES)
self.config(default_backend='store1',
group='glance_store')
self.config(filesystem_store_datadir=self._store_dir('store1'),
group='store1')
self.config(filesystem_store_datadir=self._store_dir('store2'),
group='store2')
self.config(filesystem_store_datadir=self._store_dir('staging'),
group='os_glance_staging_store')
glance_store.create_multi_stores(CONF,
reserved_stores=wsgi.RESERVED_STORES)
glance_store.verify_store()
def setUp(self):
super(SynchronousAPIBase, self).setUp()
self.setup_database()
self.setup_simple_paste()
self.setup_stores()
def start_server(self):
config.set_config_defaults()
self.api = config.load_paste_app('glance-api',
conf_file=self.paste_config)
def _headers(self, custom_headers=None):
base_headers = {
'X-Identity-Status': 'Confirmed',
'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
'X-Tenant-Id': TENANT1,
'Content-Type': 'application/json',
'X-Roles': 'admin',
}
base_headers.update(custom_headers or {})
return base_headers
def api_get(self, url, headers=None):
headers = self._headers(headers)
req = webob.Request.blank(url, method='GET',
headers=headers)
return self.api(req)
def api_post(self, url, data=None, json=None, headers=None):
headers = self._headers(headers)
req = webob.Request.blank(url, method='POST',
headers=headers)
if json and not data:
data = jsonutils.dumps(json).encode()
headers['Content-Type'] = 'application/json'
if data:
req.body = data
LOG.debug(req.as_bytes())
return self.api(req)
def api_put(self, url, data=None, json=None, headers=None, body_file=None):
headers = self._headers(headers)
req = webob.Request.blank(url, method='PUT',
headers=headers)
if json and not data:
data = jsonutils.dumps(json).encode()
if data and not body_file:
req.body = data
elif body_file:
req.body_file = body_file
return self.api(req)
class TestImageImportLocking(SynchronousAPIBase):
class TestImageImportLocking(functional.SynchronousAPIBase):
def _import_copy(self, image_id, stores):
"""Do an import of image_id to the given stores."""
body = {'method': {'name': 'copy-image'},