Skip to content

Commit ef475e1

Browse files
Joshua Harlowbcwaldon
Joshua Harlow
authored andcommitted
Implements blueprint import-dynamic-stores.
Make glance more pluggable with regard to stores. Change-Id: I7b264d1b047a321f7b60857bb73154f831b82a7b
1 parent b5bae3d commit ef475e1

22 files changed

+238
-176
lines changed

etc/glance-api.conf

+12-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@ verbose = True
55
# Show debugging output in logs (sets DEBUG log level output)
66
debug = False
77

8-
# Which backend store should Glance use by default is not specified
9-
# in a request to add a new image to Glance? Default: 'file'
10-
# Available choices are 'file', 'swift', and 's3'
8+
# Which backend scheme should Glance use by default is not specified
9+
# in a request to add a new image to Glance? Known schemes are determined
10+
# by the known_stores option below.
11+
# Default: 'file'
1112
default_store = file
1213

14+
# List of which store classes and store class locations are
15+
# currently known to glance at startup.
16+
known_stores = glance.store.filesystem.Store,
17+
glance.store.http.Store,
18+
glance.store.rbd.Store,
19+
glance.store.s3.Store,
20+
glance.store.swift.Store,
21+
1322
# Address to bind the API server
1423
bind_host = 0.0.0.0
1524

glance/api/v1/images.py

+27-32
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,8 @@
4141
from glance.common import wsgi
4242
from glance.common import utils
4343
from glance.openstack.common import cfg
44-
import glance.store
45-
import glance.store.filesystem
46-
import glance.store.http
47-
import glance.store.rbd
48-
import glance.store.s3
49-
import glance.store.swift
50-
from glance.store import (get_from_backend,
44+
from glance.store import (create_stores,
45+
get_from_backend,
5146
get_size_from_backend,
5247
schedule_delete_from_backend,
5348
get_store_from_location,
@@ -67,6 +62,10 @@
6762
# as a BigInteger.
6863
IMAGE_SIZE_CAP = 1 << 50
6964

65+
# Defined at module level due to _is_opt_registered
66+
# identity check (not equality).
67+
default_store_opt = cfg.StrOpt('default_store', default='file')
68+
7069

7170
class Controller(controller.BaseController):
7271
"""
@@ -87,13 +86,11 @@ class Controller(controller.BaseController):
8786
DELETE /images/<ID> -- Delete the image with id <ID>
8887
"""
8988

90-
default_store_opt = cfg.StrOpt('default_store', default='file')
91-
9289
def __init__(self, conf):
9390
self.conf = conf
94-
self.conf.register_opt(self.default_store_opt)
95-
glance.store.create_stores(conf)
96-
self.verify_store_or_exit(self.conf.default_store)
91+
self.conf.register_opt(default_store_opt)
92+
create_stores(self.conf)
93+
self.verify_scheme_or_exit(self.conf.default_store)
9794
self.notifier = notifier.Notifier(conf)
9895
registry.configure_registry_client(conf)
9996
self.policy = policy.Enforcer(conf)
@@ -333,8 +330,8 @@ def _upload(self, req, image_meta):
333330
"""
334331
Uploads the payload of the request to a backend store in
335332
Glance. If the `x-image-meta-store` header is set, Glance
336-
will attempt to use that store, if not, Glance will use the
337-
store set by the flag `default_store`.
333+
will attempt to use that scheme; if not, Glance will use the
334+
scheme set by the flag `default_store` to find the backing store.
338335
339336
:param req: The WSGI/Webob Request object
340337
:param image_meta: Mapping of metadata about image
@@ -367,18 +364,18 @@ def _upload(self, req, image_meta):
367364
"x-image-meta-size header"))
368365
image_size = 0
369366

370-
store_name = req.headers.get('x-image-meta-store',
371-
self.conf.default_store)
367+
scheme = req.headers.get('x-image-meta-store',
368+
self.conf.default_store)
372369

373-
store = self.get_store_or_400(req, store_name)
370+
store = self.get_store_or_400(req, scheme)
374371

375372
image_id = image_meta['id']
376373
logger.debug(_("Setting image %s to status 'saving'"), image_id)
377374
registry.update_image_metadata(req.context, image_id,
378375
{'status': 'saving'})
379376
try:
380377
logger.debug(_("Uploading image data for image %(image_id)s "
381-
"to %(store_name)s store"), locals())
378+
"to %(scheme)s store"), locals())
382379

383380
if image_size > IMAGE_SIZE_CAP:
384381
max_image_size = IMAGE_SIZE_CAP
@@ -750,41 +747,39 @@ def delete(self, req, id):
750747
else:
751748
self.notifier.info('image.delete', id)
752749

753-
def get_store_or_400(self, request, store_name):
750+
def get_store_or_400(self, request, scheme):
754751
"""
755752
Grabs the storage backend for the supplied store name
756753
or raises an HTTPBadRequest (400) response
757754
758755
:param request: The WSGI/Webob Request object
759-
:param store_name: The backend store name
756+
:param scheme: The backend store scheme
760757
761758
:raises HTTPNotFound if store does not exist
762759
"""
763760
try:
764-
return get_store_from_scheme(store_name)
761+
return get_store_from_scheme(scheme)
765762
except exception.UnknownScheme:
766-
msg = (_("Requested store %s not available on this Glance server")
767-
% store_name)
768-
logger.error(msg)
763+
msg = _("Store for scheme %s not found")
764+
logger.error(msg % scheme)
769765
raise HTTPBadRequest(msg, request=request,
770766
content_type='text/plain')
771767

772-
def verify_store_or_exit(self, store_name):
768+
def verify_scheme_or_exit(self, scheme):
773769
"""
774770
Verifies availability of the storage backend for the
775-
given store name or exits
771+
given scheme or exits
776772
777-
:param store_name: The backend store name
773+
:param scheme: The backend store scheme
778774
"""
779775
try:
780-
get_store_from_scheme(store_name)
776+
get_store_from_scheme(scheme)
781777
except exception.UnknownScheme:
782-
msg = (_("Default store %s not available on this Glance server\n")
783-
% store_name)
784-
logger.error(msg)
778+
msg = _("Store for scheme %s not found")
779+
logger.error(msg % scheme)
785780
# message on stderr will only be visible if started directly via
786781
# bin/glance-api, as opposed to being daemonized by glance-control
787-
sys.stderr.write(msg)
782+
sys.stderr.write(msg % scheme)
788783
sys.exit(255)
789784

790785

glance/api/v2/image_data.py

-5
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@
2020
from glance.common import wsgi
2121
import glance.registry.db.api
2222
import glance.store
23-
import glance.store.filesystem
24-
import glance.store.http
25-
import glance.store.rbd
26-
import glance.store.s3
27-
import glance.store.swift
2823

2924

3025
class ImageDataController(base.Controller):

glance/store/__init__.py

+48-33
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@
2828

2929
logger = logging.getLogger('glance.store')
3030

31-
# Set of known store modules
32-
REGISTERED_STORE_MODULES = []
33-
3431
# Set of store objects, constructed in create_stores()
3532
STORES = {}
3633

@@ -128,50 +125,68 @@ def __len__(self):
128125
return self.size
129126

130127

131-
def register_store(store_module, schemes):
132-
"""
133-
Registers a store module and a set of schemes
134-
for which a particular URI request should be routed.
135-
136-
:param store_module: String representing the store module
137-
:param schemes: List of strings representing schemes for
138-
which this store should be used in routing
139-
"""
128+
def _get_store_class(store_entry):
129+
store_cls = None
140130
try:
141-
utils.import_class(store_module + '.Store')
131+
logger.debug("Attempting to import store %s", store_entry)
132+
store_cls = utils.import_class(store_entry)
142133
except exception.NotFound:
143-
raise BackendException('Unable to register store. Could not find '
144-
'a class named Store in module %s.'
145-
% store_module)
146-
REGISTERED_STORE_MODULES.append(store_module)
147-
scheme_map = {}
148-
for scheme in schemes:
149-
scheme_map[scheme] = store_module
150-
location.register_scheme_map(scheme_map)
134+
raise BackendException('Unable to load store. '
135+
'Could not find a class named %s.'
136+
% store_entry)
137+
return store_cls
138+
139+
140+
known_stores_opt = cfg.ListOpt('known_stores',
141+
default=('glance.store.filesystem.Store',))
151142

152143

153144
def create_stores(conf):
154145
"""
155-
Construct the store objects with supplied configuration options
146+
Registers all store modules and all schemes
147+
from the given config. Duplicates are not re-registered.
156148
"""
157-
for store_module in REGISTERED_STORE_MODULES:
158-
try:
159-
store_class = utils.import_class(store_module + '.Store')
160-
except exception.NotFound:
161-
raise BackendException('Unable to create store. Could not find '
162-
'a class named Store in module %s.'
163-
% store_module)
164-
STORES[store_module] = store_class(conf)
149+
conf.register_opt(known_stores_opt)
150+
store_count = 0
151+
for store_entry in conf.known_stores:
152+
store_entry = store_entry.strip()
153+
if not store_entry:
154+
continue
155+
store_cls = _get_store_class(store_entry)
156+
store_instance = store_cls(conf)
157+
schemes = store_instance.get_schemes()
158+
if not schemes:
159+
raise BackendException('Unable to register store %s. '
160+
'No schemes associated with it.'
161+
% store_cls)
162+
else:
163+
if store_cls not in STORES:
164+
logger.debug("Registering store %s with schemes %s",
165+
store_cls, schemes)
166+
STORES[store_cls] = store_instance
167+
scheme_map = {}
168+
for scheme in schemes:
169+
loc_cls = store_instance.get_store_location_class()
170+
scheme_map[scheme] = {
171+
'store_class': store_cls,
172+
'location_class': loc_cls,
173+
}
174+
location.register_scheme_map(scheme_map)
175+
store_count += 1
176+
else:
177+
logger.debug("Store %s already registered", store_cls)
178+
return store_count
165179

166180

167181
def get_store_from_scheme(scheme):
168182
"""
169183
Given a scheme, return the appropriate store object
170-
for handling that scheme
184+
for handling that scheme.
171185
"""
172-
if scheme not in location.SCHEME_TO_STORE_MAP:
186+
if scheme not in location.SCHEME_TO_CLS_MAP:
173187
raise exception.UnknownScheme(scheme=scheme)
174-
return STORES[location.SCHEME_TO_STORE_MAP[scheme]]
188+
scheme_info = location.SCHEME_TO_CLS_MAP[scheme]
189+
return STORES[scheme_info['store_class']]
175190

176191

177192
def get_store_from_uri(uri):

glance/store/base.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import logging
2121

2222
from glance.common import exception
23+
from glance.common import utils
2324

2425
logger = logging.getLogger('glance.store.base')
2526

@@ -35,7 +36,7 @@ def __init__(self, conf):
3536
:param conf: Optional dictionary of configuration options
3637
"""
3738
self.conf = conf
38-
39+
self.store_location_class = None
3940
self.configure()
4041

4142
try:
@@ -54,6 +55,22 @@ def configure(self):
5455
"""
5556
pass
5657

58+
def get_schemes(self):
59+
"""
60+
Returns a tuple of schemes which this store can handle.
61+
"""
62+
raise NotImplementedError
63+
64+
def get_store_location_class(self):
65+
"""
66+
Returns the store location class that is used by this store.
67+
"""
68+
if not self.store_location_class:
69+
class_name = "%s.StoreLocation" % (self.__module__)
70+
logger.debug("Late loading location class %s", class_name)
71+
self.store_location_class = utils.import_class(class_name)
72+
return self.store_location_class
73+
5774
def configure_add(self):
5875
"""
5976
This is like `configure` except that it's specifically for

glance/store/filesystem.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ class Store(glance.store.base.Store):
9898

9999
datadir_opt = cfg.StrOpt('filesystem_store_datadir')
100100

101+
def get_schemes(self):
102+
return ('file', 'filesystem')
103+
101104
def configure_add(self):
102105
"""
103106
Configure the Store to use the stored configuration options
@@ -216,6 +219,3 @@ def add(self, image_id, image_file, image_size):
216219
logger.debug(_("Wrote %(bytes_written)d bytes to %(filepath)s with "
217220
"checksum %(checksum_hex)s") % locals())
218221
return ('file://%s' % filepath, bytes_written, checksum_hex)
219-
220-
221-
glance.store.register_store(__name__, ['filesystem', 'file'])

glance/store/http.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ def another(self):
126126

127127
return (ResponseIndexable(iterator, content_length), content_length)
128128

129+
def get_schemes(self):
130+
return ('http', 'https')
131+
129132
def get_size(self, location):
130133
"""
131134
Takes a `glance.store.location.Location` object that indicates
@@ -155,6 +158,3 @@ def _get_conn_class(self, loc):
155158
"""
156159
return {'http': httplib.HTTPConnection,
157160
'https': httplib.HTTPSConnection}[loc.scheme]
158-
159-
160-
glance.store.register_store(__name__, ['http', 'https'])

0 commit comments

Comments
 (0)