소스 검색

Merge pull request #4 from aznashwan/storage-map

Add dedicated 'storage_mappings' parameter to Migration/Replica APIs.
Nashwan Azhari 7 년 전
부모
커밋
1b15bbada0

+ 34 - 0
coriolis/api/v1/endpoint_storage.py

@@ -0,0 +1,34 @@
+# Copyright 2018 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from oslo_log import log as logging
+
+from coriolis import utils
+from coriolis.api.v1.views import endpoint_storage_view
+from coriolis.api import wsgi as api_wsgi
+from coriolis.endpoint_storage import api
+from coriolis.policies import endpoints as endpoint_policies
+
+LOG = logging.getLogger(__name__)
+
+
+class EndpointStorageController(api_wsgi.Controller):
+    def __init__(self):
+        self._storage_api = api.API()
+        super(EndpointStorageController, self).__init__()
+
+    def index(self, req, endpoint_id):
+        context = req.environ['coriolis.context']
+        context.can("%s:list_storage" % (
+            endpoint_policies.ENDPOINTS_POLICY_PREFIX))
+        env = req.GET.get("env")
+        if env is not None:
+            env = utils.decode_base64_param(env, is_json=True)
+
+        return endpoint_storage_view.collection(
+            req, self._storage_api.get_endpoint_storage(
+                context, endpoint_id, env))
+
+
+def create_resource():
+    return api_wsgi.Resource(EndpointStorageController())

+ 16 - 19
coriolis/api/v1/migrations.py

@@ -5,7 +5,7 @@ from oslo_log import log as logging
 from webob import exc
 
 from coriolis import exception
-from coriolis import schemas
+from coriolis.api.v1 import utils as api_utils
 from coriolis.api.v1.views import migration_view
 from coriolis.api import wsgi as api_wsgi
 from coriolis.endpoints import api as endpoints_api
@@ -53,29 +53,25 @@ class MigrationController(api_wsgi.Controller):
             instances = migration["instances"]
             notes = migration.get("notes")
             skip_os_morphing = migration.get("skip_os_morphing", False)
+
             network_map = migration.get("network_map", {})
-            try:
-                schemas.validate_value(
-                    network_map, schemas.CORIOLIS_NETWORK_MAP_SCHEMA)
-            except exception.SchemaValidationException:
-                raise exc.HTTPBadRequest(
-                    explanation="Invalid network_map "
-                                "%s" % network_map)
+            api_utils.validate_network_map(network_map)
+
+            storage_mappings = migration.get("storage_mappings", {})
+            api_utils.validate_storage_mappings(storage_mappings)
 
-            # NOTE: until the provider plugin interface is updated to have a
-            # separate 'network_map' field, we add it into the destination
-            # environment.
+            # TODO(aznashwan): until the provider plugin interface is updated
+            # to have separate 'network_map' and 'storage_mappings' fields,
+            # we add them as part of the destination environment:
             destination_environment['network_map'] = network_map
+            destination_environment['storage_mappings'] = storage_mappings
 
             return (origin_endpoint_id, destination_endpoint_id,
                     destination_environment, instances, notes,
-                    skip_os_morphing, network_map)
+                    skip_os_morphing, network_map, storage_mappings)
         except Exception as ex:
             LOG.exception(ex)
-            if hasattr(ex, "message"):
-                msg = ex.message
-            else:
-                msg = str(ex)
+            msg = getattr(ex, "message", str(ex))
             raise exception.InvalidInput(msg)
 
     def create(self, req, body):
@@ -100,7 +96,8 @@ class MigrationController(api_wsgi.Controller):
              destination_environment,
              instances,
              notes,
-             skip_os_morphing, network_map) = self._validate_migration_input(
+             skip_os_morphing, network_map,
+             storage_mappings) = self._validate_migration_input(
                 migration_body)
             is_valid, message = (
                 self._endpoints_api.validate_target_environment(
@@ -112,8 +109,8 @@ class MigrationController(api_wsgi.Controller):
 
             migration = self._migration_api.migrate_instances(
                 context, origin_endpoint_id, destination_endpoint_id,
-                destination_environment, instances, network_map, notes,
-                skip_os_morphing)
+                destination_environment, instances, network_map,
+                storage_mappings, notes, skip_os_morphing)
 
         return migration_view.single(req, migration)
 

+ 19 - 21
coriolis/api/v1/replicas.py

@@ -5,7 +5,7 @@ from oslo_log import log as logging
 from webob import exc
 
 from coriolis import exception
-from coriolis import schemas
+from coriolis.api.v1 import utils as api_utils
 from coriolis.api.v1.views import replica_view
 from coriolis.api import wsgi as api_wsgi
 from coriolis.endpoints import api as endpoints_api
@@ -54,28 +54,25 @@ class ReplicaController(api_wsgi.Controller):
             destination_environment = replica.get("destination_environment")
             instances = replica["instances"]
             notes = replica.get("notes")
-            network_map = replica.get("network_map")
-            try:
-                schemas.validate_value(
-                    network_map, schemas.CORIOLIS_NETWORK_MAP_SCHEMA)
-            except exception.SchemaValidationException:
-                raise exc.HTTPBadRequest(
-                    explanation="Invalid network_map "
-                                "%s" % network_map)
-
-            # NOTE: until the provider plugin interface is updated to have a
-            # separate 'network_map' field, we add it into the destination
-            # environment.
-            destination_environment["network_map"] = network_map
+
+            network_map = replica.get("network_map", {})
+            api_utils.validate_network_map(network_map)
+
+            storage_mappings = replica.get("storage_mappings", {})
+            api_utils.validate_storage_mappings(storage_mappings)
+
+            # TODO(aznashwan): until the provider plugin interface is updated
+            # to have separate 'network_map' and 'storage_mappings' fields,
+            # we add them as part of the destination environment:
+            destination_environment['network_map'] = network_map
+            destination_environment['storage_mappings'] = storage_mappings
 
             return (origin_endpoint_id, destination_endpoint_id,
-                    destination_environment, instances, network_map, notes)
+                    destination_environment, instances, network_map,
+                    storage_mappings, notes)
         except Exception as ex:
             LOG.exception(ex)
-            if hasattr(ex, "message"):
-                msg = ex.message
-            else:
-                msg = str(ex)
+            msg = getattr(ex, "message", str(ex))
             raise exception.InvalidInput(msg)
 
     def create(self, req, body):
@@ -84,7 +81,7 @@ class ReplicaController(api_wsgi.Controller):
 
         (origin_endpoint_id, destination_endpoint_id,
          destination_environment, instances, network_map,
-         notes) = self._validate_create_body(body)
+         storage_mappings, notes) = self._validate_create_body(body)
 
         is_valid, message = (
             self._endpoints_api.validate_target_environment(
@@ -97,7 +94,8 @@ class ReplicaController(api_wsgi.Controller):
 
         return replica_view.single(req, self._replica_api.create(
             context, origin_endpoint_id, destination_endpoint_id,
-            destination_environment, instances, network_map, notes))
+            destination_environment, instances, network_map,
+            storage_mappings, notes))
 
     def delete(self, req, id):
         context = req.environ["coriolis.context"]

+ 6 - 0
coriolis/api/v1/router.py

@@ -8,6 +8,7 @@ from coriolis.api.v1 import endpoint_actions
 from coriolis.api.v1 import endpoint_destination_options
 from coriolis.api.v1 import endpoint_instances
 from coriolis.api.v1 import endpoint_networks
+from coriolis.api.v1 import endpoint_storage
 from coriolis.api.v1 import endpoints
 from coriolis.api.v1 import migration_actions
 from coriolis.api.v1 import migrations
@@ -65,6 +66,11 @@ class APIRouter(api.APIRouter):
         mapper.resource('network', 'endpoints/{endpoint_id}/networks',
                         controller=self.resources['endpoint_networks'])
 
+        self.resources['endpoint_storage'] = \
+            endpoint_storage.create_resource()
+        mapper.resource('storage', 'endpoints/{endpoint_id}/storage',
+                        controller=self.resources['endpoint_storage'])
+
         self.resources['endpoint_destination_options'] = \
             endpoint_destination_options.create_resource()
         mapper.resource('destination_options',

+ 31 - 0
coriolis/api/v1/utils.py

@@ -0,0 +1,31 @@
+# Copyright 2018 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from oslo_log import log as logging
+from webob import exc
+
+from coriolis import exception
+from coriolis import schemas
+
+
+LOG = logging.getLogger(__name__)
+
+
+def validate_network_map(network_map):
+    """ Validates the JSON schema for the network_map. """
+    try:
+        schemas.validate_value(
+            network_map, schemas.CORIOLIS_NETWORK_MAP_SCHEMA)
+    except exception.SchemaValidationException as ex:
+        raise exc.HTTPBadRequest(
+            explanation="Invalid network_map: %s" % str(ex))
+
+
+def validate_storage_mappings(storage_mappings):
+    """ Validates the JSON schema for the storage_mappings. """
+    try:
+        schemas.validate_value(
+            storage_mappings, schemas.CORIOLIS_STORAGE_MAPPINGS_SCHEMA)
+    except exception.SchemaValidationException as ex:
+        raise exc.HTTPBadRequest(
+            explanation="Invalid storage_mappings: %s" % str(ex))

+ 19 - 0
coriolis/api/v1/views/endpoint_storage_view.py

@@ -0,0 +1,19 @@
+# Copyright 2018 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import itertools
+
+
+def _format_storage(req, storage, keys=None):
+    def transform(key, value):
+        if keys and key not in keys:
+            return
+        yield (key, value)
+
+    return dict(itertools.chain.from_iterable(
+        transform(k, v) for k, v in storage.items()))
+
+
+def collection(req, storages):
+    formatted_storages = [_format_storage(req, m) for m in storages]
+    return {'storage': formatted_storages}

+ 14 - 5
coriolis/conductor/rpc/client.py

@@ -77,6 +77,12 @@ class ConductorClient(object):
             endpoint_id=endpoint_id,
             env=env)
 
+    def get_endpoint_storage(self, ctxt, endpoint_id, env):
+        return self._client.call(
+            ctxt, 'get_endpoint_storage',
+            endpoint_id=endpoint_id,
+            env=env)
+
     def validate_endpoint_connection(self, ctxt, endpoint_id):
         return self._client.call(
             ctxt, 'validate_endpoint_connection',
@@ -129,8 +135,9 @@ class ConductorClient(object):
 
     def create_instances_replica(self, ctxt, origin_endpoint_id,
                                  destination_endpoint_id,
-                                 destination_environment,
-                                 instances, network_map, notes=None):
+                                 destination_environment, instances,
+                                 network_map, storage_mappings,
+                                 notes=None):
         return self._client.call(
             ctxt, 'create_instances_replica',
             origin_endpoint_id=origin_endpoint_id,
@@ -138,7 +145,8 @@ class ConductorClient(object):
             destination_environment=destination_environment,
             instances=instances,
             notes=notes,
-            network_map=network_map)
+            network_map=network_map,
+            storage_mappings=storage_mappings)
 
     def get_replicas(self, ctxt, include_tasks_executions=False):
         return self._client.call(
@@ -167,7 +175,7 @@ class ConductorClient(object):
 
     def migrate_instances(self, ctxt, origin_endpoint_id,
                           destination_endpoint_id, destination_environment,
-                          instances, network_map, notes=None,
+                          instances, network_map, storage_mappings, notes=None,
                           skip_os_morphing=False):
         return self._client.call(
             ctxt, 'migrate_instances',
@@ -177,7 +185,8 @@ class ConductorClient(object):
             instances=instances,
             notes=notes,
             skip_os_morphing=skip_os_morphing,
-            network_map=network_map)
+            network_map=network_map,
+            storage_mappings=storage_mappings)
 
     def deploy_replica_instances(self, ctxt, replica_id, clone_disks=False,
                                  force=False, skip_os_morphing=False):

+ 12 - 3
coriolis/conductor/rpc/server.py

@@ -151,6 +151,12 @@ class ConductorServerEndpoint(object):
         return self._rpc_worker_client.get_endpoint_networks(
             ctxt, endpoint.type, endpoint.connection_info, env)
 
+    def get_endpoint_storage(self, ctxt, endpoint_id, env):
+        endpoint = self.get_endpoint(ctxt, endpoint_id)
+
+        return self._rpc_worker_client.get_endpoint_storage(
+            ctxt, endpoint.type, endpoint.connection_info, env)
+
     def validate_endpoint_connection(self, ctxt, endpoint_id):
         endpoint = self.get_endpoint(ctxt, endpoint_id)
         return self._rpc_worker_client.validate_endpoint_connection(
@@ -381,7 +387,7 @@ class ConductorServerEndpoint(object):
     def create_instances_replica(self, ctxt, origin_endpoint_id,
                                  destination_endpoint_id,
                                  destination_environment, instances,
-                                 network_map, notes=None):
+                                 network_map, storage_mappings, notes=None):
         origin_endpoint = self.get_endpoint(ctxt, origin_endpoint_id)
         destination_endpoint = self.get_endpoint(ctxt, destination_endpoint_id)
         self._check_endpoints(ctxt, origin_endpoint, destination_endpoint)
@@ -396,6 +402,7 @@ class ConductorServerEndpoint(object):
         replica.info = {}
         replica.notes = notes
         replica.network_map = network_map
+        replica.storage_mappings = storage_mappings
 
         db_api.add_replica(ctxt, replica)
         LOG.info("Replica created: %s", replica.id)
@@ -489,6 +496,7 @@ class ConductorServerEndpoint(object):
         migration.destination_endpoint_id = replica.destination_endpoint_id
         migration.destination_environment = replica.destination_environment
         migration.network_map = replica.network_map
+        migration.storage_mappings = replica.storage_mappings
         migration.instances = instances
         migration.replica = replica
         migration.info = replica.info
@@ -577,8 +585,8 @@ class ConductorServerEndpoint(object):
 
     def migrate_instances(self, ctxt, origin_endpoint_id,
                           destination_endpoint_id, destination_environment,
-                          instances, network_map, skip_os_morphing=False,
-                          notes=None):
+                          instances, network_map, storage_mappings,
+                          skip_os_morphing=False, notes=None):
         origin_endpoint = self.get_endpoint(ctxt, origin_endpoint_id)
         destination_endpoint = self.get_endpoint(ctxt, destination_endpoint_id)
         self._check_endpoints(ctxt, origin_endpoint, destination_endpoint)
@@ -592,6 +600,7 @@ class ConductorServerEndpoint(object):
         migration.destination_endpoint = destination_endpoint
         migration.destination_environment = destination_environment
         migration.network_map = network_map
+        migration.storage_mappings = storage_mappings
         execution = models.TasksExecution()
         execution.status = constants.EXECUTION_STATUS_RUNNING
         execution.number = 1

+ 1 - 0
coriolis/constants.py

@@ -65,6 +65,7 @@ PROVIDER_TYPE_VALIDATE_MIGRATION_EXPORT = 2048
 PROVIDER_TYPE_VALIDATE_REPLICA_EXPORT = 4096
 PROVIDER_TYPE_VALIDATE_MIGRATION_IMPORT = 8192
 PROVIDER_TYPE_VALIDATE_REPLICA_IMPORT = 16384
+PROVIDER_TYPE_ENDPOINT_STORAGE = 32768
 
 DISK_FORMAT_VMDK = 'vmdk'
 DISK_FORMAT_RAW = 'raw'

+ 17 - 0
coriolis/db/sqlalchemy/migrate_repo/versions/007_adds_storage_mappings.py

@@ -0,0 +1,17 @@
+# Copyright 2018 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import sqlalchemy
+
+
+def upgrade(migrate_engine):
+    meta = sqlalchemy.MetaData()
+    meta.bind = migrate_engine
+
+    # add 'storage_mappings' column to 'base_transfer_action':
+    base_transfer_action = sqlalchemy.Table(
+        'base_transfer_action', meta, autoload=True)
+
+    storage_mappings = sqlalchemy.Column(
+        "storage_mappings", sqlalchemy.Text, nullable=True)
+    base_transfer_action.create_column(storage_mappings)

+ 1 - 0
coriolis/db/sqlalchemy/models.py

@@ -113,6 +113,7 @@ class BaseTransferAction(BASE, models.TimestampMixin, models.ModelBase,
         sqlalchemy.ForeignKey('endpoint.id'), nullable=False)
     transfer_result = sqlalchemy.Column(types.Json, nullable=True)
     network_map = sqlalchemy.Column(types.Json, nullable=True)
+    storage_mappings = sqlalchemy.Column(types.Json, nullable=True)
 
     __mapper_args__ = {
         'polymorphic_identity': 'base_transfer_action',

+ 0 - 0
coriolis/endpoint_storage/__init__.py


+ 13 - 0
coriolis/endpoint_storage/api.py

@@ -0,0 +1,13 @@
+# Copyright 2017 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from coriolis.conductor.rpc import client as rpc_client
+
+
+class API(object):
+    def __init__(self):
+        self._rpc_client = rpc_client.ConductorClient()
+
+    def get_endpoint_storage(self, ctxt, endpoint_id, env):
+        return self._rpc_client.get_endpoint_storage(
+            ctxt, endpoint_id, env)

+ 9 - 0
coriolis/exception.py

@@ -236,6 +236,15 @@ class NetworkNotFound(NotFound):
     message = _("Network \"%(network_name)s\" could not be found.")
 
 
+class DiskStorageMappingNotFound(NotFound):
+    message = _('No storage mapping for disk with ID "%(id)s" could be found.')
+
+
+class StorageBackendNotFound(NotFound):
+    message = _(
+        'Storage backend with name "%(storage_name)s" could not be found.')
+
+
 class ImageNotFound(NotFound):
     message = _("Image \"%(image_name)s\" could not be found.")
 

+ 3 - 3
coriolis/migrations/api.py

@@ -10,12 +10,12 @@ class API(object):
 
     def migrate_instances(self, ctxt, origin_endpoint_id,
                           destination_endpoint_id, destination_environment,
-                          instances, network_map, notes=None,
+                          instances, network_map, storage_mappings, notes=None,
                           skip_os_morphing=False):
         return self._rpc_client.migrate_instances(
             ctxt, origin_endpoint_id, destination_endpoint_id,
-            destination_environment, instances, network_map, notes,
-            skip_os_morphing)
+            destination_environment, instances, network_map, storage_mappings,
+            notes, skip_os_morphing)
 
     def deploy_replica_instances(self, ctxt, replica_id, clone_disks=False,
                                  force=False, skip_os_morphing=False):

+ 12 - 0
coriolis/policies/endpoints.py

@@ -117,6 +117,18 @@ ENDPOINTS_POLICY_DEFAULT_RULES = [
             }
         ]
     ),
+    policy.DocumentedRuleDefault(
+        get_endpoints_policy_label('list_storage'),
+        ENDPOINTS_POLICY_DEFAULT_RULE,
+        "List storage types available on the given endpoint",
+        [
+            {
+                "path": "/endpoint/{endpoint_id}/storage",
+                "method": "GET"
+            }
+        ]
+    ),
+
     policy.DocumentedRuleDefault(
         get_endpoints_policy_label('list_destination_options'),
         ENDPOINTS_POLICY_DEFAULT_RULE,

+ 7 - 0
coriolis/providers/base.py

@@ -369,3 +369,10 @@ def get_os_morphing_tools_helper(conn, os_morphing_tools_clss,
         if os_info:
             return (tools, os_info)
     raise exception.OSMorphingToolsNotFound()
+
+
+class BaseEndpointStorageProvider(object, with_metaclass(abc.ABCMeta)):
+    @abc.abstractmethod
+    def get_storage(self, ctxt, connection_info):
+        """ Returns all the storage options available"""
+        pass

+ 2 - 0
coriolis/providers/factory.py

@@ -29,6 +29,8 @@ PROVIDER_TYPE_MAP = {
         base.BaseEndpointInstancesProvider,
     constants.PROVIDER_TYPE_ENDPOINT_NETWORKS:
         base.BaseEndpointNetworksProvider,
+    constants.PROVIDER_TYPE_ENDPOINT_STORAGE:
+        base.BaseEndpointStorageProvider,
     constants.PROVIDER_TYPE_OS_MORPHING: base.BaseImportInstanceProvider,
     constants.PROVIDER_TYPE_INSTANCE_FLAVOR: base.BaseInstanceFlavorProvider,
     constants.PROVIDER_TYPE_SETUP_LIBS: base.BaseProviderSetupExtraLibsMixin,

+ 109 - 0
coriolis/providers/provider_utils.py

@@ -0,0 +1,109 @@
+# Copyright 2018 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from oslo_log import log as logging
+
+from coriolis import exception
+
+
+LOG = logging.getLogger(__name__)
+
+
+def get_storage_mapping_for_disk(
+        storage_mappings, disk_info, storage_backends,
+        config_default=None, error_on_missing_mapping=True,
+        error_on_backend_not_found=True):
+    """ Returns the storage backend identifier from the given list of
+    `storage_backends` to map for the disk given by its `disk_info`.
+
+    Order of mapping resolution is:
+        - per-disk-ID mappings from storage_mappings['disk_mappings']
+        - per-storage-bakend mappings for storage_mappings['backend_mappings']
+        - storage_mappings['default']
+        - the supplies `config_default` parameter
+
+    param storage_mappings: dict(): storage mappings dict compliant with the
+    `coriolis.schemas.CORIOLIS_STORAGE_MAPPINGS_SCHEMA`
+    param disk_info: dict(): dict with the disk info compliant with the
+    structure of the `devices['disks']` fields in the
+    `coriolis.schemas.CORIOLIS_VM_EXPORT_INFO_SCHEMA`
+    param storage_backends: list(dict()): list of dicts corresponding to the
+    available storage backends as specified in the
+    `coriolis.schemas.CORIOLIS_STORAGE_SCHEMA`
+    param config_default: str: optional default value from the configuration
+    file to fall back to.
+    param error_on_missing_mapping: bool(): whether or not to raise an
+    exception if there is no mapping found for the disk and no
+    storage_mappings['default'] or `config_default` is provided.
+    param error_on_backend_not_found: bool(): whether or not ro raise an
+    exception if a storage backend scpecified in the mapping is not found.
+    """
+    disk_mappings = {
+        mapping['disk_id']: mapping['destination']
+        for mapping in storage_mappings.get("disk_mappings", [])}
+    backend_mappings = {
+        mapping['source']: mapping['destination']
+        for mapping in storage_mappings.get('backend_mappings', [])}
+
+    LOG.debug(
+        "Resolving disk storage backend mapping for disk '%s' from available "
+        "backends: %s (disk_mappings=%s, backend_mappings=%s, default=%s, "
+        "config_default=%s)", disk_info, storage_backends, disk_mappings,
+        backend_mappings, storage_mappings.get('default'), config_default)
+
+    mapped_backend = None
+
+    # 1) check for explicit disk mapping:
+    # NOTE: the core VM export info schema allows for the disk IDs to be ints
+    # as well, so we need to convert to a string (the JSON structure of the
+    # 'storage_mappings' API field guarantees the disk ID keys will be strings)
+    disk_id = str(disk_info['id'])
+    if disk_id in disk_mappings:
+        mapped_backend = disk_mappings[disk_id]
+        LOG.debug(
+            "Found mapping for disk ID '%s' in the 'disk_mappings': %s",
+            disk_id, mapped_backend)
+
+    # 2) check for backend mapping if available:
+    if not mapped_backend:
+        if 'storage_backend_identifier' in disk_info:
+            if disk_info['storage_backend_identifier'] in backend_mappings:
+                mapped_backend = backend_mappings[
+                    disk_info['storage_backend_identifier']]
+                LOG.debug(
+                    "Found mapping for disk ID '%s' in the "
+                    "'backend_mappings': %s", disk_id, mapped_backend)
+            else:
+                LOG.debug(
+                    "'storage_backend_identifier' for disk '%s' is not mapped "
+                    "in the 'backend_mappings' from the 'storage_mappings'.",
+                    disk_info)
+        else:
+            LOG.debug(
+                "No 'storage_backend_identifier' set for disk '%s'", disk_info)
+
+    # 3) use provided default:
+    if not mapped_backend:
+        mapped_backend = storage_mappings.get('default', config_default)
+
+    if mapped_backend is None:
+        LOG.warn(
+            "Could not find mapped storage backend for disk '%s'", disk_info)
+        if error_on_missing_mapping:
+            raise exception.DiskStorageMappingNotFound(id=disk_id)
+
+    if mapped_backend:
+        if mapped_backend not in [
+                backend['name'] for backend in storage_backends]:
+            LOG.warn(
+                "Mapped storage backend for disk '%s' ('%s') does not exist!",
+                disk_info, mapped_backend)
+            if error_on_backend_not_found:
+                raise exception.StorageBackendNotFound(
+                    storage_name=mapped_backend)
+
+    LOG.info(
+        "Mapped storage backend for disk '%s' is: %s",
+        disk_info, mapped_backend)
+
+    return mapped_backend

+ 4 - 2
coriolis/replicas/api.py

@@ -9,10 +9,12 @@ class API(object):
         self._rpc_client = rpc_client.ConductorClient()
 
     def create(self, ctxt, origin_endpoint_id, destination_endpoint_id,
-               destination_environment, instances, network_map, notes=None):
+               destination_environment, instances, network_map,
+               storage_mappings, notes=None):
         return self._rpc_client.create_instances_replica(
             ctxt, origin_endpoint_id, destination_endpoint_id,
-            destination_environment, instances, network_map, notes)
+            destination_environment, instances, network_map, storage_mappings,
+            notes)
 
     def delete(self, ctxt, replica_id):
         self._rpc_client.delete_replica(ctxt, replica_id)

+ 4 - 0
coriolis/schemas.py

@@ -27,6 +27,7 @@ _CORIOLIS_VM_NETWORK_SCHEMA_NAME = "vm_network_schema.json"
 _SCHEDULE_API_BODY_SCHEMA_NAME = "replica_schedule_schema.json"
 _CORIOLIS_DESTINATION_OPTIONS_SCHEMA_NAME = "destination_options_schema.json"
 _CORIOLIS_NETWORK_MAP_SCHEMA_NAME = "network_map_schema.json"
+_CORIOLIS_STORAGE_MAPPINGS_SCHEMA_NAME = "storage_mappings_schema.json"
 
 
 def get_schema(package_name, schema_name,
@@ -89,3 +90,6 @@ CORIOLIS_DESTINATION_ENVIRONMENT = get_schema(
 
 CORIOLIS_NETWORK_MAP_SCHEMA = get_schema(
     __name__, _CORIOLIS_NETWORK_MAP_SCHEMA_NAME)
+
+CORIOLIS_STORAGE_MAPPINGS_SCHEMA = get_schema(
+    __name__, _CORIOLIS_STORAGE_MAPPINGS_SCHEMA_NAME)

+ 43 - 0
coriolis/schemas/storage_mappings_schema.json

@@ -0,0 +1,43 @@
+{
+  "$schema": "http://cloudbase.it/coriolis/schemas/storage_mappings#",
+  "description": "Mapping combinations for disk storage backends of migrated/replicated VMs. The order of precendence of mapping is per-disk, per-backend, and finally the default option.",
+  "type": "object",
+  "properties": {
+    "default": {
+      "type": "string"
+    },
+    "backend_mappings": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "source": {
+            "type": "string"
+          },
+          "destination": {
+            "type": "string"
+          }
+        },
+        "required": ["source", "destination"],
+        "additionalProperties": false
+      }
+    },
+    "disk_mappings": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "disk_id": {
+            "type": "string"
+          },
+          "destination": {
+            "type": "string"
+          }
+        },
+        "required": ["disk_id", "destination"],
+        "additionalProperties": false
+      }
+    }
+  },
+  "additionalProperties": false
+}

+ 7 - 0
coriolis/worker/rpc/client.py

@@ -91,6 +91,13 @@ class WorkerClient(object):
             platform_name=platform_name,
             target_env=target_env)
 
+    def get_endpoint_storage(self, ctxt, platform_name, connection_info, env):
+        return self._client.call(
+            ctxt, 'get_endpoint_storage',
+            platform_name=platform_name,
+            connection_info=connection_info,
+            env=env)
+
     def get_available_providers(self, ctxt):
         return self._client.call(
             ctxt, 'get_available_providers')

+ 12 - 0
coriolis/worker/rpc/server.py

@@ -277,6 +277,18 @@ class WorkerServerEndpoint(object):
 
         return networks_info
 
+    def get_endpoint_storage(self, ctxt, platform_name, connection_info, env):
+        provider = providers_factory.get_provider(
+            platform_name, constants.PROVIDER_TYPE_ENDPOINT_STORAGE, None)
+
+        secret_connection_info = utils.get_secret_connection_info(
+            ctxt, connection_info)
+
+        storage = provider.get_storage(
+            ctxt, secret_connection_info, env)
+
+        return storage
+
     def get_available_providers(self, ctxt):
         return providers_factory.get_available_providers()
 

+ 1 - 0
etc/coriolis/policy.yaml

@@ -12,6 +12,7 @@
 "migration:endpoints:list_instances": "rule:admin_or_owner"
 "migration:endpoints:get_instance": "rule:admin_or_owner"
 "migration:endpoints:list_networks": "rule:admin_or_owner"
+"migration:endpoints:list_storage": "rule:admin_or_owner"
 "migration:endpoints:list_destination_options": "rule:admin_or_owner"
 
 "migration:migrations:create": "rule:admin_or_owner"