Merge "Add 'doc' tag for bug link on doc theme"
diff --git a/.zuul.yaml b/.zuul.yaml
index 2c066ae..f2ca000 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -9,7 +9,7 @@
- zuul: openstack-dev/devstack
vars:
devstack_services:
- tempest: True
+ tempest: true
run: playbooks/devstack-tempest.yaml
post-run: playbooks/post-tempest.yaml
@@ -29,16 +29,22 @@
vars:
tox_envlist: full
devstack_localrc:
- ENABLE_FILE_INJECTION: True
+ ENABLE_FILE_INJECTION: true
- job:
name: tempest-full-py3
- parent: tempest-full
+ parent: devstack-tempest
+ branches: ^(?!driverfixes/)master$
+ description: |
+ Base integration test with Neutron networking and py3.
+ Former names for this job where:
+ * legacy-tempest-dsvm-py35
+ * gate-tempest-dsvm-py35
vars:
+ tox_envlist: full
devstack_localrc:
USE_PYTHON3: True
FORCE_CONFIG_DRIVE: True
- ENABLE_FILE_INJECTION: False
devstack_services:
s-account: false
s-container: false
@@ -124,7 +130,6 @@
- openstack/zun-tempest-plugin
- project:
- name: openstack/tempest
check:
jobs:
- devstack-tempest:
@@ -132,15 +137,8 @@
- ^playbooks/
- ^roles/
- ^.zuul.yaml$
- - tempest-full-py3:
- voting: false
- irrelevant-files:
- - ^(test-|)requirements.txt$
- - ^.*\.rst$
- - ^doc/.*$
- - ^etc/.*$
- - ^releasenotes/.*$
- - ^setup.cfg$
- - ^tempest/hacking/.*$
- - ^tempest/tests/.*$
+ - nova-multiattach
- tempest-tox-plugin-sanity-check
+ gate:
+ jobs:
+ - nova-multiattach
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 9c4ac0b..942f969 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -342,6 +342,10 @@
.. _2.48: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id43
+ * `2.60`_
+
+ .. _2.60: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id54
+
* Volume
* `3.3`_
diff --git a/releasenotes/notes/config-volume-multiattach-ea8138dfa4fd308c.yaml b/releasenotes/notes/config-volume-multiattach-ea8138dfa4fd308c.yaml
new file mode 100644
index 0000000..8d53dda
--- /dev/null
+++ b/releasenotes/notes/config-volume-multiattach-ea8138dfa4fd308c.yaml
@@ -0,0 +1,12 @@
+---
+other:
+ - |
+ A new configuration option ``[compute-feature-enabled]/volume_multiattach``
+ has been added which defaults to False. Set this to True to enable volume
+ multiattach testing. These tests require that compute API version 2.60 is
+ available and block storage API version 3.44 is available.
+
+ .. note:: In the Queens release, the only compute driver that supports
+ volume multiattach is the libvirt driver, and only then when qemu<2.10
+ or libvirt>=3.10. The only volume backend in Queens that supports volume
+ multiattach is lvm.
diff --git a/roles/process-stackviz/README.rst b/roles/process-stackviz/README.rst
index b05326d..54c217b 100644
--- a/roles/process-stackviz/README.rst
+++ b/roles/process-stackviz/README.rst
@@ -11,7 +11,7 @@
The devstack base directory.
.. zuul:rolevar:: stage_dir
- :default: /opt/stack/logs
+ :default: "{{ ansible_user_dir }}"
The stage directory where the input data can be found and
the output will be produced.
diff --git a/roles/process-stackviz/defaults/main.yaml b/roles/process-stackviz/defaults/main.yaml
index b1eb8d9..c6a64d1 100644
--- a/roles/process-stackviz/defaults/main.yaml
+++ b/roles/process-stackviz/defaults/main.yaml
@@ -1,3 +1,3 @@
devstack_base_dir: /opt/stack
-stage_dir: /opt/stack/
+stage_dir: "{{ ansible_user_dir }}"
test_results_stage_name: test_results
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 9e897e3..2398cf1 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -158,14 +158,11 @@
self.attach_volume(server, volume, device='/dev/xvdb')
server = self.admin_servers_client.show_server(server_id)['server']
volume_id1 = server["os-extended-volumes:volumes_attached"][0]["id"]
- self._migrate_server_to(server_id, target_host)
- waiters.wait_for_server_status(self.servers_client,
- server_id, 'ACTIVE')
+ self._live_migrate(server_id, target_host, 'ACTIVE')
server = self.admin_servers_client.show_server(server_id)['server']
volume_id2 = server["os-extended-volumes:volumes_attached"][0]["id"]
- self.assertEqual(target_host, self.get_host_for_server(server_id))
self.assertEqual(volume_id1, volume_id2)
@@ -204,10 +201,7 @@
self._verify_console_interaction(server01_id)
self._verify_console_interaction(server02_id)
- self._migrate_server_to(server01_id, host02_id)
- waiters.wait_for_server_status(self.servers_client,
- server01_id, 'ACTIVE')
- self.assertEqual(host02_id, self.get_host_for_server(server01_id))
+ self._live_migrate(server01_id, host02_id, 'ACTIVE')
self._verify_console_interaction(server01_id)
# At this point, both instances have a valid serial console
# connection, which means the ports got updated.
diff --git a/tempest/api/compute/admin/test_volume_swap.py b/tempest/api/compute/admin/test_volume_swap.py
index d715a42..99bad8f 100644
--- a/tempest/api/compute/admin/test_volume_swap.py
+++ b/tempest/api/compute/admin/test_volume_swap.py
@@ -22,30 +22,16 @@
CONF = config.CONF
-class TestVolumeSwap(base.BaseV2ComputeAdminTest):
- """The test suite for swapping of volume with admin user.
-
- The following is the scenario outline:
- 1. Create a volume "volume1" with non-admin.
- 2. Create a volume "volume2" with non-admin.
- 3. Boot an instance "instance1" with non-admin.
- 4. Attach "volume1" to "instance1" with non-admin.
- 5. Swap volume from "volume1" to "volume2" as admin.
- 6. Check the swap volume is successful and "volume2"
- is attached to "instance1" and "volume1" is in available state.
- 7. Swap volume from "volume2" to "volume1" as admin.
- 8. Check the swap volume is successful and "volume1"
- is attached to "instance1" and "volume2" is in available state.
- """
+class TestVolumeSwapBase(base.BaseV2ComputeAdminTest):
@classmethod
def skip_checks(cls):
- super(TestVolumeSwap, cls).skip_checks()
+ super(TestVolumeSwapBase, cls).skip_checks()
if not CONF.compute_feature_enabled.swap_volume:
raise cls.skipException("Swapping volumes is not supported.")
- def _wait_for_server_volume_swap(self, server_id, old_volume_id,
- new_volume_id):
+ def wait_for_server_volume_swap(self, server_id, old_volume_id,
+ new_volume_id):
"""Waits for a server to swap the old volume to a new one."""
volume_attachments = self.servers_client.list_volume_attachments(
server_id)['volumeAttachments']
@@ -79,6 +65,23 @@
'timeout': self.servers_client.build_timeout})
raise lib_exc.TimeoutException(message)
+
+class TestVolumeSwap(TestVolumeSwapBase):
+ """The test suite for swapping of volume with admin user.
+
+ The following is the scenario outline:
+ 1. Create a volume "volume1" with non-admin.
+ 2. Create a volume "volume2" with non-admin.
+ 3. Boot an instance "instance1" with non-admin.
+ 4. Attach "volume1" to "instance1" with non-admin.
+ 5. Swap volume from "volume1" to "volume2" as admin.
+ 6. Check the swap volume is successful and "volume2"
+ is attached to "instance1" and "volume1" is in available state.
+ 7. Swap volume from "volume2" to "volume1" as admin.
+ 8. Check the swap volume is successful and "volume1"
+ is attached to "instance1" and "volume2" is in available state.
+ """
+
@decorators.idempotent_id('1769f00d-a693-4d67-a631-6a3496773813')
@utils.services('volume')
def test_volume_swap(self):
@@ -99,8 +102,8 @@
volume1['id'], 'available')
waiters.wait_for_volume_resource_status(self.volumes_client,
volume2['id'], 'in-use')
- self._wait_for_server_volume_swap(server['id'], volume1['id'],
- volume2['id'])
+ self.wait_for_server_volume_swap(server['id'], volume1['id'],
+ volume2['id'])
# Verify "volume2" is attached to the server
vol_attachments = self.servers_client.list_volume_attachments(
server['id'])['volumeAttachments']
@@ -114,10 +117,64 @@
volume2['id'], 'available')
waiters.wait_for_volume_resource_status(self.volumes_client,
volume1['id'], 'in-use')
- self._wait_for_server_volume_swap(server['id'], volume2['id'],
- volume1['id'])
+ self.wait_for_server_volume_swap(server['id'], volume2['id'],
+ volume1['id'])
# Verify "volume1" is attached to the server
vol_attachments = self.servers_client.list_volume_attachments(
server['id'])['volumeAttachments']
self.assertEqual(1, len(vol_attachments))
self.assertIn(volume1['id'], vol_attachments[0]['volumeId'])
+
+
+class AttachVolumeMultiAttachTest(TestVolumeSwapBase):
+ min_microversion = '2.60'
+ max_microversion = 'latest'
+
+ @classmethod
+ def skip_checks(cls):
+ super(AttachVolumeMultiAttachTest, cls).skip_checks()
+ if not CONF.compute_feature_enabled.volume_multiattach:
+ raise cls.skipException('Volume multi-attach is not available.')
+
+ @decorators.idempotent_id('e8f8f9d1-d7b7-4cd2-8213-ab85ef697b6e')
+ @utils.services('volume')
+ def test_volume_swap_with_multiattach(self):
+ # Create two volumes.
+ # NOTE(gmann): Volumes are created before server creation so that
+ # volumes cleanup can happen successfully irrespective of which volume
+ # is attached to server.
+ volume1 = self.create_volume(multiattach=True)
+ volume2 = self.create_volume(multiattach=True)
+
+ # Boot server1
+ server1 = self.create_test_server(wait_until='ACTIVE')
+ # Attach volume1 to server1
+ self.attach_volume(server1, volume1)
+ # Boot server2
+ server2 = self.create_test_server(wait_until='ACTIVE')
+ # Attach volume1 to server2
+ self.attach_volume(server2, volume1)
+
+ # Swap volume1 to volume2 on server1, volume1 should remain attached
+ # to server 2
+ self.admin_servers_client.update_attached_volume(
+ server1['id'], volume1['id'], volumeId=volume2['id'])
+ # volume1 will return to in-use after the swap
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume1['id'], 'in-use')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume2['id'], 'in-use')
+ self.wait_for_server_volume_swap(server1['id'], volume1['id'],
+ volume2['id'])
+
+ # Verify volume2 is attached to server1
+ vol_attachments = self.servers_client.list_volume_attachments(
+ server1['id'])['volumeAttachments']
+ self.assertEqual(1, len(vol_attachments))
+ self.assertIn(volume2['id'], vol_attachments[0]['volumeId'])
+
+ # Verify volume1 is still attached to server2
+ vol_attachments = self.servers_client.list_volume_attachments(
+ server2['id'])['volumeAttachments']
+ self.assertEqual(1, len(vol_attachments))
+ self.assertIn(volume1['id'], vol_attachments[0]['volumeId'])
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index e6184b7..caa445d 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -13,8 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest.api.compute import base
from tempest.common import compute
+from tempest.common import utils
from tempest.common.utils.linux import remote_client
from tempest.common import waiters
from tempest import config
@@ -261,3 +264,177 @@
# volume(s)
self._unshelve_server_and_check_volumes(
server, validation_resources, num_vol)
+
+
+class AttachVolumeMultiAttachTest(BaseAttachVolumeTest):
+ min_microversion = '2.60'
+ max_microversion = 'latest'
+
+ @classmethod
+ def skip_checks(cls):
+ super(AttachVolumeMultiAttachTest, cls).skip_checks()
+ if not CONF.compute_feature_enabled.volume_multiattach:
+ raise cls.skipException('Volume multi-attach is not available.')
+
+ def _attach_volume_to_servers(self, volume, servers):
+ """Attaches the given volume to the list of servers.
+
+ :param volume: The multiattach volume to use.
+ :param servers: list of server instances on which the volume will be
+ attached
+ :returns: dict of server ID to volumeAttachment dict entries
+ """
+ attachments = {}
+ for server in servers:
+ # map the server id to the volume attachment
+ attachments[server['id']] = self.attach_volume(server, volume)
+ # NOTE(mriedem): In the case of multi-attach, after the first
+ # attach the volume will be in-use. On the second attach, nova will
+ # 'reserve' the volume which puts it back into 'attaching' status
+ # and then the volume shouldn't go back to in-use until the compute
+ # actually attaches the server to the volume.
+ return attachments
+
+ def _detach_multiattach_volume(self, volume_id, server_id):
+ """Detaches a multiattach volume from the given server.
+
+ Depending on the number of attachments the volume has, this method
+ will wait for the volume to go to back to 'in-use' status if there are
+ more attachments or 'available' state if there are no more attachments.
+ """
+ # Count the number of attachments before starting the detach.
+ volume = self.volumes_client.show_volume(volume_id)['volume']
+ attachments = volume['attachments']
+ wait_status = 'in-use' if len(attachments) > 1 else 'available'
+ # Now detach the volume from the given server.
+ self.servers_client.detach_volume(server_id, volume_id)
+ # Now wait for the volume status to change.
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, volume_id, wait_status)
+
+ def _create_multiattach_volume(self, bootable=False):
+ kwargs = {}
+ if bootable:
+ kwargs['image_ref'] = CONF.compute.image_ref
+ return self.create_volume(multiattach=True, **kwargs)
+
+ def _create_and_multiattach(self):
+ """Creates two server instances and a volume and attaches to both.
+
+ :returns: A three-item tuple of the list of created servers,
+ the created volume, and dict of server ID to volumeAttachment
+ dict entries
+ """
+ servers = []
+ for x in range(2):
+ name = 'multiattach-server-%i' % x
+ servers.append(self.create_test_server(name=name))
+
+ # Now wait for the servers to be ACTIVE.
+ for server in servers:
+ waiters.wait_for_server_status(self.servers_client, server['id'],
+ 'ACTIVE')
+
+ volume = self._create_multiattach_volume()
+
+ # Attach the volume to the servers
+ attachments = self._attach_volume_to_servers(volume, servers)
+ return servers, volume, attachments
+
+ @decorators.idempotent_id('8d5853f7-56e7-4988-9b0c-48cea3c7049a')
+ def test_list_get_volume_attachments_multiattach(self):
+ # Attach a single volume to two servers.
+ servers, volume, attachments = self._create_and_multiattach()
+
+ # List attachments from the volume and make sure the server uuids
+ # are in that list.
+ vol_attachments = self.volumes_client.show_volume(
+ volume['id'])['volume']['attachments']
+ attached_server_ids = [attachment['server_id']
+ for attachment in vol_attachments]
+ self.assertEqual(2, len(attached_server_ids))
+
+ # List Volume attachment of the servers
+ for server in servers:
+ self.assertIn(server['id'], attached_server_ids)
+ vol_attachments = self.servers_client.list_volume_attachments(
+ server['id'])['volumeAttachments']
+ self.assertEqual(1, len(vol_attachments))
+ attachment = attachments[server['id']]
+ self.assertDictEqual(attachment, vol_attachments[0])
+ # Detach the volume from this server.
+ self._detach_multiattach_volume(volume['id'], server['id'])
+
+ def _boot_from_multiattach_volume(self):
+ """Boots a server from a multiattach volume.
+
+ The volume will not be deleted when the server is deleted.
+
+ :returns: 2-item tuple of (server, volume)
+ """
+ volume = self._create_multiattach_volume(bootable=True)
+ # Now create a server from the bootable volume.
+ bdm = [{
+ 'uuid': volume['id'],
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'boot_index': 0,
+ 'delete_on_termination': False}]
+ server = self.create_test_server(
+ image_id='', block_device_mapping_v2=bdm, wait_until='ACTIVE')
+ # Assert the volume is attached to the server.
+ attachments = self.servers_client.list_volume_attachments(
+ server['id'])['volumeAttachments']
+ self.assertEqual(1, len(attachments))
+ self.assertEqual(volume['id'], attachments[0]['volumeId'])
+ return server, volume
+
+ @decorators.idempotent_id('65e33aa2-185b-44c8-b22e-e524973ed625')
+ def test_boot_from_multiattach_volume(self):
+ """Simple test to boot an instance from a multiattach volume."""
+ self._boot_from_multiattach_volume()
+
+ @utils.services('image')
+ @decorators.idempotent_id('885ac48a-2d7a-40c5-ae8b-1993882d724c')
+ def test_snapshot_volume_backed_multiattach(self):
+ """Boots a server from a multiattach volume and snapshots the server.
+
+ Creating the snapshot of the server will also create a snapshot of
+ the volume.
+ """
+ server, volume = self._boot_from_multiattach_volume()
+ # Create a snapshot of the server (and volume implicitly).
+ self.create_image_from_server(
+ server['id'], name='multiattach-snapshot',
+ wait_until='active', wait_for_server=True)
+ # TODO(mriedem): Make sure the volume snapshot exists. This requires
+ # adding the volume snapshots client to BaseV2ComputeTest.
+ # Delete the server, wait for it to be gone, and make sure the volume
+ # still exists.
+ self.servers_client.delete_server(server['id'])
+ waiters.wait_for_server_termination(self.servers_client, server['id'])
+ # Delete the volume and cascade the delete of the volume snapshot.
+ self.volumes_client.delete_volume(volume['id'], cascade=True)
+ # Now we have to wait for the volume to be gone otherwise the normal
+ # teardown will fail since it will race with our call and the snapshot
+ # might still exist.
+ self.volumes_client.wait_for_resource_deletion(volume['id'])
+
+ @decorators.idempotent_id('f01c7169-a124-4fc7-ae60-5e380e247c9c')
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
+ def test_resize_server_with_multiattached_volume(self):
+ # Attach a single volume to multiple servers, then resize the servers
+ servers, volume, _ = self._create_and_multiattach()
+
+ for server in servers:
+ self.resize_server(server['id'], self.flavor_ref_alt)
+
+ for server in servers:
+ self._detach_multiattach_volume(volume['id'], server['id'])
+
+ # TODO(mriedem): Might be interesting to create a bootable multiattach
+ # volume with delete_on_termination=True, create server1 from the
+ # volume, then attach it to server2, and then delete server1 in which
+ # case the volume won't be deleted because it's still attached to
+ # server2 and make sure the volume is still attached to server2.
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index 7103d56..142e3c2 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -183,23 +183,3 @@
disk_format='raw')
self.addCleanup(self.client.delete_image, image['id'])
return image['id']
-
-
-class BaseV1ImageAdminTest(BaseImageTest):
- credentials = ['admin', 'primary']
-
- @classmethod
- def setup_clients(cls):
- super(BaseV1ImageAdminTest, cls).setup_clients()
- cls.client = cls.os_primary.image_client
- cls.admin_client = cls.os_admin.image_client
-
-
-class BaseV2ImageAdminTest(BaseImageTest):
- credentials = ['admin', 'primary']
-
- @classmethod
- def setup_clients(cls):
- super(BaseV2ImageAdminTest, cls).setup_clients()
- cls.client = cls.os_primary.image_client_v2
- cls.admin_client = cls.os_admin.image_client_v2
diff --git a/tempest/api/network/admin/test_ports.py b/tempest/api/network/admin/test_ports.py
index 807994b..483b405 100644
--- a/tempest/api/network/admin/test_ports.py
+++ b/tempest/api/network/admin/test_ports.py
@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import socket
-
from tempest.api.network import base
from tempest import config
from tempest.lib import decorators
@@ -25,10 +23,16 @@
class PortsAdminExtendedAttrsTestJSON(base.BaseAdminNetworkTest):
@classmethod
+ def setup_clients(cls):
+ super(PortsAdminExtendedAttrsTestJSON, cls).setup_clients()
+ cls.hyper_client = cls.os_admin.hypervisor_client
+
+ @classmethod
def resource_setup(cls):
super(PortsAdminExtendedAttrsTestJSON, cls).resource_setup()
cls.network = cls.create_network()
- cls.host_id = socket.gethostname()
+ hyper_list = cls.hyper_client.list_hypervisors()
+ cls.host_id = hyper_list['hypervisors'][0]['hypervisor_hostname']
@decorators.idempotent_id('8e8569c1-9ac7-44db-8bc1-f5fb2814f29b')
def test_create_port_binding_ext_attr(self):
diff --git a/tempest/api/network/test_ports.py b/tempest/api/network/test_ports.py
index eb53fbb..5168423 100644
--- a/tempest/api/network/test_ports.py
+++ b/tempest/api/network/test_ports.py
@@ -13,7 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
+import ipaddress
+
import netaddr
+import six
import testtools
from tempest.api.network import base_security_groups as sec_base
@@ -178,6 +181,83 @@
self.assertIn(port_1_fixed_ip, port_ips)
self.assertIn(network['id'], port_net_ids)
+ @decorators.idempotent_id('79895408-85d5-460d-94e7-9531c5fd9123')
+ @testtools.skipUnless(
+ utils.is_extension_enabled('ip-substring-filtering', 'network'),
+ 'ip-substring-filtering extension not enabled.')
+ def test_port_list_filter_by_ip_substr(self):
+ # Create network and subnet
+ network = self.create_network()
+ subnet = self.create_subnet(network)
+ self.addCleanup(self.subnets_client.delete_subnet, subnet['id'])
+
+ # Get two IP addresses
+ ip_address_1 = None
+ ip_address_2 = None
+ ip_network = ipaddress.ip_network(six.text_type(subnet['cidr']))
+ for ip in ip_network:
+ if ip == ip_network.network_address:
+ continue
+ if ip_address_1 is None:
+ ip_address_1 = six.text_type(ip)
+ else:
+ ip_address_2 = ip_address_1
+ ip_address_1 = six.text_type(ip)
+ # Make sure these two IP addresses have different substring
+ if ip_address_1[:-1] != ip_address_2[:-1]:
+ break
+
+ # Create two ports
+ fixed_ips = [{'subnet_id': subnet['id'], 'ip_address': ip_address_1}]
+ port_1 = self.ports_client.create_port(network_id=network['id'],
+ fixed_ips=fixed_ips)
+ self.addCleanup(self.ports_client.delete_port, port_1['port']['id'])
+ fixed_ips = [{'subnet_id': subnet['id'], 'ip_address': ip_address_2}]
+ port_2 = self.ports_client.create_port(network_id=network['id'],
+ fixed_ips=fixed_ips)
+ self.addCleanup(self.ports_client.delete_port, port_2['port']['id'])
+
+ # Scenario 1: List port1 (port2 is filtered out)
+ if ip_address_1[:-1] != ip_address_2[:-1]:
+ ips_filter = 'ip_address_substr=' + ip_address_1[:-1]
+ else:
+ ips_filter = 'ip_address_substr=' + ip_address_1
+ ports = self.ports_client.list_ports(fixed_ips=ips_filter)['ports']
+ # Check that we got the desired port
+ port_ids = [port['id'] for port in ports]
+ fixed_ips = [port['fixed_ips'] for port in ports]
+ port_ips = []
+ for addr in fixed_ips:
+ port_ips.extend([a['ip_address'] for a in addr])
+
+ port_net_ids = [port['network_id'] for port in ports]
+ self.assertIn(network['id'], port_net_ids)
+ self.assertIn(port_1['port']['id'], port_ids)
+ self.assertIn(port_1['port']['fixed_ips'][0]['ip_address'], port_ips)
+ self.assertNotIn(port_2['port']['id'], port_ids)
+ self.assertNotIn(
+ port_2['port']['fixed_ips'][0]['ip_address'], port_ips)
+
+ # Scenario 2: List both port1 and port2
+ substr = ip_address_1
+ while substr not in ip_address_2:
+ substr = substr[:-1]
+ ips_filter = 'ip_address_substr=' + substr
+ ports = self.ports_client.list_ports(fixed_ips=ips_filter)['ports']
+ # Check that we got both port
+ port_ids = [port['id'] for port in ports]
+ fixed_ips = [port['fixed_ips'] for port in ports]
+ port_ips = []
+ for addr in fixed_ips:
+ port_ips.extend([a['ip_address'] for a in addr])
+
+ port_net_ids = [port['network_id'] for port in ports]
+ self.assertIn(network['id'], port_net_ids)
+ self.assertIn(port_1['port']['id'], port_ids)
+ self.assertIn(port_1['port']['fixed_ips'][0]['ip_address'], port_ips)
+ self.assertIn(port_2['port']['id'], port_ids)
+ self.assertIn(port_2['port']['fixed_ips'][0]['ip_address'], port_ips)
+
@decorators.idempotent_id('5ad01ed0-0e6e-4c5d-8194-232801b15c72')
def test_port_list_filter_by_router_id(self):
# Create a router
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 0a6b79d..81fd6e6 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -31,6 +31,11 @@
"""Base test case class for all Cinder API tests."""
_api_version = 2
+ # if api_v2 is not enabled while api_v3 is enabled, the volume v2 classes
+ # should be transferred to volume v3 classes.
+ if (not CONF.volume_feature_enabled.api_v2 and
+ CONF.volume_feature_enabled.api_v3):
+ _api_version = 3
credentials = ['primary']
@classmethod
diff --git a/tempest/clients.py b/tempest/clients.py
index ca205c8..b06eafb 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -234,7 +234,9 @@
self.volumes_client = self.volume_v1.VolumesClient()
self.volumes_extension_client = self.volume_v1.ExtensionsClient()
- if CONF.volume_feature_enabled.api_v2:
+ # if only api_v3 is enabled, all these clients should be available
+ if (CONF.volume_feature_enabled.api_v2 or
+ CONF.volume_feature_enabled.api_v3):
self.backups_v2_client = self.volume_v2.BackupsClient()
self.encryption_types_v2_client = \
self.volume_v2.EncryptionTypesClient()
diff --git a/tempest/config.py b/tempest/config.py
index 231d005..4b7ace2 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -475,6 +475,11 @@
default=False,
help='Does the test environment support volume-backed live '
'migration?'),
+ cfg.BoolOpt('volume_multiattach',
+ default=False,
+ help='Does the test environment support attaching a volume to '
+ 'more than one instance? This depends on hypervisor and '
+ 'volume backend/type and compute API version 2.60.'),
]
diff --git a/tempest/lib/cli/base.py b/tempest/lib/cli/base.py
index 3a97801..3fb56ec 100644
--- a/tempest/lib/cli/base.py
+++ b/tempest/lib/cli/base.py
@@ -58,8 +58,6 @@
if six.PY2:
cmd = cmd.encode('utf-8')
cmd = shlex.split(cmd)
- result = ''
- result_err = ''
stdout = subprocess.PIPE
stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE
proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
diff --git a/tempest/scenario/test_minimum_basic.py b/tempest/scenario/test_minimum_basic.py
index 29f1743..2b35e45 100644
--- a/tempest/scenario/test_minimum_basic.py
+++ b/tempest/scenario/test_minimum_basic.py
@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import testtools
-
from tempest.common import custom_matchers
from tempest.common import utils
from tempest.common import waiters
@@ -101,10 +99,6 @@
return address
@decorators.idempotent_id('bdbb5441-9204-419d-a225-b4fdbfb1a1a8')
- @testtools.skipUnless(CONF.network.public_network_id,
- 'The public_network_id option must be specified.')
- @testtools.skipUnless(CONF.network_feature_enabled.floating_ips,
- 'Floating ips are not available')
@utils.services('compute', 'volume', 'image', 'network')
def test_minimum_basic_scenario(self):
image = self.glance_image_create()
@@ -126,22 +120,29 @@
self.addCleanup(self.nova_volume_detach, server, volume)
self.cinder_show(volume)
- floating_ip = self.create_floating_ip(server)
- # fetch the server again to make sure the addresses were refreshed
- # after associating the floating IP
+ floating_ip = None
server = self.servers_client.show_server(server['id'])['server']
- address = self._get_floating_ip_in_server_addresses(
- floating_ip, server)
- self.assertIsNotNone(
- address,
- "Failed to find floating IP '%s' in server addresses: %s" %
- (floating_ip['ip'], server['addresses']))
+ if (CONF.network_feature_enabled.floating_ips and
+ CONF.network.floating_network_name):
+ floating_ip = self.create_floating_ip(server)
+ # fetch the server again to make sure the addresses were refreshed
+ # after associating the floating IP
+ server = self.servers_client.show_server(server['id'])['server']
+ address = self._get_floating_ip_in_server_addresses(
+ floating_ip, server)
+ self.assertIsNotNone(
+ address,
+ "Failed to find floating IP '%s' in server addresses: %s" %
+ (floating_ip['ip'], server['addresses']))
+ ssh_ip = floating_ip['ip']
+ else:
+ ssh_ip = self.get_server_ip(server)
self.create_and_add_security_group_to_server(server)
# check that we can SSH to the server before reboot
self.linux_client = self.get_remote_client(
- floating_ip['ip'], private_key=keypair['private_key'],
+ ssh_ip, private_key=keypair['private_key'],
server=server)
self.nova_reboot(server)
@@ -149,25 +150,27 @@
# check that we can SSH to the server after reboot
# (both connections are part of the scenario)
self.linux_client = self.get_remote_client(
- floating_ip['ip'], private_key=keypair['private_key'],
+ ssh_ip, private_key=keypair['private_key'],
server=server)
self.check_disks()
- # delete the floating IP, this should refresh the server addresses
- self.compute_floating_ips_client.delete_floating_ip(floating_ip['id'])
+ if floating_ip:
+ # delete the floating IP, this should refresh the server addresses
+ self.compute_floating_ips_client.delete_floating_ip(
+ floating_ip['id'])
- def is_floating_ip_detached_from_server():
- server_info = self.servers_client.show_server(
- server['id'])['server']
- address = self._get_floating_ip_in_server_addresses(
- floating_ip, server_info)
- return (not address)
+ def is_floating_ip_detached_from_server():
+ server_info = self.servers_client.show_server(
+ server['id'])['server']
+ address = self._get_floating_ip_in_server_addresses(
+ floating_ip, server_info)
+ return (not address)
- if not test_utils.call_until_true(
- is_floating_ip_detached_from_server,
- CONF.compute.build_timeout,
- CONF.compute.build_interval):
- msg = ("Floating IP '%s' should not be in server addresses: %s" %
- (floating_ip['ip'], server['addresses']))
- raise exceptions.TimeoutException(msg)
+ if not test_utils.call_until_true(
+ is_floating_ip_detached_from_server,
+ CONF.compute.build_timeout,
+ CONF.compute.build_interval):
+ msg = ("Floating IP '%s' should not be in server addresses: %s"
+ % (floating_ip['ip'], server['addresses']))
+ raise exceptions.TimeoutException(msg)
diff --git a/tempest/scenario/test_server_basic_ops.py b/tempest/scenario/test_server_basic_ops.py
index d5c378e..1be8625 100644
--- a/tempest/scenario/test_server_basic_ops.py
+++ b/tempest/scenario/test_server_basic_ops.py
@@ -43,12 +43,6 @@
* Terminate the instance
"""
- @classmethod
- def skip_checks(cls):
- super(TestServerBasicOps, cls).skip_checks()
- if not CONF.network_feature_enabled.floating_ips:
- raise cls.skipException("Floating ips are not available")
-
def setUp(self):
super(TestServerBasicOps, self).setUp()
self.run_ssh = CONF.validation.run_validation
@@ -56,11 +50,17 @@
def verify_ssh(self, keypair):
if self.run_ssh:
- # Obtain a floating IP
- self.fip = self.create_floating_ip(self.instance)['ip']
+ # Obtain a floating IP if floating_ips is enabled
+ if (CONF.network_feature_enabled.floating_ips and
+ CONF.network.floating_network_name):
+ self.ip = self.create_floating_ip(self.instance)['ip']
+ else:
+ server = self.servers_client.show_server(
+ self.instance['id'])['server']
+ self.ip = self.get_server_ip(server)
# Check ssh
self.ssh_client = self.get_remote_client(
- ip_address=self.fip,
+ ip_address=self.ip,
username=self.ssh_user,
private_key=keypair['private_key'],
server=self.instance)
@@ -75,8 +75,8 @@
result = self.ssh_client.exec_command(cmd)
if result:
msg = ('Failed while verifying metadata on server. Result '
- 'of command "%s" is NOT "%s".' % (cmd, self.fip))
- self.assertEqual(self.fip, result, msg)
+ 'of command "%s" is NOT "%s".' % (cmd, self.ip))
+ self.assertEqual(self.ip, result, msg)
return 'Verification is successful!'
if not test_utils.call_until_true(exec_cmd_and_verify_output,
diff --git a/tools/check_logs.py b/tools/check_logs.py
index fc21f75..b80ccc0 100755
--- a/tools/check_logs.py
+++ b/tools/check_logs.py
@@ -62,7 +62,7 @@
for (name, filename) in file_specs:
whitelist = whitelists.get(name, [])
with open(filename) as content:
- if scan_content(name, content, regexp, whitelist):
+ if scan_content(content, regexp, whitelist):
logs_with_errors.append(name)
for (name, url) in url_specs:
whitelist = whitelists.get(name, [])
@@ -71,12 +71,12 @@
page = urlreq.urlopen(req)
buf = six.StringIO(page.read())
f = gzip.GzipFile(fileobj=buf)
- if scan_content(name, f.read().splitlines(), regexp, whitelist):
+ if scan_content(f.read().splitlines(), regexp, whitelist):
logs_with_errors.append(name)
return logs_with_errors
-def scan_content(name, content, regexp, whitelist):
+def scan_content(content, regexp, whitelist):
had_errors = False
for line in content:
if not line.startswith("Stderr:") and regexp.match(line):