Merge "Update integrated template to run grenade-skip-level-always on stable/2026.1"
diff --git a/releasenotes/notes/add-volume-replication-config-option-e3f7a9c2b5d4f8e1.yaml b/releasenotes/notes/add-volume-replication-config-option-e3f7a9c2b5d4f8e1.yaml
new file mode 100644
index 0000000..77ad702
--- /dev/null
+++ b/releasenotes/notes/add-volume-replication-config-option-e3f7a9c2b5d4f8e1.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ A new config option ``replication`` in the ``volume-feature-enabled``
+ section is added. This boolean option (default: False) allows toggling
+ of Cinder volume replication-related test execution. When enabled, it
+ indicates that the deployment supports volume replication features and
+ replication tests should be executed.
diff --git a/releasenotes/notes/cinder-replication-support-a7b2c4d8e9f1a3b5.yaml b/releasenotes/notes/cinder-replication-support-a7b2c4d8e9f1a3b5.yaml
new file mode 100644
index 0000000..e8cec72
--- /dev/null
+++ b/releasenotes/notes/cinder-replication-support-a7b2c4d8e9f1a3b5.yaml
@@ -0,0 +1,13 @@
+---
+features:
+ - |
+ Added support for the Cinder ``failover_host`` admin action in the
+ volume v3 services client. This API allows triggering a failover
+ operation on a Cinder backend host, which is essential for testing
+ volume replication and disaster recovery scenarios.
+ - |
+ Added a new waiter function ``wait_for_volume_replication_status()`` in
+ the ``tempest.common.waiters`` module. This waiter polls a volume until
+ it reaches the expected replication_status value, with configurable
+ timeout and interval. This is useful for validating replication state
+ transitions such as 'enabled', 'copying', 'active', and 'failed-over'.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index 29ec4d5..3e88a93 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,6 +6,8 @@
:maxdepth: 1
unreleased
+ v46.2.0
+ v46.1.0
v46.0.0
v45.0.0
v44.0.0
diff --git a/releasenotes/source/v46.1.0.rst b/releasenotes/source/v46.1.0.rst
new file mode 100644
index 0000000..dc0453d
--- /dev/null
+++ b/releasenotes/source/v46.1.0.rst
@@ -0,0 +1,6 @@
+=====================
+v46.1.0 Release Notes
+=====================
+
+.. release-notes:: 46.1.0 Release Notes
+ :version: 46.1.0
diff --git a/releasenotes/source/v46.2.0.rst b/releasenotes/source/v46.2.0.rst
new file mode 100644
index 0000000..8b5f085
--- /dev/null
+++ b/releasenotes/source/v46.2.0.rst
@@ -0,0 +1,6 @@
+=====================
+v46.2.0 Release Notes
+=====================
+
+.. release-notes:: 46.2.0 Release Notes
+ :version: 46.2.0
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index b4312b7..fa01dc9 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -433,6 +433,29 @@
'seconds', attachment_id, volume_id, time.time() - start)
+def wait_for_volume_replication_status(client, volume_id, expected_status):
+ """Waits for a volume to reach the expected replication_status."""
+ start = int(time.time())
+ volume = client.show_volume(volume_id)['volume']
+ current_status = volume['replication_status']
+
+ while current_status != expected_status:
+ if int(time.time()) - start >= client.build_timeout:
+ message = ('Timeout waiting for volume %s to reach '
+ 'replication_status "%s". Last known status: "%s" '
+ '(waited %s seconds).' %
+ (volume_id, expected_status, current_status,
+ client.build_timeout))
+ raise lib_exc.TimeoutException(message)
+
+ time.sleep(client.build_interval)
+ volume = client.show_volume(volume_id)['volume']
+ current_status = volume['replication_status']
+
+ LOG.info('Volume %s reached replication_status "%s" after %f seconds',
+ volume_id, expected_status, time.time() - start)
+
+
def wait_for_volume_attachment_remove_from_server(
client, server_id, volume_id):
"""Waits for a volume to be removed from a given server.
diff --git a/tempest/config.py b/tempest/config.py
index f176218..05f323f 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -1125,6 +1125,9 @@
cfg.BoolOpt('manage_volume',
default=False,
help='Runs Cinder manage volume tests'),
+ cfg.BoolOpt('replication',
+ default=False,
+ help='Runs Cinder volume replication tests'),
cfg.ListOpt('api_extensions',
default=['all'],
help='A list of enabled volume extensions with a special '
diff --git a/tempest/lib/api_schema/response/volume/services.py b/tempest/lib/api_schema/response/volume/services.py
index 216631c..849806b 100644
--- a/tempest/lib/api_schema/response/volume/services.py
+++ b/tempest/lib/api_schema/response/volume/services.py
@@ -86,3 +86,4 @@
freeze_host = {'status_code': [200]}
thaw_host = {'status_code': [200]}
+failover_host = {'status_code': [202]}
diff --git a/tempest/lib/services/volume/v3/services_client.py b/tempest/lib/services/volume/v3/services_client.py
index 1111f81..ca01402 100644
--- a/tempest/lib/services/volume/v3/services_client.py
+++ b/tempest/lib/services/volume/v3/services_client.py
@@ -109,3 +109,15 @@
resp, body = self.put('os-services/thaw', put_body)
self.validate_response(schema.thaw_host, resp, body)
return rest_client.ResponseBody(resp)
+
+ def failover_host(self, **kwargs):
+ """Failover a Cinder Backend Host.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://docs.openstack.org/api-ref/block-storage/v3/#failover-a-cinder-backend-host
+ """
+ put_body = json.dumps(kwargs)
+ resp, body = self.put('os-services/failover_host', put_body)
+ self.validate_response(schema.failover_host, resp, body)
+ return rest_client.ResponseBody(resp)
diff --git a/tempest/tests/common/test_waiters.py b/tempest/tests/common/test_waiters.py
old mode 100755
new mode 100644
index f7f2dc7..dcb0de8
--- a/tempest/tests/common/test_waiters.py
+++ b/tempest/tests/common/test_waiters.py
@@ -644,6 +644,42 @@
# Assert that show volume is only called once before we return
show_volume.assert_called_once_with(uuids.volume_id)
+ def test_wait_for_volume_replication_status(self):
+ vol_replicating = {'volume': {'replication_status': 'copying'}}
+ vol_active = {'volume': {'replication_status': 'active'}}
+ show_volume = mock.MagicMock(side_effect=[
+ vol_replicating,
+ vol_replicating,
+ vol_active])
+ client = mock.Mock(spec=volumes_client.VolumesClient,
+ resource_type="volume",
+ build_interval=1,
+ build_timeout=5,
+ show_volume=show_volume)
+ self.patch('time.time', return_value=0.)
+ self.patch('time.sleep')
+ waiters.wait_for_volume_replication_status(
+ client, mock.sentinel.volume_id, 'active')
+ # Assert that show volume is called until the expected status is
+ # reached
+ show_volume.assert_has_calls([mock.call(mock.sentinel.volume_id),
+ mock.call(mock.sentinel.volume_id),
+ mock.call(mock.sentinel.volume_id)])
+
+ def test_wait_for_volume_replication_status_timeout(self):
+ vol_replicating = {'volume': {'replication_status': 'copying'}}
+ show_volume = mock.MagicMock(return_value=vol_replicating)
+ client = mock.Mock(spec=volumes_client.VolumesClient,
+ resource_type="volume",
+ build_interval=1,
+ build_timeout=1,
+ show_volume=show_volume)
+ self.patch('time.time', side_effect=[0., client.build_timeout + 1.])
+ self.patch('time.sleep')
+ self.assertRaises(lib_exc.TimeoutException,
+ waiters.wait_for_volume_replication_status,
+ client, mock.sentinel.volume_id, 'active')
+
def test_wait_for_volume_attachment_remove_from_server(self):
volume_attached = {
"volumeAttachments": [{"volumeId": uuids.volume_id}]}
diff --git a/tempest/tests/lib/services/volume/v3/test_services_client.py b/tempest/tests/lib/services/volume/v3/test_services_client.py
index c807bc2..c8d9a9a 100644
--- a/tempest/tests/lib/services/volume/v3/test_services_client.py
+++ b/tempest/tests/lib/services/volume/v3/test_services_client.py
@@ -173,6 +173,16 @@
bytes_body,
**kwargs)
+ def _test_failover_host(self, bytes_body=False):
+ kwargs = {'host': 'host1@lvm', 'backend_id': 'backend2'}
+ self.check_service_client_function(
+ self.client.failover_host,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ status=202,
+ **kwargs)
+
def test_list_services_with_str_body(self):
self._test_list_services()
@@ -212,3 +222,9 @@
def test_thaw_host_with_bytes_body(self):
self._test_thaw_host(bytes_body=True)
+
+ def test_failover_host_with_str_body(self):
+ self._test_failover_host()
+
+ def test_failover_host_with_bytes_body(self):
+ self._test_failover_host(bytes_body=True)
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index 0690d57..a22e8a4 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -83,6 +83,9 @@
# in this plugin was 7 years ago.
# https://opendev.org/airship/tempest-plugin
'airship/tempest-plugin'
+ # It is broken by the below change:
+ # https://review.opendev.org/c/x/networking-cisco/+/969038
+ 'x/networking-cisco'
]
url = 'https://review.opendev.org/projects/'