Merge "Adding basic connectivity scenario to Neutron"
diff --git a/neutron/tests/tempest/api/base.py b/neutron/tests/tempest/api/base.py
index 033ac67..993e356 100644
--- a/neutron/tests/tempest/api/base.py
+++ b/neutron/tests/tempest/api/base.py
@@ -14,6 +14,7 @@
 #    under the License.
 
 import functools
+import math
 
 import netaddr
 from tempest.lib.common.utils import data_utils
@@ -21,6 +22,7 @@
 from tempest import test
 
 from neutron.common import constants
+from neutron.common import utils
 from neutron.tests.tempest.api import clients
 from neutron.tests.tempest import config
 from neutron.tests.tempest import exceptions
@@ -488,35 +490,42 @@
     # This should be defined by subclasses to reflect resource name to test
     resource = None
 
-    # also test a case when there are multiple resources with the same name
+    # NOTE(ihrachys): some names, like those starting with an underscore (_)
+    # are sorted differently depending on whether the plugin implements native
+    # sorting support, or not. So we avoid any such cases here, sticking to
+    # alphanumeric. Also test a case when there are multiple resources with the
+    # same name
     resource_names = ('test1', 'abc1', 'test10', '123test') + ('test1',)
 
-    list_kwargs = {'shared': False}
-
     force_tenant_isolation = True
 
-    @classmethod
-    def resource_setup(cls):
-        super(BaseSearchCriteriaTest, cls).resource_setup()
+    list_kwargs = {}
 
-        cls.create_method = getattr(cls, 'create_%s' % cls.resource)
+    def assertSameOrder(self, original, actual):
+        # gracefully handle iterators passed
+        original = list(original)
+        actual = list(actual)
+        self.assertEqual(len(original), len(actual))
+        for expected, res in zip(original, actual):
+            self.assertEqual(expected['name'], res['name'])
 
-        # NOTE(ihrachys): some names, like those starting with an underscore
-        # (_) are sorted differently depending on whether the plugin implements
-        # native sorting support, or not. So we avoid any such cases here,
-        # sticking to alphanumeric.
-        for name in cls.resource_names:
-            args = {'%s_name' % cls.resource: name}
-            cls.create_method(**args)
+    @utils.classproperty
+    def plural_name(self):
+        return '%ss' % self.resource
 
     def list_method(self, *args, **kwargs):
-        method = getattr(self.client, 'list_%ss' % self.resource)
+        method = getattr(self.client, 'list_%s' % self.plural_name)
         kwargs.update(self.list_kwargs)
         return method(*args, **kwargs)
 
+    def get_bare_url(self, url):
+        base_url = self.client.base_url
+        self.assertTrue(url.startswith(base_url))
+        return url[len(base_url):]
+
     @classmethod
     def _extract_resources(cls, body):
-        return body['%ss' % cls.resource]
+        return body[cls.plural_name]
 
     def _test_list_sorts(self, direction):
         sort_args = {
@@ -560,9 +569,7 @@
         resources = self._extract_resources(body)
         self.assertTrue(len(resources) >= len(self.resource_names))
 
-    @_require_pagination
-    @_require_sorting
-    def _test_list_pagination_with_marker(self):
+    def _test_list_pagination_iteratively(self, lister):
         # first, collect all resources for later comparison
         sort_args = {
             'sort_dir': constants.SORT_DIRECTION_ASC,
@@ -572,10 +579,19 @@
         expected_resources = self._extract_resources(body)
         self.assertNotEmpty(expected_resources)
 
+        resources = lister(
+            len(expected_resources), sort_args
+        )
+
+        # finally, compare that the list retrieved in one go is identical to
+        # the one containing pagination results
+        self.assertSameOrder(expected_resources, resources)
+
+    def _list_all_with_marker(self, niterations, sort_args):
         # paginate resources one by one, using last fetched resource as a
         # marker
         resources = []
-        for i in range(len(expected_resources)):
+        for i in range(niterations):
             pagination_args = sort_args.copy()
             pagination_args['limit'] = 1
             if resources:
@@ -584,8 +600,119 @@
             resources_ = self._extract_resources(body)
             self.assertEqual(1, len(resources_))
             resources.extend(resources_)
+        return resources
 
-        # finally, compare that the list retrieved in one go is identical to
-        # the one containing pagination results
-        for expected, res in zip(expected_resources, resources):
-            self.assertEqual(expected['name'], res['name'])
+    @_require_pagination
+    @_require_sorting
+    def _test_list_pagination_with_marker(self):
+        self._test_list_pagination_iteratively(self._list_all_with_marker)
+
+    def _list_all_with_hrefs(self, niterations, sort_args):
+        # paginate resources one by one, using next href links
+        resources = []
+        prev_links = {}
+
+        for i in range(niterations):
+            if prev_links:
+                uri = self.get_bare_url(prev_links['next'])
+            else:
+                uri = self.client.build_uri(
+                    self.plural_name, limit=1, **sort_args)
+            prev_links, body = self.client.get_uri_with_links(
+                self.plural_name, uri
+            )
+            resources_ = self._extract_resources(body)
+            self.assertEqual(1, len(resources_))
+            resources.extend(resources_)
+
+        # The last element is empty and does not contain 'next' link
+        uri = self.get_bare_url(prev_links['next'])
+        prev_links, body = self.client.get_uri_with_links(
+            self.plural_name, uri
+        )
+        self.assertNotIn('next', prev_links)
+
+        # Now walk backwards and compare results
+        resources2 = []
+        for i in range(niterations):
+            uri = self.get_bare_url(prev_links['previous'])
+            prev_links, body = self.client.get_uri_with_links(
+                self.plural_name, uri
+            )
+            resources_ = self._extract_resources(body)
+            self.assertEqual(1, len(resources_))
+            resources2.extend(resources_)
+
+        self.assertSameOrder(resources, reversed(resources2))
+
+        return resources
+
+    @_require_pagination
+    @_require_sorting
+    def _test_list_pagination_with_href_links(self):
+        self._test_list_pagination_iteratively(self._list_all_with_hrefs)
+
+    @_require_pagination
+    @_require_sorting
+    def _test_list_pagination_page_reverse_with_href_links(
+            self, direction=constants.SORT_DIRECTION_ASC):
+        pagination_args = {
+            'sort_dir': direction,
+            'sort_key': 'name',
+        }
+        body = self.list_method(**pagination_args)
+        expected_resources = self._extract_resources(body)
+
+        page_size = 2
+        pagination_args['limit'] = page_size
+
+        prev_links = {}
+        resources = []
+        num_resources = len(expected_resources)
+        niterations = int(math.ceil(float(num_resources) / page_size))
+        for i in range(niterations):
+            if prev_links:
+                uri = self.get_bare_url(prev_links['previous'])
+            else:
+                uri = self.client.build_uri(
+                    self.plural_name, page_reverse=True, **pagination_args)
+            prev_links, body = self.client.get_uri_with_links(
+                self.plural_name, uri
+            )
+            resources_ = self._extract_resources(body)
+            self.assertTrue(page_size >= len(resources_))
+            resources.extend(reversed(resources_))
+
+        self.assertSameOrder(expected_resources, reversed(resources))
+
+    @_require_pagination
+    @_require_sorting
+    def _test_list_pagination_page_reverse_asc(self):
+        self._test_list_pagination_page_reverse(
+            direction=constants.SORT_DIRECTION_ASC)
+
+    @_require_pagination
+    @_require_sorting
+    def _test_list_pagination_page_reverse_desc(self):
+        self._test_list_pagination_page_reverse(
+            direction=constants.SORT_DIRECTION_DESC)
+
+    def _test_list_pagination_page_reverse(self, direction):
+        pagination_args = {
+            'sort_dir': direction,
+            'sort_key': 'name',
+            'limit': 3,
+        }
+        body = self.list_method(**pagination_args)
+        expected_resources = self._extract_resources(body)
+
+        pagination_args['limit'] -= 1
+        pagination_args['marker'] = expected_resources[-1]['id']
+        pagination_args['page_reverse'] = True
+        body = self.list_method(**pagination_args)
+
+        self.assertSameOrder(
+            # the last entry is not included in 2nd result when used as a
+            # marker
+            expected_resources[:-1],
+            self._extract_resources(body))
diff --git a/neutron/tests/tempest/api/test_network_ip_availability.py b/neutron/tests/tempest/api/test_network_ip_availability.py
index f84e14e..74395ea 100644
--- a/neutron/tests/tempest/api/test_network_ip_availability.py
+++ b/neutron/tests/tempest/api/test_network_ip_availability.py
@@ -99,9 +99,11 @@
 def calc_total_ips(prefix, ip_version):
     # will calculate total ips after removing reserved.
     if ip_version == lib_constants.IP_VERSION_4:
-        total_ips = 2 ** (32 - prefix) - DEFAULT_IP4_RESERVED
+        total_ips = 2 ** (lib_constants.IPv4_BITS
+                          - prefix) - DEFAULT_IP4_RESERVED
     elif ip_version == lib_constants.IP_VERSION_6:
-        total_ips = 2 ** (128 - prefix) - DEFAULT_IP6_RESERVED
+        total_ips = 2 ** (lib_constants.IPv6_BITS
+                          - prefix) - DEFAULT_IP6_RESERVED
     return total_ips
 
 
diff --git a/neutron/tests/tempest/api/test_networks.py b/neutron/tests/tempest/api/test_networks.py
index 4816d79..951e8c9 100644
--- a/neutron/tests/tempest/api/test_networks.py
+++ b/neutron/tests/tempest/api/test_networks.py
@@ -95,6 +95,14 @@
 
     resource = 'network'
 
+    list_kwargs = {'shared': False}
+
+    @classmethod
+    def resource_setup(cls):
+        super(NetworksSearchCriteriaTest, cls).resource_setup()
+        for name in cls.resource_names:
+            cls.create_network(network_name=name)
+
     @test.attr(type='smoke')
     @test.idempotent_id('de27d34a-bd9d-4516-83d6-81ef723f7d0d')
     def test_list_sorts_asc(self):
@@ -111,11 +119,31 @@
         self._test_list_pagination()
 
     @test.attr(type='smoke')
-    @test.idempotent_id('71389852-f57b-49f2-b109-77b705e9e8af')
+    @test.idempotent_id('b7e153d2-37c3-48d4-8390-ec13498fee3d')
     def test_list_pagination_with_marker(self):
         self._test_list_pagination_with_marker()
 
     @test.attr(type='smoke')
+    @test.idempotent_id('8a9c89df-0ee7-4c0d-8f1d-ec8f27cf362f')
+    def test_list_pagination_with_href_links(self):
+        self._test_list_pagination_with_href_links()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('79a52810-2156-4ab6-b577-9e46e58d4b58')
+    def test_list_pagination_page_reverse_asc(self):
+        self._test_list_pagination_page_reverse_asc()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('36a4671f-a542-442f-bc44-a8873ee778d1')
+    def test_list_pagination_page_reverse_desc(self):
+        self._test_list_pagination_page_reverse_desc()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('13eb066c-aa90-406d-b4c3-39595bf8f910')
+    def test_list_pagination_page_reverse_with_href_links(self):
+        self._test_list_pagination_page_reverse_with_href_links()
+
+    @test.attr(type='smoke')
     @test.idempotent_id('f1867fc5-e1d6-431f-bc9f-8b882e43a7f9')
     def test_list_no_pagination_limit_0(self):
         self._test_list_no_pagination_limit_0()
diff --git a/neutron/tests/tempest/api/test_ports.py b/neutron/tests/tempest/api/test_ports.py
index 7bf4790..eec97ba 100644
--- a/neutron/tests/tempest/api/test_ports.py
+++ b/neutron/tests/tempest/api/test_ports.py
@@ -46,3 +46,60 @@
         self.create_port(self.network)
         self.client.update_subnet(s['id'], enable_dhcp=True)
         self.create_port(self.network)
+
+
+class PortsSearchCriteriaTest(base.BaseSearchCriteriaTest):
+
+    resource = 'port'
+
+    @classmethod
+    def resource_setup(cls):
+        super(PortsSearchCriteriaTest, cls).resource_setup()
+        net = cls.create_network(network_name='port-search-test-net')
+        for name in cls.resource_names:
+            cls.create_port(net, name=name)
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('9ab73df4-960a-4ae3-87d3-60992b8d3e2d')
+    def test_list_sorts_asc(self):
+        self._test_list_sorts_asc()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('b426671d-7270-430f-82ff-8f33eec93010')
+    def test_list_sorts_desc(self):
+        self._test_list_sorts_desc()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('a202fdc8-6616-45df-b6a0-463932de6f94')
+    def test_list_pagination(self):
+        self._test_list_pagination()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('f4723b8e-8186-4b9a-bf9e-57519967e048')
+    def test_list_pagination_with_marker(self):
+        self._test_list_pagination_with_marker()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('fcd02a7a-f07e-4d5e-b0ca-b58e48927a9b')
+    def test_list_pagination_with_href_links(self):
+        self._test_list_pagination_with_href_links()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('3afe7024-77ab-4cfe-824b-0b2bf4217727')
+    def test_list_no_pagination_limit_0(self):
+        self._test_list_no_pagination_limit_0()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('b8857391-dc44-40cc-89b7-2800402e03ce')
+    def test_list_pagination_page_reverse_asc(self):
+        self._test_list_pagination_page_reverse_asc()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('4e51e9c9-ceae-4ec0-afd4-147569247699')
+    def test_list_pagination_page_reverse_desc(self):
+        self._test_list_pagination_page_reverse_desc()
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('74293e59-d794-4a93-be09-38667199ef68')
+    def test_list_pagination_page_reverse_with_href_links(self):
+        self._test_list_pagination_page_reverse_with_href_links()
diff --git a/neutron/tests/tempest/api/test_security_groups_negative.py b/neutron/tests/tempest/api/test_security_groups_negative.py
index 628dfc6..68b5fd0 100644
--- a/neutron/tests/tempest/api/test_security_groups_negative.py
+++ b/neutron/tests/tempest/api/test_security_groups_negative.py
@@ -29,38 +29,6 @@
     def resource_setup(cls):
         super(NegativeSecGroupTest, cls).resource_setup()
 
-    @test.attr(type=['negative', 'gate'])
-    @test.idempotent_id('0d9c7791-f2ad-4e2f-ac73-abf2373b0d2d')
-    def test_create_security_group_rule_with_invalid_ports(self):
-        group_create_body, _ = self._create_security_group()
-
-        # Create rule for tcp protocol with invalid ports
-        states = [(-16, 80, 'Invalid value for port -16'),
-                  (80, 79, 'port_range_min must be <= port_range_max'),
-                  (80, 65536, 'Invalid value for port 65536'),
-                  (None, 6, 'port_range_min must be <= port_range_max'),
-                  (-16, 65536, 'Invalid value for port')]
-        for pmin, pmax, msg in states:
-            ex = self.assertRaises(
-                lib_exc.BadRequest, self.client.create_security_group_rule,
-                security_group_id=group_create_body['security_group']['id'],
-                protocol='tcp', port_range_min=pmin, port_range_max=pmax,
-                direction='ingress', ethertype=self.ethertype)
-            self.assertIn(msg, str(ex))
-
-        # Create rule for icmp protocol with invalid ports
-        states = [(1, 256, 'Invalid value for ICMP code'),
-                  (-1, 25, 'Invalid value'),
-                  (None, 6, 'ICMP type (port-range-min) is missing'),
-                  (300, 1, 'Invalid value for ICMP type')]
-        for pmin, pmax, msg in states:
-            ex = self.assertRaises(
-                lib_exc.BadRequest, self.client.create_security_group_rule,
-                security_group_id=group_create_body['security_group']['id'],
-                protocol='icmp', port_range_min=pmin, port_range_max=pmax,
-                direction='ingress', ethertype=self.ethertype)
-            self.assertIn(msg, str(ex))
-
     @test.attr(type='negative')
     @test.idempotent_id('55100aa8-b24f-333c-0bef-64eefd85f15c')
     def test_update_default_security_group_name(self):
diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py
index 70cc207..87a77e5 100644
--- a/neutron/tests/tempest/services/network/json/network_client.py
+++ b/neutron/tests/tempest/services/network/json/network_client.py
@@ -69,6 +69,12 @@
             uri = '%s/%s' % (self.uri_prefix, plural_name)
         return uri
 
+    def build_uri(self, plural_name, **kwargs):
+        uri = self.get_uri(plural_name)
+        if kwargs:
+            uri += '?' + urlparse.urlencode(kwargs, doseq=1)
+        return uri
+
     def pluralize(self, resource_name):
         # get plural from map or just add 's'
 
@@ -83,11 +89,16 @@
         }
         return resource_plural_map.get(resource_name, resource_name + 's')
 
+    def get_uri_with_links(self, plural_name, uri):
+        resp, body = self.get(uri)
+        result = {plural_name: self.deserialize_list(body)}
+        links = self.deserialize_links(body)
+        self.expected_success(200, resp.status)
+        return links, service_client.ResponseBody(resp, result)
+
     def _lister(self, plural_name):
         def _list(**filters):
-            uri = self.get_uri(plural_name)
-            if filters:
-                uri += '?' + urlparse.urlencode(filters, doseq=1)
+            uri = self.build_uri(plural_name, **filters)
             resp, body = self.get(uri)
             result = {plural_name: self.deserialize_list(body)}
             self.expected_success(200, resp.status)
@@ -272,6 +283,19 @@
                 continue
             return res[k]
 
+    def deserialize_links(self, body):
+        res = jsonutils.loads(body)
+        # expecting response in form
+        # {'resources': [ res1, res2] } => when pagination disabled
+        # {'resources': [..], 'resources_links': {}} => if pagination enabled
+        for k in res.keys():
+            if k.endswith("_links"):
+                return {
+                    link['rel']: link['href']
+                    for link in res[k]
+                }
+        return {}
+
     def serialize(self, data):
         return jsonutils.dumps(data)