diff --git a/glance/common/client.py b/glance/common/client.py index e35eaa5ea7..53a3691da9 100644 --- a/glance/common/client.py +++ b/glance/common/client.py @@ -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()): diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py index bee7c3581b..cd6c8f8ec4 100644 --- a/glance/tests/functional/__init__.py +++ b/glance/tests/functional/__init__.py @@ -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. diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py index cec1a7f0ef..a109e6a531 100644 --- a/glance/tests/functional/v2/test_images.py +++ b/glance/tests/functional/v2/test_images.py @@ -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): diff --git a/glance/tests/utils.py b/glance/tests/utils.py index 3757483ac7..190c935a0d 100644 --- a/glance/tests/utils.py +++ b/glance/tests/utils.py @@ -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