|
@@ -14,7 +14,6 @@ import os
|
|
|
import unittest
|
|
import unittest
|
|
|
import uuid
|
|
import uuid
|
|
|
|
|
|
|
|
-import paramiko
|
|
|
|
|
from oslo_config import cfg
|
|
from oslo_config import cfg
|
|
|
from oslo_log import log as logging
|
|
from oslo_log import log as logging
|
|
|
|
|
|
|
@@ -27,10 +26,12 @@ from coriolis.providers.base import (
|
|
|
BaseEndpointSourceOptionsProvider,
|
|
BaseEndpointSourceOptionsProvider,
|
|
|
BaseReplicaExportProvider,
|
|
BaseReplicaExportProvider,
|
|
|
BaseReplicaExportValidationProvider,
|
|
BaseReplicaExportValidationProvider,
|
|
|
|
|
+ BaseSourceMinionPoolProvider,
|
|
|
BaseUpdateSourceReplicaProvider,
|
|
BaseUpdateSourceReplicaProvider,
|
|
|
)
|
|
)
|
|
|
from coriolis.tests.integration import provider_test_base
|
|
from coriolis.tests.integration import provider_test_base
|
|
|
from coriolis.tests.integration import utils as test_utils
|
|
from coriolis.tests.integration import utils as test_utils
|
|
|
|
|
+from coriolis.tests.integration.test_provider import common
|
|
|
|
|
|
|
|
CONF = cfg.CONF
|
|
CONF = cfg.CONF
|
|
|
LOG = logging.getLogger(__name__)
|
|
LOG = logging.getLogger(__name__)
|
|
@@ -49,6 +50,7 @@ _CONTAINER_PREFIXES = ("coriolis-replicator-",)
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestExportProvider(
|
|
class TestExportProvider(
|
|
|
|
|
+ common.TestProviderMixin,
|
|
|
BaseEndpointInstancesProvider,
|
|
BaseEndpointInstancesProvider,
|
|
|
BaseEndpointInventoryExportProvider,
|
|
BaseEndpointInventoryExportProvider,
|
|
|
BaseEndpointSourceOptionsProvider,
|
|
BaseEndpointSourceOptionsProvider,
|
|
@@ -56,6 +58,7 @@ class TestExportProvider(
|
|
|
BaseReplicaExportProvider,
|
|
BaseReplicaExportProvider,
|
|
|
BaseReplicaExportValidationProvider,
|
|
BaseReplicaExportValidationProvider,
|
|
|
provider_test_base.BaseTestExportProvider,
|
|
provider_test_base.BaseTestExportProvider,
|
|
|
|
|
+ BaseSourceMinionPoolProvider,
|
|
|
):
|
|
):
|
|
|
"""Source-side provider backed by a local loop device.
|
|
"""Source-side provider backed by a local loop device.
|
|
|
|
|
|
|
@@ -133,20 +136,13 @@ class TestExportProvider(
|
|
|
def _make_replicator(self, conn_info, event_mgr, volumes_info, repl_state):
|
|
def _make_replicator(self, conn_info, event_mgr, volumes_info, repl_state):
|
|
|
"""Build a Replicator that connects via SSH to *conn_info*.
|
|
"""Build a Replicator that connects via SSH to *conn_info*.
|
|
|
|
|
|
|
|
- *conn_info* must contain ``ip``, ``port``, ``username``, and
|
|
|
|
|
- ``pkey_path`` keys. An optional ``use_tunnel`` key forces the
|
|
|
|
|
- replicator client to connect through an SSH tunnel instead of
|
|
|
|
|
- directly to the replicator's TCP port.
|
|
|
|
|
|
|
+ *conn_info* must contain ``ip``, ``port``, ``username``, and a ``pkey``, as
|
|
|
|
|
+ returned by ``TestProviderMixin._create_minion``'s ``ssh_connection_info``.
|
|
|
|
|
+ An optional ``use_tunnel`` key forces the replicator client to connect through
|
|
|
|
|
+ an SSH tunnel instead of directly to the replicator's TCP port.
|
|
|
"""
|
|
"""
|
|
|
- pkey = paramiko.RSAKey.from_private_key_file(conn_info["pkey_path"])
|
|
|
|
|
- repl_conn_info = {
|
|
|
|
|
- "ip": conn_info["ip"],
|
|
|
|
|
- "port": conn_info.get("port", 22),
|
|
|
|
|
- "username": conn_info.get("username", "root"),
|
|
|
|
|
- "pkey": pkey,
|
|
|
|
|
- }
|
|
|
|
|
return replicator_module.Replicator(
|
|
return replicator_module.Replicator(
|
|
|
- repl_conn_info,
|
|
|
|
|
|
|
+ conn_info,
|
|
|
event_mgr,
|
|
event_mgr,
|
|
|
volumes_info,
|
|
volumes_info,
|
|
|
repl_state,
|
|
repl_state,
|
|
@@ -154,23 +150,6 @@ class TestExportProvider(
|
|
|
_allow_loop_devices=True,
|
|
_allow_loop_devices=True,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- # BaseProvider / BaseEndpointProvider
|
|
|
|
|
-
|
|
|
|
|
- def get_connection_info_schema(self):
|
|
|
|
|
- return {
|
|
|
|
|
- "type": "object",
|
|
|
|
|
- "properties": {
|
|
|
|
|
- "pkey_path": {"type": "string"},
|
|
|
|
|
- "role": {"type": "string"},
|
|
|
|
|
- },
|
|
|
|
|
- "required": ["pkey_path"],
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- def validate_connection(self, ctxt, connection_info):
|
|
|
|
|
- pkey_path = connection_info["pkey_path"]
|
|
|
|
|
- if not os.path.exists(pkey_path):
|
|
|
|
|
- raise ValueError("SSH private key not found: %s" % pkey_path)
|
|
|
|
|
-
|
|
|
|
|
# BaseExportInstanceProvider
|
|
# BaseExportInstanceProvider
|
|
|
|
|
|
|
|
def get_source_environment_schema(self):
|
|
def get_source_environment_schema(self):
|
|
@@ -313,28 +292,17 @@ class TestExportProvider(
|
|
|
):
|
|
):
|
|
|
block_devices = source_environment.get("instance_block_devices", {})
|
|
block_devices = source_environment.get("instance_block_devices", {})
|
|
|
block_device_paths = block_devices.get(export_info["instance_name"], [])
|
|
block_device_paths = block_devices.get(export_info["instance_name"], [])
|
|
|
- pkey_path = connection_info["pkey_path"]
|
|
|
|
|
-
|
|
|
|
|
- container_name = "coriolis-replicator-%s" % uuid.uuid4().hex[:8]
|
|
|
|
|
- container_id = test_utils.run_container(
|
|
|
|
|
- test_utils.DATA_MINION_IMAGE,
|
|
|
|
|
- container_name,
|
|
|
|
|
- is_systemd=True,
|
|
|
|
|
- ssh_key=f"{pkey_path}.pub",
|
|
|
|
|
|
|
+
|
|
|
|
|
+ info = self._create_minion(
|
|
|
|
|
+ "coriolis-replicator",
|
|
|
|
|
+ connection_info,
|
|
|
devices=block_device_paths,
|
|
devices=block_device_paths,
|
|
|
)
|
|
)
|
|
|
|
|
+ container_id = info["container_id"]
|
|
|
|
|
+ src_conn_info = info["ssh_connection_info"]
|
|
|
|
|
+ src_conn_info["use_tunnel"] = source_environment.get("use_tunnel", False)
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
- container_ip = test_utils.get_container_ip(container_id)
|
|
|
|
|
- test_utils.wait_for_ssh(container_ip, 22, "root", pkey_path)
|
|
|
|
|
-
|
|
|
|
|
- src_conn_info = {
|
|
|
|
|
- "ip": container_ip,
|
|
|
|
|
- "port": 22,
|
|
|
|
|
- "username": "root",
|
|
|
|
|
- "pkey_path": pkey_path,
|
|
|
|
|
- "use_tunnel": source_environment.get("use_tunnel", False),
|
|
|
|
|
- }
|
|
|
|
|
replicator = self._make_replicator(
|
|
replicator = self._make_replicator(
|
|
|
src_conn_info, self._event_manager(), [], None
|
|
src_conn_info, self._event_manager(), [], None
|
|
|
)
|
|
)
|
|
@@ -375,13 +343,34 @@ class TestExportProvider(
|
|
|
):
|
|
):
|
|
|
repl_state = _extract_repl_state(volumes_info) if incremental else None
|
|
repl_state = _extract_repl_state(volumes_info) if incremental else None
|
|
|
|
|
|
|
|
|
|
+ disk_mappings = source_resources.get("disk_mappings")
|
|
|
|
|
+ reused_minion = disk_mappings is None
|
|
|
|
|
+ if disk_mappings is None:
|
|
|
|
|
+ # Minion pool case: "source_resources" only carries the pool minion's
|
|
|
|
|
+ # container_id. the block devices were never attached to the container,
|
|
|
|
|
+ # so hotplug them now.
|
|
|
|
|
+ container_id = source_resources["container_id"]
|
|
|
|
|
+ block_devices = source_environment.get("instance_block_devices", {})
|
|
|
|
|
+ block_device_paths = block_devices.get(instance_name, [])
|
|
|
|
|
+ for path in block_device_paths:
|
|
|
|
|
+ test_utils.hotplug_device_to_container(container_id, path)
|
|
|
|
|
+
|
|
|
|
|
+ disk_mappings = {
|
|
|
|
|
+ os.path.basename(path): path for path in block_device_paths
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
replicator = self._make_replicator(
|
|
replicator = self._make_replicator(
|
|
|
source_conn_info, self._event_manager(), volumes_info, repl_state
|
|
source_conn_info, self._event_manager(), volumes_info, repl_state
|
|
|
)
|
|
)
|
|
|
replicator.init_replicator()
|
|
replicator.init_replicator()
|
|
|
|
|
+ if reused_minion and incremental:
|
|
|
|
|
+ # The pool minion's replicator process persists across executions and it
|
|
|
|
|
+ # only computes whole-disk checksums once, at process startup. Without
|
|
|
|
|
+ # restarting it, "verify_disk_integrity" would keep comparing against that
|
|
|
|
|
+ # stale, pre-sync checksum on every subsequent execution against the minion.
|
|
|
|
|
+ replicator.update_state(repl_state or [], restart=True)
|
|
|
replicator.wait_for_chunks()
|
|
replicator.wait_for_chunks()
|
|
|
|
|
|
|
|
- disk_mappings = source_resources.get("disk_mappings", {})
|
|
|
|
|
source_volumes_info = [
|
|
source_volumes_info = [
|
|
|
{
|
|
{
|
|
|
"disk_id": vol["disk_id"],
|
|
"disk_id": vol["disk_id"],
|
|
@@ -429,6 +418,45 @@ class TestExportProvider(
|
|
|
):
|
|
):
|
|
|
return {}
|
|
return {}
|
|
|
|
|
|
|
|
|
|
+ # BaseSourceMinionPoolProvider
|
|
|
|
|
+
|
|
|
|
|
+ def get_minion_pool_environment_schema(self):
|
|
|
|
|
+ return self.get_source_environment_schema()
|
|
|
|
|
+
|
|
|
|
|
+ def get_minion_pool_options(
|
|
|
|
|
+ self, ctxt, connection_info, env=None, option_names=None
|
|
|
|
|
+ ):
|
|
|
|
|
+ return self.get_source_environment_options(
|
|
|
|
|
+ ctxt, connection_info, env, option_names
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def create_minion(
|
|
|
|
|
+ self,
|
|
|
|
|
+ ctxt,
|
|
|
|
|
+ connection_info,
|
|
|
|
|
+ environment_options,
|
|
|
|
|
+ pool_identifier,
|
|
|
|
|
+ pool_os_type,
|
|
|
|
|
+ pool_shared_resources,
|
|
|
|
|
+ new_minion_identifier,
|
|
|
|
|
+ ):
|
|
|
|
|
+ # Devices are hotplugged after container creation via mknod / nsenter.
|
|
|
|
|
+ # We must pre-authorize all block devices through the
|
|
|
|
|
+ # --device-cgroup-rule option, otherwise any device added will be
|
|
|
|
|
+ # inaccessible ("operation not permitted" error on open).
|
|
|
|
|
+ result = self._create_minion(
|
|
|
|
|
+ "coriolis-pool-minion",
|
|
|
|
|
+ connection_info,
|
|
|
|
|
+ device_cgroup_rules=["b *:* rwm"],
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "connection_info": result["ssh_connection_info"],
|
|
|
|
|
+ "minion_provider_properties": {
|
|
|
|
|
+ "container_id": result["container_id"],
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
|
|
|
# Helpers
|
|
# Helpers
|
|
|
def _get_block_device_size(device):
|
|
def _get_block_device_size(device):
|