API compare-and-swap updates based on revision_number

Allows posting revision number matching in the If-Match header
so updates/deletes will only be satisfied if the current revision
number of the object matches.

DocImpact: The Neutron API now supports conditional updates to resources
           that contain the standard 'revision_number' attribute by
           setting the revision_number in an HTTP If-Match header.
APIImpact

Partial-Bug: #1493714
Partially-Implements: blueprint push-notifications
Change-Id: I7d97d6044378eb59cb2c7bdc788dc6c174783299
diff --git a/neutron/tests/tempest/api/test_revisions.py b/neutron/tests/tempest/api/test_revisions.py
index d94aede..83c8410 100644
--- a/neutron/tests/tempest/api/test_revisions.py
+++ b/neutron/tests/tempest/api/test_revisions.py
@@ -13,6 +13,7 @@
 import netaddr
 
 from tempest.lib import decorators
+from tempest.lib import exceptions
 from tempest import test
 
 from neutron.tests.tempest.api import base
@@ -33,6 +34,35 @@
         self.assertGreater(updated['network']['revision_number'],
                            net['revision_number'])
 
+    @decorators.idempotent_id('4a26a4be-9c53-483c-bc50-b11111113333')
+    def test_update_network_constrained_by_revision(self):
+        net = self.create_network()
+        current = net['revision_number']
+        stale = current - 1
+        # using a stale number should fail
+        self.assertRaises(
+            exceptions.PreconditionFailed,
+            self.client.update_network,
+            net['id'], name='newnet',
+            headers={'If-Match': 'revision_number=%s' % stale}
+        )
+
+        # using current should pass. in case something is updating the network
+        # on the server at the same time, we have to re-read and update to be
+        # safe
+        for i in range(100):
+            current = (self.client.show_network(net['id'])
+                       ['network']['revision_number'])
+            try:
+                self.client.update_network(
+                    net['id'], name='newnet',
+                    headers={'If-Match': 'revision_number=%s' % current})
+            except exceptions.UnexpectedResponseCode:
+                continue
+            break
+        else:
+            self.fail("Failed to update network after 100 tries.")
+
     @decorators.idempotent_id('cac7ecde-12d5-4331-9a03-420899dea077')
     def test_update_port_bumps_revision(self):
         net = self.create_network()