Merge "glance_http: add IPv6 support"
diff --git a/tempest/common/glance_http.py b/tempest/common/glance_http.py
index baf796d..00062de 100644
--- a/tempest/common/glance_http.py
+++ b/tempest/common/glance_http.py
@@ -306,13 +306,32 @@
 
     def connect(self):
         """Connect to SSL port and apply per-connection parameters."""
-        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        if self.timeout is not None:
-            # '0' microseconds
-            sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
-                            struct.pack('LL', self.timeout, 0))
-        self.sock = OpenSSLConnectionDelegator(self.context, sock)
-        self.sock.connect((self.host, self.port))
+        try:
+            addresses = socket.getaddrinfo(self.host,
+                                           self.port,
+                                           socket.AF_UNSPEC,
+                                           socket.SOCK_STREAM)
+        except OSError as msg:
+            raise exc.RestClientException(msg)
+        for res in addresses:
+            af, socktype, proto, canonname, sa = res
+            sock = socket.socket(af, socket.SOCK_STREAM)
+
+            if self.timeout is not None:
+                # '0' microseconds
+                sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
+                                struct.pack('LL', self.timeout, 0))
+            self.sock = OpenSSLConnectionDelegator(self.context, sock)
+            try:
+                self.sock.connect(sa)
+            except OSError as msg:
+                if self.sock:
+                    self.sock = None
+                    continue
+            break
+        if self.sock is None:
+            # Happen only when all results have failed.
+            raise exc.RestClientException('Cannot connect to %s' % self.host)
 
     def close(self):
         if self.sock:
diff --git a/tempest/tests/test_glance_http.py b/tempest/tests/test_glance_http.py
index db9db34..1811141 100644
--- a/tempest/tests/test_glance_http.py
+++ b/tempest/tests/test_glance_http.py
@@ -13,6 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import socket
+
 import mock
 from oslotest import mockpatch
 import six
@@ -101,6 +103,20 @@
         self.assertTrue(isinstance(self.client._get_connection(),
                                    glance_http.VerifiedHTTPSConnection))
 
+    def test_get_connection_ipv4_https(self):
+        endpoint = 'https://127.0.0.1'
+        self.fake_auth.base_url = mock.MagicMock(return_value=endpoint)
+        self.client = glance_http.HTTPClient(self.fake_auth, {})
+        self.assertTrue(isinstance(self.client._get_connection(),
+                                   glance_http.VerifiedHTTPSConnection))
+
+    def test_get_connection_ipv6_https(self):
+        endpoint = 'https://[::1]'
+        self.fake_auth.base_url = mock.MagicMock(return_value=endpoint)
+        self.client = glance_http.HTTPClient(self.fake_auth, {})
+        self.assertTrue(isinstance(self.client._get_connection(),
+                                   glance_http.VerifiedHTTPSConnection))
+
     def test_get_connection_url_not_fount(self):
         self.useFixture(mockpatch.PatchObject(self.client, 'connection_class',
                                               side_effect=httplib.InvalidURL()
@@ -146,6 +162,64 @@
         self.assertEqual(6, len(kwargs.keys()))
 
 
+class TestVerifiedHTTPSConnection(base.TestCase):
+
+    @mock.patch('socket.socket')
+    @mock.patch('tempest.common.glance_http.OpenSSLConnectionDelegator')
+    def test_connect_ipv4(self, mock_delegator, mock_socket):
+        connection = glance_http.VerifiedHTTPSConnection('127.0.0.1')
+        connection.connect()
+
+        mock_socket.assert_called_once_with(socket.AF_INET, socket.SOCK_STREAM)
+        mock_delegator.assert_called_once_with(connection.context,
+                                               mock_socket.return_value)
+        mock_delegator.return_value.connect.assert_called_once_with(
+            (connection.host, 443))
+
+    @mock.patch('socket.socket')
+    @mock.patch('tempest.common.glance_http.OpenSSLConnectionDelegator')
+    def test_connect_ipv6(self, mock_delegator, mock_socket):
+        connection = glance_http.VerifiedHTTPSConnection('[::1]')
+        connection.connect()
+
+        mock_socket.assert_called_once_with(socket.AF_INET6,
+                                            socket.SOCK_STREAM)
+        mock_delegator.assert_called_once_with(connection.context,
+                                               mock_socket.return_value)
+        mock_delegator.return_value.connect.assert_called_once_with(
+            (connection.host, 443, 0, 0))
+
+    @mock.patch('tempest.common.glance_http.OpenSSLConnectionDelegator')
+    @mock.patch('socket.getaddrinfo',
+                side_effect=OSError('Gettaddrinfo failed'))
+    def test_connect_with_address_lookup_failure(self, mock_getaddrinfo,
+                                                 mock_delegator):
+        connection = glance_http.VerifiedHTTPSConnection('127.0.0.1')
+        self.assertRaises(exceptions.RestClientException, connection.connect)
+
+        mock_getaddrinfo.assert_called_once_with(
+            connection.host, connection.port, 0, socket.SOCK_STREAM)
+
+    @mock.patch('socket.socket')
+    @mock.patch('socket.getaddrinfo',
+                return_value=[(2, 1, 6, '', ('127.0.0.1', 443))])
+    @mock.patch('tempest.common.glance_http.OpenSSLConnectionDelegator')
+    def test_connect_with_socket_failure(self, mock_delegator,
+                                         mock_getaddrinfo,
+                                         mock_socket):
+        mock_delegator.return_value.connect.side_effect = \
+            OSError('Connect failed')
+
+        connection = glance_http.VerifiedHTTPSConnection('127.0.0.1')
+        self.assertRaises(exceptions.RestClientException, connection.connect)
+
+        mock_getaddrinfo.assert_called_once_with(
+            connection.host, connection.port, 0, socket.SOCK_STREAM)
+        mock_socket.assert_called_once_with(socket.AF_INET, socket.SOCK_STREAM)
+        mock_delegator.return_value.connect.\
+            assert_called_once_with((connection.host, 443))
+
+
 class TestResponseBodyIterator(base.TestCase):
 
     def test_iter_default_chunk_size_64k(self):