Tempest test for Neutron extension: extraroute-atomic

Change-Id: I9b104432f1cd1aa1471087226ffc53152b823222
Depends-On: https://review.opendev.org/670851
Partial-Bug: #1826396 (rfe)
Related-Change: https://review.opendev.org/655680 (spec)
diff --git a/.zuul.yaml b/.zuul.yaml
index 011bfe5..00be4ed 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -31,6 +31,7 @@
         - external-net
         - extra_dhcp_opt
         - extraroute
+        - extraroute-atomic
         - filter-validation
         - fip-port-details
         - flavors
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index 79ac4a6..71a0e5e 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -724,6 +724,14 @@
         return interface
 
     @classmethod
+    def add_extra_routes_atomic(cls, *args, **kwargs):
+        return cls.client.add_extra_routes_atomic(*args, **kwargs)
+
+    @classmethod
+    def remove_extra_routes_atomic(cls, *args, **kwargs):
+        return cls.client.remove_extra_routes_atomic(*args, **kwargs)
+
+    @classmethod
     def get_supported_qos_rule_types(cls):
         body = cls.client.list_qos_rule_types()
         return [rule_type['type'] for rule_type in body['rule_types']]
diff --git a/neutron_tempest_plugin/api/test_routers.py b/neutron_tempest_plugin/api/test_routers.py
index 3b9867b..d866dbc 100644
--- a/neutron_tempest_plugin/api/test_routers.py
+++ b/neutron_tempest_plugin/api/test_routers.py
@@ -200,6 +200,71 @@
     def _delete_extra_routes(self, router_id):
         self.client.delete_extra_routes(router_id)
 
+    @decorators.idempotent_id('b29d1698-d603-11e9-9c66-079cc4aec539')
+    @tutils.requires_ext(extension='extraroute-atomic', service='network')
+    def test_extra_routes_atomic(self):
+        self.network = self.create_network()
+        self.subnet = self.create_subnet(self.network)
+        self.router = self._create_router(
+            data_utils.rand_name('router-'), True)
+        self.create_router_interface(self.router['id'], self.subnet['id'])
+        self.addCleanup(
+            self._delete_extra_routes,
+            self.router['id'])
+
+        if self._ip_version == 6:
+            dst = '2001:db8:%s::/64'
+        else:
+            dst = '10.0.%s.0/24'
+
+        cidr = netaddr.IPNetwork(self.subnet['cidr'])
+
+        routes = [
+            {'destination': dst % 2, 'nexthop': cidr[2]},
+        ]
+        resp = self.client.add_extra_routes_atomic(
+            self.router['id'], routes)
+        self.assertEqual(1, len(resp['router']['routes']))
+
+        routes = [
+            {'destination': dst % 2, 'nexthop': cidr[2]},
+            {'destination': dst % 3, 'nexthop': cidr[3]},
+        ]
+        resp = self.client.add_extra_routes_atomic(
+            self.router['id'], routes)
+        self.assertEqual(2, len(resp['router']['routes']))
+
+        routes = [
+            {'destination': dst % 3, 'nexthop': cidr[3]},
+            {'destination': dst % 4, 'nexthop': cidr[4]},
+        ]
+        resp = self.client.remove_extra_routes_atomic(
+            self.router['id'], routes)
+        self.assertEqual(1, len(resp['router']['routes']))
+
+        routes = [
+            {'destination': dst % 2, 'nexthop': cidr[5]},
+        ]
+        resp = self.client.add_extra_routes_atomic(
+            self.router['id'], routes)
+        self.assertEqual(2, len(resp['router']['routes']))
+
+        routes = [
+            {'destination': dst % 2, 'nexthop': cidr[5]},
+        ]
+        resp = self.client.remove_extra_routes_atomic(
+            self.router['id'], routes)
+        self.assertEqual(1, len(resp['router']['routes']))
+
+        routes = [
+            {'destination': dst % 2, 'nexthop': cidr[2]},
+            {'destination': dst % 3, 'nexthop': cidr[3]},
+            {'destination': dst % 2, 'nexthop': cidr[5]},
+        ]
+        resp = self.client.remove_extra_routes_atomic(
+            self.router['id'], routes)
+        self.assertEqual(0, len(resp['router']['routes']))
+
     @decorators.idempotent_id('01f185d1-d1a6-4cf9-abf7-e0e1384c169c')
     def test_network_attached_with_two_routers(self):
         network = self.create_network(data_utils.rand_name('network1'))
diff --git a/neutron_tempest_plugin/services/network/json/network_client.py b/neutron_tempest_plugin/services/network/json/network_client.py
index 11ba8ef..dabf3af 100644
--- a/neutron_tempest_plugin/services/network/json/network_client.py
+++ b/neutron_tempest_plugin/services/network/json/network_client.py
@@ -564,6 +564,22 @@
         body = jsonutils.loads(body)
         return service_client.ResponseBody(resp, body)
 
+    def add_extra_routes_atomic(self, router_id, routes):
+        uri = '%s/routers/%s/add_extraroutes' % (self.uri_prefix, router_id)
+        request_body = {'router': {'routes': routes}}
+        resp, response_body = self.put(uri, jsonutils.dumps(request_body))
+        self.expected_success(200, resp.status)
+        return service_client.ResponseBody(
+            resp, jsonutils.loads(response_body))
+
+    def remove_extra_routes_atomic(self, router_id, routes):
+        uri = '%s/routers/%s/remove_extraroutes' % (self.uri_prefix, router_id)
+        request_body = {'router': {'routes': routes}}
+        resp, response_body = self.put(uri, jsonutils.dumps(request_body))
+        self.expected_success(200, resp.status)
+        return service_client.ResponseBody(
+            resp, jsonutils.loads(response_body))
+
     def add_dhcp_agent_to_network(self, agent_id, network_id):
         post_body = {'network_id': network_id}
         body = jsonutils.dumps(post_body)