IPv6 fix in Glance for malformed URLs.

Fix for a bug 1599123. URL construction is now considering what is
defined as a hostname (IPv6 address or something else). The change
results in a url constructed as http?://IPv4_address:port/, or
http?://hostname:port/, or http?://[IPv6_address]:port/. There
should be no more malformed URLs like http://fd00::f00d:9191/
generated for IPv6 addresses.

It also includes a test in glance/tests/functional/test_images.py,
named TestImagesIPv6. Additional functions which work on IPv6 only
were added since the whole testing suite is hardcoded for IPv4.

Change-Id: I66d6f2c57d1ccd086f941fc9e3764b4cc321241f
Closes-Bug: #1599123
This commit is contained in:
Tomislav Sukser 2016-11-08 13:49:14 +01:00
parent f72d9556b4
commit 8be3e10586
4 changed files with 124 additions and 5 deletions

View File

@ -41,6 +41,7 @@ except ImportError:
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import netutils
import six
from six.moves import http_client
# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
@ -379,7 +380,10 @@ class BaseClient(object):
action = urlparse.quote(action)
path = '/'.join([self.doc_root or '', action.lstrip('/')])
scheme = "https" if self.use_ssl else "http"
netloc = "%s:%d" % (self.host, self.port)
if netutils.is_valid_ipv6(self.host):
netloc = "[%s]:%d" % (self.host, self.port)
else:
netloc = "%s:%d" % (self.host, self.port)
if isinstance(params, dict):
for (key, value) in list(params.items()):

View File

@ -281,6 +281,8 @@ class ApiServer(Server):
self.server_name = 'api'
self.server_module = 'glance.cmd.%s' % self.server_name
self.default_store = kwargs.get("default_store", "file")
self.bind_host = "127.0.0.1"
self.registry_host = "127.0.0.1"
self.key_file = ""
self.cert_file = ""
self.metadata_encryption_key = "012345678901234567890123456789ab"
@ -321,12 +323,12 @@ class ApiServer(Server):
self.conf_base = """[DEFAULT]
debug = %(debug)s
default_log_levels = eventlet.wsgi.server=DEBUG
bind_host = 127.0.0.1
bind_host = %(bind_host)s
bind_port = %(bind_port)s
key_file = %(key_file)s
cert_file = %(cert_file)s
metadata_encryption_key = %(metadata_encryption_key)s
registry_host = 127.0.0.1
registry_host = %(registry_host)s
registry_port = %(registry_port)s
use_user_token = %(use_user_token)s
send_identity_credentials = %(send_identity_credentials)s
@ -462,6 +464,7 @@ class RegistryServer(Server):
self.sql_connection = os.environ.get('GLANCE_TEST_SQL_CONNECTION',
default_sql_connection)
self.bind_host = "127.0.0.1"
self.pid_file = os.path.join(self.test_dir, "registry.pid")
self.log_file = os.path.join(self.test_dir, "registry.log")
self.owner_is_tenant = True
@ -475,7 +478,7 @@ class RegistryServer(Server):
self.conf_base = """[DEFAULT]
debug = %(debug)s
bind_host = 127.0.0.1
bind_host = %(bind_host)s
bind_port = %(bind_port)s
log_file = %(log_file)s
sql_connection = %(sql_connection)s
@ -534,6 +537,8 @@ class ScrubberDaemon(Server):
self.server_module = 'glance.cmd.%s' % self.server_name
self.daemon = daemon
self.registry_host = "127.0.0.1"
self.image_dir = os.path.join(self.test_dir, "images")
self.scrub_time = 5
self.pid_file = os.path.join(self.test_dir, "scrubber.pid")
@ -556,7 +561,7 @@ log_file = %(log_file)s
daemon = %(daemon)s
wakeup_time = 2
scrub_time = %(scrub_time)s
registry_host = 127.0.0.1
registry_host = %(registry_host)s
registry_port = %(registry_port)s
metadata_encryption_key = %(metadata_encryption_key)s
lock_path = %(lock_path)s
@ -817,6 +822,25 @@ class FunctionalTest(test_utils.BaseTestCase):
finally:
s.close()
def ping_server_ipv6(self, port):
"""
Simple ping on the port. If responsive, return True, else
return False.
:note We use raw sockets, not ping here, since ping uses ICMP and
has no concept of ports...
The function uses IPv6 (therefore AF_INET6 and ::1).
"""
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
try:
s.connect(("::1", port))
return True
except socket.error:
return False
finally:
s.close()
def wait_for_servers(self, servers, expect_launch=True, timeout=30):
"""
Tight loop, waiting for the given server port(s) to be available.

View File

@ -2910,6 +2910,76 @@ class TestImagesWithRegistry(TestImages):
self.registry_server.deployment_flavor = 'trusted-auth'
class TestImagesIPv6(functional.FunctionalTest):
"""Verify that API and REG servers running IPv6 can communicate"""
def setUp(self):
"""
First applying monkey patches of functions and methods which have
IPv4 hardcoded.
"""
# Setting up initial monkey patch (1)
test_utils.get_unused_port_ipv4 = test_utils.get_unused_port
test_utils.get_unused_port_and_socket_ipv4 = (
test_utils.get_unused_port_and_socket)
test_utils.get_unused_port = test_utils.get_unused_port_ipv6
test_utils.get_unused_port_and_socket = (
test_utils.get_unused_port_and_socket_ipv6)
super(TestImagesIPv6, self).setUp()
self.cleanup()
# Setting up monkey patch (2), after object is ready...
self.ping_server_ipv4 = self.ping_server
self.ping_server = self.ping_server_ipv6
def tearDown(self):
# Cleaning up monkey patch (2).
self.ping_server = self.ping_server_ipv4
super(TestImagesIPv6, self).tearDown()
# Cleaning up monkey patch (1).
test_utils.get_unused_port = test_utils.get_unused_port_ipv4
test_utils.get_unused_port_and_socket = (
test_utils.get_unused_port_and_socket_ipv4)
def _url(self, path):
return "http://[::1]:%d%s" % (self.api_port, path)
def _headers(self, custom_headers=None):
base_headers = {
'X-Identity-Status': 'Confirmed',
'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
'X-Tenant-Id': TENANT1,
'X-Roles': 'member',
}
base_headers.update(custom_headers or {})
return base_headers
def test_image_list_ipv6(self):
# Image list should be empty
self.api_server.data_api = (
'glance.tests.functional.v2.registry_data_api')
self.registry_server.deployment_flavor = 'trusted-auth'
# Setting up configuration parameters properly
# (bind_host is not needed since it is replaced by monkey patches,
# but it would be reflected in the configuration file, which is
# at least improving consistency)
self.registry_server.bind_host = "::1"
self.api_server.bind_host = "::1"
self.api_server.registry_host = "::1"
self.scrubber_daemon.registry_host = "::1"
self.start_servers(**self.__dict__.copy())
requests.get(self._url('/'), headers=self._headers())
path = self._url('/v2/images')
response = requests.get(path, headers=self._headers())
self.assertEqual(200, response.status_code)
images = jsonutils.loads(response.text)['images']
self.assertEqual(0, len(images))
class TestImageDirectURLVisibility(functional.FunctionalTest):
def setUp(self):

View File

@ -382,6 +382,27 @@ def get_unused_port_and_socket():
return (port, s)
def get_unused_port_ipv6():
"""
Returns an unused port on localhost on IPv6 (uses ::1).
"""
port, s = get_unused_port_and_socket_ipv6()
s.close()
return port
def get_unused_port_and_socket_ipv6():
"""
Returns an unused port on localhost and the open socket
from which it was created, but uses IPv6 (::1).
"""
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
s.bind(('::1', 0))
# Ignoring flowinfo and scopeid...
addr, port, flowinfo, scopeid = s.getsockname()
return (port, s)
def xattr_writes_supported(path):
"""
Returns True if the we can write a file to the supplied