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