From 4dc7caf89c1daa03ab11636fff6b4cc4e00ac13c Mon Sep 17 00:00:00 2001
From: Mohammad Banikazemi <mb@us.ibm.com>
Date: Thu, 23 Jun 2016 14:20:34 -0400
Subject: [PATCH] Waiting for Neutron port to become ACTIVE

This patch set adds two config options similar to those
used in Nova, namely vif_plugging_is_fatal and
vif_plugging_timeout to make it possible to ask Kuryr
to wait until the port status becomes Active during libnetwork
join operations.

Closes-Bug: #1578016

Change-Id: I106d2c8c289c7fca2a465c72f1dbd53acc539f39
---
 kuryr/common/config.py              |  7 ++++++
 kuryr/common/constants.py           |  3 +++
 kuryr/common/exceptions.py          |  9 ++++++++
 kuryr/controllers.py                | 35 +++++++++++++++++++++++++++++
 kuryr/tests/unit/base.py            |  7 +++---
 kuryr/tests/unit/test_join.py       |  2 +-
 kuryr/tests/unit/test_kuryr.py      | 25 +++++++++++++++++----
 kuryr/tests/unit/test_kuryr_ipam.py |  4 ++--
 kuryr/tests/unit/test_leave.py      |  3 ++-
 9 files changed, 84 insertions(+), 11 deletions(-)

diff --git a/kuryr/common/config.py b/kuryr/common/config.py
index e5dca7cd..b7ab51db 100644
--- a/kuryr/common/config.py
+++ b/kuryr/common/config.py
@@ -61,6 +61,13 @@ neutron_opts = [
     cfg.StrOpt('default_subnetpool_v6',
                default='kuryr6',
                help=_('Name of default subnetpool version 6')),
+    cfg.BoolOpt('vif_plugging_is_fatal',
+                default=False,
+                help=_("Whether a plugging operation is failed if the port "
+                       "to plug does not become active")),
+    cfg.IntOpt('vif_plugging_timeout',
+               default=0,
+               help=_("Seconds to wait for port to become active")),
 ]
 keystone_opts = [
     cfg.StrOpt('auth_uri',
diff --git a/kuryr/common/constants.py b/kuryr/common/constants.py
index ec2c70fe..640e1a84 100644
--- a/kuryr/common/constants.py
+++ b/kuryr/common/constants.py
@@ -25,6 +25,9 @@ ROUTE_TYPE = {
      "CONNECTED": 1
 }
 
+PORT_STATUS_ACTIVE = 'ACTIVE'
+PORT_STATUS_DOWN = 'DOWN'
+
 DEVICE_OWNER = 'kuryr:container'
 NIC_NAME_LEN = 14
 VETH_PREFIX = 'tap'
diff --git a/kuryr/common/exceptions.py b/kuryr/common/exceptions.py
index 9fd8b838..05ef61c0 100644
--- a/kuryr/common/exceptions.py
+++ b/kuryr/common/exceptions.py
@@ -63,6 +63,15 @@ class NoResourceException(KuryrException):
     """
 
 
+class InactiveResourceException(KuryrException):
+    """Exception represents the resource for the given query is not active.
+
+    This exception is thrown when you query the Neutron resource associated
+    with the given query and you get the status of the resource as something
+    other than ACTIVE.
+    """
+
+
 class VethCreationFailure(KuryrException):
     """Exception represents the veth pair creation is failed.
 
diff --git a/kuryr/controllers.py b/kuryr/controllers.py
index 9a52fe2a..f64de894 100755
--- a/kuryr/controllers.py
+++ b/kuryr/controllers.py
@@ -15,11 +15,13 @@ import os_client_config
 import flask
 import jsonschema
 import netaddr
+import time
 
 from neutronclient.common import exceptions as n_exceptions
 from neutronclient.neutron import client
 from oslo_concurrency import processutils
 from oslo_config import cfg
+from oslo_log import log
 from oslo_utils import excutils
 
 from kuryr import app
@@ -31,6 +33,8 @@ from kuryr._i18n import _LE, _LI, _LW
 from kuryr import schemata
 from kuryr import utils
 
+LOG = log.getLogger(__name__)
+
 
 MANDATORY_NEUTRON_EXTENSION = "subnet_allocation"
 TAG_NEUTRON_EXTENSION = "tag"
@@ -110,6 +114,8 @@ def neutron_client():
     if not hasattr(app, 'neutron'):
         app.neutron = get_neutron_client()
         app.enable_dhcp = cfg.CONF.neutron_client.enable_dhcp
+        app.vif_plug_is_fatal = cfg.CONF.neutron_client.vif_plugging_is_fatal
+        app.vif_plug_timeout = cfg.CONF.neutron_client.vif_plugging_timeout
         app.neutron.format = 'json'
 
 
@@ -367,6 +373,27 @@ def _get_networks_by_identifier(identifier):
     return _get_networks_by_attrs(name=identifier)
 
 
+def _port_active(neutron_port_id, vif_plug_timeout):
+    port_active = False
+    tries = 0
+    while True:
+        try:
+            port = app.neutron.show_port(neutron_port_id)
+        except n_exceptions.NeutronClientException as ex:
+            app.logger.error(_LE('Could not get the port %s to check '
+                                 'its status'), ex)
+        else:
+            if port['port']['status'] == const.PORT_STATUS_ACTIVE:
+                port_active = True
+        if port_active or (tries >= vif_plug_timeout):
+            break
+        LOG.debug('Waiting for port %s to become ACTIVE', neutron_port_id)
+        tries += 1
+        time.sleep(1)
+
+    return port_active
+
+
 @app.route('/Plugin.Activate', methods=['POST'])
 def plugin_activate():
     """Returns the list of the implemented drivers.
@@ -825,6 +852,14 @@ def network_driver_join():
                 app.logger.error(_LE(
                     'Could not bind the Neutron port to the veth endpoint.'))
 
+        if app.vif_plug_is_fatal:
+            port_active = _port_active(neutron_port['id'],
+                                       app.vif_plug_timeout)
+            if not port_active:
+                raise exceptions.InactiveResourceException(
+                    "Neutron port {0} did not become active on time."
+                    .format(neutron_port_name))
+
         join_response = {
             "InterfaceName": {
                 "SrcName": peer_name,
diff --git a/kuryr/tests/unit/base.py b/kuryr/tests/unit/base.py
index 49c8e47c..138b165a 100644
--- a/kuryr/tests/unit/base.py
+++ b/kuryr/tests/unit/base.py
@@ -169,6 +169,7 @@ class TestKuryrBase(TestCase):
     @staticmethod
     def _get_fake_port(docker_endpoint_id, neutron_network_id,
                        neutron_port_id,
+                       neutron_port_status=const.PORT_STATUS_DOWN,
                        neutron_subnet_v4_id=None,
                        neutron_subnet_v6_id=None,
                        neutron_subnet_v4_address="192.168.1.2",
@@ -177,7 +178,7 @@ class TestKuryrBase(TestCase):
         #   http://developer.openstack.org/api-ref-networking-v2.html#createPort  # noqa
         fake_port = {
             'port': {
-                "status": "DOWN",
+                "status": neutron_port_status,
                 "name": utils.get_neutron_port_name(docker_endpoint_id),
                 "allowed_address_pairs": [],
                 "admin_state_up": True,
@@ -206,11 +207,11 @@ class TestKuryrBase(TestCase):
 
     @classmethod
     def _get_fake_ports(cls, docker_endpoint_id, neutron_network_id,
-                        fake_neutron_port_id,
+                        fake_neutron_port_id, neutron_port_status,
                         fake_neutron_subnet_v4_id, fake_neutron_subnet_v6_id):
         fake_port = cls._get_fake_port(
             docker_endpoint_id, neutron_network_id,
-            fake_neutron_port_id,
+            fake_neutron_port_id, neutron_port_status,
             fake_neutron_subnet_v4_id, fake_neutron_subnet_v6_id)
         fake_port = fake_port['port']
         fake_ports = {
diff --git a/kuryr/tests/unit/test_join.py b/kuryr/tests/unit/test_join.py
index 011f70db..5c49492f 100644
--- a/kuryr/tests/unit/test_join.py
+++ b/kuryr/tests/unit/test_join.py
@@ -79,7 +79,7 @@ class TestKuryrJoinFailures(base.TestKuryrFailures):
         fake_neutron_v6_subnet_id = str(uuid.uuid4())
         fake_neutron_ports_response = self._get_fake_ports(
             fake_docker_endpoint_id, fake_neutron_network_id,
-            fake_neutron_port_id,
+            fake_neutron_port_id, const.PORT_STATUS_ACTIVE,
             fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id)
         app.neutron.list_ports(name=neutron_port_name).AndReturn(
             fake_neutron_ports_response)
diff --git a/kuryr/tests/unit/test_kuryr.py b/kuryr/tests/unit/test_kuryr.py
index c083f97e..213dff16 100644
--- a/kuryr/tests/unit/test_kuryr.py
+++ b/kuryr/tests/unit/test_kuryr.py
@@ -420,7 +420,7 @@ class TestKuryr(base.TestKuryrBase):
         fake_port_id = str(uuid.uuid4())
         fake_port = self._get_fake_port(
             docker_endpoint_id, fake_neutron_net_id,
-            fake_port_id,
+            fake_port_id, constants.PORT_STATUS_ACTIVE,
             subnet_v4_id, subnet_v6_id)
         fake_fixed_ips = ['subnet_id=%s' % subnet_v4_id,
                           'ip_address=192.168.1.2',
@@ -477,7 +477,13 @@ class TestKuryr(base.TestKuryrBase):
         decoded_json = jsonutils.loads(response.data)
         self.assertEqual(constants.SCHEMA['SUCCESS'], decoded_json)
 
-    def test_network_driver_join(self):
+    @ddt.data(
+        (False), (True))
+    def test_network_driver_join(self, vif_plug_is_fatal):
+        if vif_plug_is_fatal:
+            self.mox.StubOutWithMock(app, "vif_plug_is_fatal")
+            app.vif_plug_is_fatal = True
+
         fake_docker_net_id = utils.get_hash()
         fake_docker_endpoint_id = utils.get_hash()
         fake_container_id = utils.get_hash()
@@ -492,7 +498,7 @@ class TestKuryr(base.TestKuryrBase):
         fake_neutron_v6_subnet_id = str(uuid.uuid4())
         fake_neutron_ports_response = self._get_fake_ports(
             fake_docker_endpoint_id, fake_neutron_net_id,
-            fake_neutron_port_id,
+            fake_neutron_port_id, constants.PORT_STATUS_DOWN,
             fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id)
         app.neutron.list_ports(name=neutron_port_name).AndReturn(
             fake_neutron_ports_response)
@@ -507,6 +513,16 @@ class TestKuryr(base.TestKuryrBase):
         fake_neutron_subnets = fake_neutron_subnets_response['subnets']
         _, fake_peer_name, _ = self._mock_out_binding(
             fake_docker_endpoint_id, fake_neutron_port, fake_neutron_subnets)
+
+        if vif_plug_is_fatal:
+            self.mox.StubOutWithMock(app.neutron, 'show_port')
+            fake_neutron_ports_response_2 = self._get_fake_port(
+                fake_docker_endpoint_id, fake_neutron_net_id,
+                fake_neutron_port_id, constants.PORT_STATUS_ACTIVE,
+                fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id)
+            app.neutron.show_port(fake_neutron_port_id).AndReturn(
+                fake_neutron_ports_response_2)
+
         self.mox.ReplayAll()
 
         fake_subnets_dict_by_id = {subnet['id']: subnet
@@ -554,7 +570,7 @@ class TestKuryr(base.TestKuryrBase):
         fake_neutron_v6_subnet_id = str(uuid.uuid4())
         fake_neutron_ports_response = self._get_fake_ports(
             fake_docker_endpoint_id, fake_neutron_net_id,
-            fake_neutron_port_id,
+            fake_neutron_port_id, constants.PORT_STATUS_ACTIVE,
             fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id)
         app.neutron.list_ports(name=neutron_port_name).AndReturn(
             fake_neutron_ports_response)
@@ -570,6 +586,7 @@ class TestKuryr(base.TestKuryrBase):
                                  content_type='application/json',
                                  data=jsonutils.dumps(leave_request))
 
+        self.mox.ReplayAll()
         self.assertEqual(200, response.status_code)
         decoded_json = jsonutils.loads(response.data)
         self.assertEqual(constants.SCHEMA['SUCCESS'], decoded_json)
diff --git a/kuryr/tests/unit/test_kuryr_ipam.py b/kuryr/tests/unit/test_kuryr_ipam.py
index 734e512e..544f289c 100644
--- a/kuryr/tests/unit/test_kuryr_ipam.py
+++ b/kuryr/tests/unit/test_kuryr_ipam.py
@@ -196,7 +196,7 @@ class TestKuryrIpam(base.TestKuryrBase):
         fake_neutron_port_id = str(uuid.uuid4())
         fake_port = base.TestKuryrBase._get_fake_port(
             docker_endpoint_id, neutron_network_id,
-            fake_neutron_port_id,
+            fake_neutron_port_id, const.PORT_STATUS_ACTIVE,
             subnet_v4_id,
             neutron_subnet_v4_address="10.0.0.5")
         port_request = {
@@ -361,7 +361,7 @@ class TestKuryrIpam(base.TestKuryrBase):
         fake_neutron_port_id = str(uuid.uuid4())
         fake_port = base.TestKuryrBase._get_fake_port(
             docker_endpoint_id, neutron_network_id,
-            fake_neutron_port_id,
+            fake_neutron_port_id, const.PORT_STATUS_ACTIVE,
             subnet_v4_id,
             neutron_subnet_v4_address=fake_ip4)
         port_request = {
diff --git a/kuryr/tests/unit/test_leave.py b/kuryr/tests/unit/test_leave.py
index 5b5f7251..a2e1be5d 100644
--- a/kuryr/tests/unit/test_leave.py
+++ b/kuryr/tests/unit/test_leave.py
@@ -19,6 +19,7 @@ from werkzeug import exceptions as w_exceptions
 
 from kuryr import app
 from kuryr import binding
+from kuryr.common import constants as const
 from kuryr.common import exceptions
 from kuryr.tests.unit import base
 from kuryr import utils
@@ -68,7 +69,7 @@ class TestKuryrLeaveFailures(base.TestKuryrFailures):
         fake_neutron_v6_subnet_id = str(uuid.uuid4())
         fake_neutron_ports_response = self._get_fake_ports(
             fake_docker_endpoint_id, fake_neutron_network_id,
-            fake_neutron_port_id,
+            fake_neutron_port_id, const.PORT_STATUS_ACTIVE,
             fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id)
         app.neutron.list_ports(name=neutron_port_name).AndReturn(
             fake_neutron_ports_response)