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:
parent
f72d9556b4
commit
8be3e10586
|
@ -41,6 +41,7 @@ except ImportError:
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import encodeutils
|
from oslo_utils import encodeutils
|
||||||
|
from oslo_utils import netutils
|
||||||
import six
|
import six
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
|
# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
|
||||||
|
@ -379,7 +380,10 @@ class BaseClient(object):
|
||||||
action = urlparse.quote(action)
|
action = urlparse.quote(action)
|
||||||
path = '/'.join([self.doc_root or '', action.lstrip('/')])
|
path = '/'.join([self.doc_root or '', action.lstrip('/')])
|
||||||
scheme = "https" if self.use_ssl else "http"
|
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):
|
if isinstance(params, dict):
|
||||||
for (key, value) in list(params.items()):
|
for (key, value) in list(params.items()):
|
||||||
|
|
|
@ -281,6 +281,8 @@ class ApiServer(Server):
|
||||||
self.server_name = 'api'
|
self.server_name = 'api'
|
||||||
self.server_module = 'glance.cmd.%s' % self.server_name
|
self.server_module = 'glance.cmd.%s' % self.server_name
|
||||||
self.default_store = kwargs.get("default_store", "file")
|
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.key_file = ""
|
||||||
self.cert_file = ""
|
self.cert_file = ""
|
||||||
self.metadata_encryption_key = "012345678901234567890123456789ab"
|
self.metadata_encryption_key = "012345678901234567890123456789ab"
|
||||||
|
@ -321,12 +323,12 @@ class ApiServer(Server):
|
||||||
self.conf_base = """[DEFAULT]
|
self.conf_base = """[DEFAULT]
|
||||||
debug = %(debug)s
|
debug = %(debug)s
|
||||||
default_log_levels = eventlet.wsgi.server=DEBUG
|
default_log_levels = eventlet.wsgi.server=DEBUG
|
||||||
bind_host = 127.0.0.1
|
bind_host = %(bind_host)s
|
||||||
bind_port = %(bind_port)s
|
bind_port = %(bind_port)s
|
||||||
key_file = %(key_file)s
|
key_file = %(key_file)s
|
||||||
cert_file = %(cert_file)s
|
cert_file = %(cert_file)s
|
||||||
metadata_encryption_key = %(metadata_encryption_key)s
|
metadata_encryption_key = %(metadata_encryption_key)s
|
||||||
registry_host = 127.0.0.1
|
registry_host = %(registry_host)s
|
||||||
registry_port = %(registry_port)s
|
registry_port = %(registry_port)s
|
||||||
use_user_token = %(use_user_token)s
|
use_user_token = %(use_user_token)s
|
||||||
send_identity_credentials = %(send_identity_credentials)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',
|
self.sql_connection = os.environ.get('GLANCE_TEST_SQL_CONNECTION',
|
||||||
default_sql_connection)
|
default_sql_connection)
|
||||||
|
|
||||||
|
self.bind_host = "127.0.0.1"
|
||||||
self.pid_file = os.path.join(self.test_dir, "registry.pid")
|
self.pid_file = os.path.join(self.test_dir, "registry.pid")
|
||||||
self.log_file = os.path.join(self.test_dir, "registry.log")
|
self.log_file = os.path.join(self.test_dir, "registry.log")
|
||||||
self.owner_is_tenant = True
|
self.owner_is_tenant = True
|
||||||
|
@ -475,7 +478,7 @@ class RegistryServer(Server):
|
||||||
|
|
||||||
self.conf_base = """[DEFAULT]
|
self.conf_base = """[DEFAULT]
|
||||||
debug = %(debug)s
|
debug = %(debug)s
|
||||||
bind_host = 127.0.0.1
|
bind_host = %(bind_host)s
|
||||||
bind_port = %(bind_port)s
|
bind_port = %(bind_port)s
|
||||||
log_file = %(log_file)s
|
log_file = %(log_file)s
|
||||||
sql_connection = %(sql_connection)s
|
sql_connection = %(sql_connection)s
|
||||||
|
@ -534,6 +537,8 @@ class ScrubberDaemon(Server):
|
||||||
self.server_module = 'glance.cmd.%s' % self.server_name
|
self.server_module = 'glance.cmd.%s' % self.server_name
|
||||||
self.daemon = daemon
|
self.daemon = daemon
|
||||||
|
|
||||||
|
self.registry_host = "127.0.0.1"
|
||||||
|
|
||||||
self.image_dir = os.path.join(self.test_dir, "images")
|
self.image_dir = os.path.join(self.test_dir, "images")
|
||||||
self.scrub_time = 5
|
self.scrub_time = 5
|
||||||
self.pid_file = os.path.join(self.test_dir, "scrubber.pid")
|
self.pid_file = os.path.join(self.test_dir, "scrubber.pid")
|
||||||
|
@ -556,7 +561,7 @@ log_file = %(log_file)s
|
||||||
daemon = %(daemon)s
|
daemon = %(daemon)s
|
||||||
wakeup_time = 2
|
wakeup_time = 2
|
||||||
scrub_time = %(scrub_time)s
|
scrub_time = %(scrub_time)s
|
||||||
registry_host = 127.0.0.1
|
registry_host = %(registry_host)s
|
||||||
registry_port = %(registry_port)s
|
registry_port = %(registry_port)s
|
||||||
metadata_encryption_key = %(metadata_encryption_key)s
|
metadata_encryption_key = %(metadata_encryption_key)s
|
||||||
lock_path = %(lock_path)s
|
lock_path = %(lock_path)s
|
||||||
|
@ -817,6 +822,25 @@ class FunctionalTest(test_utils.BaseTestCase):
|
||||||
finally:
|
finally:
|
||||||
s.close()
|
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):
|
def wait_for_servers(self, servers, expect_launch=True, timeout=30):
|
||||||
"""
|
"""
|
||||||
Tight loop, waiting for the given server port(s) to be available.
|
Tight loop, waiting for the given server port(s) to be available.
|
||||||
|
|
|
@ -2910,6 +2910,76 @@ class TestImagesWithRegistry(TestImages):
|
||||||
self.registry_server.deployment_flavor = 'trusted-auth'
|
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):
|
class TestImageDirectURLVisibility(functional.FunctionalTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -382,6 +382,27 @@ def get_unused_port_and_socket():
|
||||||
return (port, s)
|
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):
|
def xattr_writes_supported(path):
|
||||||
"""
|
"""
|
||||||
Returns True if the we can write a file to the supplied
|
Returns True if the we can write a file to the supplied
|
||||||
|
|
Loading…
Reference in New Issue