1# Copyright 2018 RedHat Inc.
2# All Rights Reserved.
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15
16import copy
17import hashlib
18import logging
19
20from oslo_config import cfg
21from oslo_utils import encodeutils
22from oslo_utils import units
23import six
24from stevedore import driver
25from stevedore import extension
26
27from glance_store import capabilities
28from glance_store import exceptions
29from glance_store.i18n import _
30from glance_store import location
31
32
33CONF = cfg.CONF
34LOG = logging.getLogger(__name__)
35
36_STORE_OPTS = [
37    cfg.StrOpt('default_backend',
38               help=_("""
39The store identifier for the default backend in which data will be
40stored.
41
42The value must be defined as one of the keys in the dict defined
43by the ``enabled_backends`` configuration option in the DEFAULT
44configuration group.
45
46If a value is not defined for this option:
47
48* the consuming service may refuse to start
49* store_add calls that do not specify a specific backend will
50  raise a ``glance_store.exceptions.UnknownScheme`` exception
51
52Related Options:
53    * enabled_backends
54
55""")),
56]
57
58FS_CONF_DATADIR_HELP = """
59Directory of which the reserved store {} uses.
60
61Possible values:
62    * A valid path to a directory
63
64Refer to [glance_store]/filesystem store config opts for more details.
65"""
66
67FS_CONF_CHUNKSIZE_HELP = """
68Chunk size, in bytes to be used by reserved store {}.
69
70The chunk size used when reading or writing image files. Raising this value
71may improve the throughput but it may also slightly increase the memory usage
72when handling a large number of requests.
73
74Possible Values:
75    * Any positive integer value
76
77"""
78
79
80_STORE_CFG_GROUP = 'glance_store'
81_RESERVED_STORES = {}
82
83
84def _list_config_opts():
85    # NOTE(abhishekk): This separated approach could list
86    # store options before all driver ones, which easier
87    # to generate sampe config file.
88    driver_opts = _list_driver_opts()
89    sample_opts = [(_STORE_CFG_GROUP, _STORE_OPTS)]
90    for store_entry in driver_opts:
91        # NOTE(abhishekk): Do not include no_conf store
92        if store_entry == "no_conf":
93            continue
94        sample_opts.append((store_entry, driver_opts[store_entry]))
95
96    return sample_opts
97
98
99def _list_driver_opts():
100    driver_opts = {}
101    mgr = extension.ExtensionManager('glance_store.drivers')
102    # NOTE(zhiyan): Handle available drivers entry_points provided
103    # NOTE(nikhil): Return a sorted list of drivers to ensure that the sample
104    # configuration files generated by oslo config generator retain the order
105    # in which the config opts appear across different runs. If this order of
106    # config opts is not preserved, some downstream packagers may see a long
107    # diff of the changes though not relevant as only order has changed. See
108    # some more details at bug 1619487.
109    drivers = sorted([ext.name for ext in mgr])
110    handled_drivers = []  # Used to handle backwards-compatible entries
111    for store_entry in drivers:
112        driver_cls = _load_multi_store(None, store_entry, False)
113        if driver_cls and driver_cls not in handled_drivers:
114            if getattr(driver_cls, 'OPTIONS', None) is not None:
115                driver_opts[store_entry] = driver_cls.OPTIONS
116            handled_drivers.append(driver_cls)
117
118    # NOTE(zhiyan): This separated approach could list
119    # store options before all driver ones, which easier
120    # to read and configure by operator.
121    return driver_opts
122
123
124def register_store_opts(conf, reserved_stores=None):
125    LOG.debug("Registering options for group %s", _STORE_CFG_GROUP)
126    conf.register_opts(_STORE_OPTS, group=_STORE_CFG_GROUP)
127
128    configured_backends = copy.deepcopy(conf.enabled_backends)
129    if reserved_stores:
130        conf.enabled_backends.update(reserved_stores)
131        for key in reserved_stores.keys():
132            fs_conf_template = [
133                cfg.StrOpt('filesystem_store_datadir',
134                           default='/var/lib/glance/{}'.format(key),
135                           help=FS_CONF_DATADIR_HELP.format(key)),
136                cfg.MultiStrOpt('filesystem_store_datadirs',
137                                help="""Not used"""),
138                cfg.StrOpt('filesystem_store_metadata_file',
139                           help="""Not used"""),
140                cfg.IntOpt('filesystem_store_file_perm',
141                           default=0,
142                           help="""Not used"""),
143                cfg.IntOpt('filesystem_store_chunk_size',
144                           default=64 * units.Ki,
145                           min=1,
146                           help=FS_CONF_CHUNKSIZE_HELP.format(key)),
147                cfg.BoolOpt('filesystem_thin_provisioning',
148                            default=False,
149                            help="""Not used""")]
150            LOG.debug("Registering options for reserved store: {}".format(key))
151            conf.register_opts(fs_conf_template, group=key)
152
153    driver_opts = _list_driver_opts()
154    for backend in configured_backends:
155        for opt_list in driver_opts:
156            if configured_backends[backend] not in opt_list:
157                continue
158
159            LOG.debug("Registering options for group %s", backend)
160            conf.register_opts(driver_opts[opt_list], group=backend)
161
162
163def _load_multi_store(conf, store_entry,
164                      invoke_load=True,
165                      backend=None):
166    if backend:
167        invoke_args = [conf, backend]
168    else:
169        invoke_args = [conf]
170    try:
171        LOG.debug("Attempting to import store %s", store_entry)
172        mgr = driver.DriverManager('glance_store.drivers',
173                                   store_entry,
174                                   invoke_args=invoke_args,
175                                   invoke_on_load=invoke_load)
176        return mgr.driver
177    except RuntimeError as e:
178        LOG.warning("Failed to load driver %(driver)s. The "
179                    "driver will be disabled", dict(driver=str([driver, e])))
180
181
182def _load_multi_stores(conf, reserved_stores=None):
183    enabled_backends = conf.enabled_backends
184    if reserved_stores:
185        enabled_backends.update(reserved_stores)
186        _RESERVED_STORES.update(reserved_stores)
187
188    for backend, store_entry in enabled_backends.items():
189        try:
190            # FIXME(flaper87): Don't hide BadStoreConfiguration
191            # exceptions. These exceptions should be propagated
192            # to the user of the library.
193            store_instance = _load_multi_store(conf, store_entry,
194                                               backend=backend)
195
196            if not store_instance:
197                continue
198
199            yield (store_entry, store_instance, backend)
200
201        except exceptions.BadStoreConfiguration:
202            continue
203
204
205def create_multi_stores(conf=CONF, reserved_stores=None):
206    """
207    Registers all store modules and all schemes from the given configuration
208    object.
209
210    :param conf: A oslo_config (or compatible) object
211    :param reserved_stores: A list of stores for the consuming service's
212                            internal use.  The list must be the same
213                            format as the ``enabled_backends`` configuration
214                            setting.  The default value is None
215    :return: The number of stores configured
216    :raises: ``glance_store.exceptions.BackendException``
217
218    *Configuring Multiple Backends*
219
220    The backends to be configured are expected to be found in the
221    ``enabled_backends`` configuration variable in the DEFAULT group
222    of the object.  The format for the variable is a dictionary of
223    key:value pairs where the key is an arbitrary store identifier
224    and the value is the store type identifier for the store.
225
226    The type identifiers must be defined in the  ``[entry points]``
227    section of the glance_store ``setup.cfg`` file as values for
228    the ``glance_store.drivers`` configuration.  (See the default
229    ``setup.cfg`` file for an example.)  The store type identifiers
230    for the currently supported drivers are already defined in the file.
231
232    Thus an example value for ``enabled_backends`` is::
233
234        {'store_one': 'http', 'store_two': 'file', 'store_three': 'rbd'}
235
236    The ``reserved_stores`` parameter, if included, must have the same
237    format.  There is no difference between the ``enabled_backends`` and
238    ``reserved_stores`` from the glance_store point of view: the reserved
239    stores are a convenience for the consuming service, which may wish
240    to handle the two sets of stores differently.
241
242    *The Default Store*
243
244    If you wish to set a default store, its store identifier should be
245    defined as the value of the ``default_backend`` configuration option
246    in the ``glance_store`` group of the ``conf`` parameter.  The store
247    identifier, or course, should be specified as one of the keys in the
248    ``enabled_backends`` dict.  It is recommended that a default store
249    be set.
250
251    *Configuring Individual Backends*
252
253    To configure each store mentioned in the ``enabled_backends``
254    configuration option, you must define an option group with the
255    same name as the store identifier.  The options defined for that
256    backend will depend upon the store type; consult the documentation
257    for the appropriate backend driver to determine what these are.
258
259    For example, given the ``enabled_backends`` example above, you
260    would put the following in the configuration file that loads the
261    ``conf`` object::
262
263        [DEFAULT]
264        enabled_backends = store_one:rbd,store_two:file,store_three:http
265
266        [store_one]
267        store_description = "A human-readable string aimed at end users"
268        rbd_store_chunk_size = 8
269        rbd_store_pool = images
270        rbd_store_user = admin
271        rbd_store_ceph_conf = /etc/ceph/ceph.conf
272
273        [store_two]
274        store_description = "Human-readable description of this store"
275        filesystem_store_datadir = /opt/stack/data/glance/store_two
276
277        [store_three]
278        store_description = "A read-only store"
279        https_ca_certificates_file = /opt/stack/certs/gs.cert
280
281        [glance_store]
282        default_backend = store_two
283
284    The ``store_description`` options may be used by a consuming service.
285    As recommended above, this file also defines a default backend.
286    """
287
288    store_count = 0
289    scheme_map = {}
290    for (store_entry, store_instance,
291         store_identifier) in _load_multi_stores(
292            conf, reserved_stores=reserved_stores):
293        try:
294            schemes = store_instance.get_schemes()
295            store_instance.configure(re_raise_bsc=False)
296        except NotImplementedError:
297            continue
298
299        if not schemes:
300            raise exceptions.BackendException(
301                _('Unable to register store %s. No schemes associated '
302                  'with it.') % store_entry)
303        else:
304            LOG.debug("Registering store %s with schemes %s",
305                      store_entry, schemes)
306
307            loc_cls = store_instance.get_store_location_class()
308            for scheme in schemes:
309                if scheme not in scheme_map:
310                    scheme_map[scheme] = {}
311                scheme_map[scheme][store_identifier] = {
312                    'store': store_instance,
313                    'location_class': loc_cls,
314                    'store_entry': store_entry
315                }
316                location.register_scheme_backend_map(scheme_map)
317                store_count += 1
318
319    return store_count
320
321
322def verify_store():
323    store_id = CONF.glance_store.default_backend
324    if not store_id:
325        msg = _("'default_backend' config option is not set.")
326        raise RuntimeError(msg)
327
328    try:
329        get_store_from_store_identifier(store_id)
330    except exceptions.UnknownScheme:
331        msg = _("Store for identifier %s not found") % store_id
332        raise RuntimeError(msg)
333
334
335def get_store_from_store_identifier(store_identifier):
336    """Determine backing store from identifier.
337
338    Given a store identifier, return the appropriate store object
339    for handling that scheme.
340    """
341    scheme_map = {}
342    enabled_backends = CONF.enabled_backends
343    enabled_backends.update(_RESERVED_STORES)
344
345    try:
346        scheme = enabled_backends[store_identifier]
347    except KeyError:
348        msg = _("Store for identifier %s not found") % store_identifier
349        raise exceptions.UnknownScheme(msg)
350
351    if scheme not in location.SCHEME_TO_CLS_BACKEND_MAP:
352        raise exceptions.UnknownScheme(scheme=scheme)
353
354    scheme_info = location.SCHEME_TO_CLS_BACKEND_MAP[scheme][store_identifier]
355    store = scheme_info['store']
356
357    if not store.is_capable(capabilities.BitMasks.DRIVER_REUSABLE):
358        # Driver instance isn't stateless so it can't
359        # be reused safely and need recreation.
360        store_entry = scheme_info['store_entry']
361        store = _load_multi_store(store.conf, store_entry, invoke_load=True,
362                                  backend=store_identifier)
363        store.configure()
364        try:
365            loc_cls = store.get_store_location_class()
366            for new_scheme in store.get_schemes():
367                if new_scheme not in scheme_map:
368                    scheme_map[new_scheme] = {}
369
370                scheme_map[new_scheme][store_identifier] = {
371                    'store': store,
372                    'location_class': loc_cls,
373                    'store_entry': store_entry
374                }
375                location.register_scheme_backend_map(scheme_map)
376        except NotImplementedError:
377            scheme_info['store'] = store
378
379    return store
380
381
382def add(conf, image_id, data, size, backend, context=None,
383        verifier=None):
384    if not backend:
385        backend = conf.glance_store.default_backend
386
387    store = get_store_from_store_identifier(backend)
388    return store_add_to_backend(image_id, data, size, store, context,
389                                verifier)
390
391
392def add_with_multihash(conf, image_id, data, size, backend, hashing_algo,
393                       scheme=None, context=None, verifier=None):
394    if not backend:
395        backend = conf.glance_store.default_backend
396
397    store = get_store_from_store_identifier(backend)
398    return store_add_to_backend_with_multihash(
399        image_id, data, size, hashing_algo, store, context, verifier)
400
401
402def _check_metadata(store, metadata):
403    if not isinstance(metadata, dict):
404        msg = (_("The storage driver %(driver)s returned invalid "
405                 " metadata %(metadata)s. This must be a dictionary type")
406               % dict(driver=str(store), metadata=str(metadata)))
407        LOG.error(msg)
408        raise exceptions.BackendException(msg)
409    try:
410        check_location_metadata(metadata)
411    except exceptions.BackendException as e:
412        e_msg = (_("A bad metadata structure was returned from the "
413                   "%(driver)s storage driver: %(metadata)s.  %(e)s.") %
414                 dict(driver=encodeutils.exception_to_unicode(store),
415                      metadata=encodeutils.exception_to_unicode(metadata),
416                      e=encodeutils.exception_to_unicode(e)))
417        LOG.error(e_msg)
418        raise exceptions.BackendException(e_msg)
419
420
421def store_add_to_backend(image_id, data, size, store, context=None,
422                         verifier=None):
423    """A wrapper around a call to each stores add() method.
424
425    This gives glance a common place to check the output.
426
427    :param image_id:  The image add to which data is added
428    :param data: The data to be stored
429    :param size: The length of the data in bytes
430    :param store: The store to which the data is being added
431    :param context: The request context
432    :param verifier: An object used to verify signatures for images
433    :param backend: Name of the backend to store the image
434    :return: The url location of the file,
435             the size amount of data,
436             the checksum of the data
437             the storage systems metadata dictionary for the location
438    """
439    (location, size, checksum, metadata) = store.add(image_id,
440                                                     data,
441                                                     size,
442                                                     context=context,
443                                                     verifier=verifier)
444
445    if metadata is not None:
446        _check_metadata(store, metadata)
447
448    return (location, size, checksum, metadata)
449
450
451def store_add_to_backend_with_multihash(
452        image_id, data, size, hashing_algo, store,
453        context=None, verifier=None):
454    """
455    A wrapper around a call to each store's add() method that requires
456    a hashing_algo identifier and returns a 5-tuple including the
457    "multihash" computed using the specified hashing_algo.  (This
458    is an enhanced version of store_add_to_backend(), which is left
459    as-is for backward compatibility.)
460
461    :param image_id:  The image add to which data is added
462    :param data: The data to be stored
463    :param size: The length of the data in bytes
464    :param store: The store to which the data is being added
465    :param hashing_algo: A hashlib algorithm identifier (string)
466    :param context: The request context
467    :param verifier: An object used to verify signatures for images
468    :return: The url location of the file,
469             the size amount of data,
470             the checksum of the data,
471             the multihash of the data,
472             the storage system's metadata dictionary for the location
473    :raises: ``glance_store.exceptions.BackendException``
474             ``glance_store.exceptions.UnknownHashingAlgo``
475    """
476
477    if hashing_algo not in hashlib.algorithms_available:
478        raise exceptions.UnknownHashingAlgo(algo=hashing_algo)
479
480    (location, size, checksum, multihash, metadata) = store.add(
481        image_id, data, size, hashing_algo, context=context, verifier=verifier)
482
483    if metadata is not None:
484        _check_metadata(store, metadata)
485
486    return (location, size, checksum, multihash, metadata)
487
488
489def check_location_metadata(val, key=''):
490    if isinstance(val, dict):
491        for key in val:
492            check_location_metadata(val[key], key=key)
493    elif isinstance(val, list):
494        ndx = 0
495        for v in val:
496            check_location_metadata(v, key='%s[%d]' % (key, ndx))
497            ndx = ndx + 1
498    elif not isinstance(val, six.text_type):
499        raise exceptions.BackendException(_("The image metadata key %(key)s "
500                                            "has an invalid type of %(type)s. "
501                                            "Only dict, list, and unicode are "
502                                            "supported.")
503                                          % dict(key=key, type=type(val)))
504
505
506def delete(uri, backend, context=None):
507    """Removes chunks of data from backend specified by uri."""
508    if backend:
509        loc = location.get_location_from_uri_and_backend(
510            uri, backend, conf=CONF)
511        store = get_store_from_store_identifier(backend)
512        return store.delete(loc, context=context)
513
514    LOG.warning('Backend is not set to image, searching all backends based on '
515                'location URI.')
516
517    backends = CONF.enabled_backends
518    for backend in backends:
519        try:
520            if not uri.startswith(backends[backend]):
521                continue
522
523            loc = location.get_location_from_uri_and_backend(
524                uri, backend, conf=CONF)
525            store = get_store_from_store_identifier(backend)
526            return store.delete(loc, context=context)
527        except (exceptions.NotFound, exceptions.UnknownScheme):
528            continue
529
530    raise exceptions.NotFound(_("Image not found in any configured backend"))
531
532
533def set_acls_for_multi_store(location_uri, backend, public=False,
534                             read_tenants=[],
535                             write_tenants=None, context=None):
536
537    if write_tenants is None:
538        write_tenants = []
539
540    loc = location.get_location_from_uri_and_backend(
541        location_uri, backend, conf=CONF)
542    store = get_store_from_store_identifier(backend)
543    try:
544        store.set_acls(loc, public=public,
545                       read_tenants=read_tenants,
546                       write_tenants=write_tenants,
547                       context=context)
548    except NotImplementedError:
549        LOG.debug("Skipping store.set_acls... not implemented")
550
551
552def get(uri, backend, offset=0, chunk_size=None, context=None):
553    """Yields chunks of data from backend specified by uri."""
554
555    if backend:
556        loc = location.get_location_from_uri_and_backend(uri, backend,
557                                                         conf=CONF)
558        store = get_store_from_store_identifier(backend)
559
560        return store.get(loc, offset=offset,
561                         chunk_size=chunk_size,
562                         context=context)
563
564    LOG.warning('Backend is not set to image, searching all backends based on '
565                'location URI.')
566
567    backends = CONF.enabled_backends
568    for backend in backends:
569        try:
570            if not uri.startswith(backends[backend]):
571                continue
572
573            loc = location.get_location_from_uri_and_backend(
574                uri, backend, conf=CONF)
575            store = get_store_from_store_identifier(backend)
576            data, size = store.get(loc, offset=offset,
577                                   chunk_size=chunk_size,
578                                   context=context)
579            if data:
580                return data, size
581        except (exceptions.NotFound, exceptions.UnknownScheme):
582            continue
583
584    raise exceptions.NotFound(_("Image not found in any configured backend"))
585
586
587def get_known_schemes_for_multi_store():
588    """Returns list of known schemes."""
589    return location.SCHEME_TO_CLS_BACKEND_MAP.keys()
590
591
592def get_size_from_uri_and_backend(uri, backend, context=None):
593    """Retrieves image size from backend specified by uri."""
594
595    loc = location.get_location_from_uri_and_backend(
596        uri, backend, conf=CONF)
597    store = get_store_from_store_identifier(backend)
598    return store.get_size(loc, context=context)
599