Browse Source

hue ipv6 changes

Athithyaa Selvam 4 months ago
parent
commit
e1bec0e64c

+ 3 - 0
desktop/conf.dist/hue.ini

@@ -27,6 +27,9 @@ secret_key=
 http_host=0.0.0.0
 http_port=8888
 
+# Enable IPv6 support. If true, Hue will attempt to bind to IPv6 addresses.
+## enable_ipv6=false
+
 # A comma-separated list of available Hue load balancers
 ## hue_load_balancer=
 

+ 3 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -32,6 +32,9 @@
   http_host=0.0.0.0
   http_port=8000
 
+  # Enable IPv6 support. If true, Hue will attempt to bind to IPv6 addresses.
+  ## enable_ipv6=false
+
   # A comma-separated list of available Hue load balancers
   ## hue_load_balancer=
 

+ 6 - 0
desktop/core/src/desktop/conf.py

@@ -213,6 +213,12 @@ HTTP_PORT = Config(
   type=int,
   default=8888)
 
+ENABLE_IPV6 = Config(
+  key="enable_ipv6",
+  help=_("Enable IPv6 support. If true, Hue will attempt to bind to IPv6 addresses."),
+  type=coerce_bool,
+  default=False)
+
 HTTP_ALLOWED_METHODS = Config(
   key="http_allowed_methods",
   help=_("HTTP methods the server will be allowed to service."),

+ 77 - 0
desktop/core/src/desktop/lib/ip_utils.py

@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import ipaddress
+import logging
+import socket
+
+
+def fetch_ipv6_bind_address(http_host, http_port):
+  """
+  Formats the bind address for IPv6, handling common input scenarios:
+  - Already bracketed IPv6: [2001:db8::1] -> [2001:db8::1]:8888
+  - Raw IPv6 literal: 2001:db8::1 -> [2001:db8::1]:8888
+  - IPv4 or hostname: 0.0.0.0 or hostname.com -> resolve to IPv6 and bracket
+  - Special addresses: ::1, :: -> [::1]:8888, [::]:8888
+  """
+  try:
+    # Case 1: Already-bracketed IPv6; validate the inner literal before using.
+    if http_host.startswith('[') and http_host.endswith(']'):
+      ipv6_part = http_host[1:-1]
+      ipaddress.IPv6Address(ipv6_part)  # Validate it's a proper IPv6
+      bind_addr = f"{http_host}:{http_port}"
+      logging.info(f"Using pre-bracketed IPv6 address: {http_host}")
+      return bind_addr
+
+    # Case 2: Try to parse as raw IPv6 address literal
+    try:
+      ipaddress.IPv6Address(http_host)
+      bind_addr = f"[{http_host}]:{http_port}"
+      logging.info(f"Formatted IPv6 literal {http_host} as {bind_addr}")
+      return bind_addr
+    except ipaddress.AddressValueError:
+      # Not an IPv6 literal, continue to hostname resolution
+      pass
+
+    # Case 3: Not an IPv6 literal, treat as hostname/IPv4 and resolve to IPv6
+    try:
+      addr_info = socket.getaddrinfo(http_host, None, socket.AF_INET6, socket.SOCK_STREAM)
+      if addr_info:
+        # Get the first IPv6 address from resolution
+        ipv6_addr = str(addr_info[0][4][0])
+        # Remove any zone identifier (like %eth0) for binding
+        if '%' in ipv6_addr:
+          ipv6_addr = ipv6_addr.split('%')[0]
+        bind_addr = f"[{ipv6_addr}]:{http_port}"
+        logging.info(f"Resolved hostname {http_host} to IPv6 address {ipv6_addr}")
+        return bind_addr
+      else:
+        # No IPv6 resolution available, fallback
+        bind_addr = f"{http_host}:{http_port}"
+        logging.warning(f"No IPv6 address found for {http_host}, using as-is: {bind_addr}")
+        return bind_addr
+    except socket.gaierror as e:
+      # DNS resolution failed
+      bind_addr = f"{http_host}:{http_port}"
+      logging.warning(f"IPv6 DNS resolution failed for {http_host}: {e}, using as-is: {bind_addr}")
+      return bind_addr
+
+  except Exception as e:
+    # Catch-all to avoid breaking bind path on unexpected inputs.
+    bind_addr = f"{http_host}:{http_port}"
+    logging.warning(f"IPv6 address formatting failed for {http_host}: {e}, using as-is: {bind_addr}")
+    return bind_addr

+ 47 - 0
desktop/core/src/desktop/lib/ip_utils_test.py

@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import socket
+
+from desktop.lib.ip_utils import fetch_ipv6_bind_address
+
+
+def test_fetch_ipv6_with_hostname_aaaa(monkeypatch):
+  addrinfo = [
+    (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('2001:db8::1', 0, 0, 0))
+  ]
+  monkeypatch.setattr(socket, 'getaddrinfo', lambda *args, **kwargs: addrinfo)
+
+  bind = fetch_ipv6_bind_address('hue.local', '8888')
+  assert bind == '[2001:db8::1]:8888'
+
+
+def test_fetch_ipv6_zone_id_stripped(monkeypatch):
+  addrinfo = [
+    (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('fe80::1%en0', 0, 0, 0))
+  ]
+  monkeypatch.setattr(socket, 'getaddrinfo', lambda *args, **kwargs: addrinfo)
+
+  bind = fetch_ipv6_bind_address('hue.local', '7777')
+  assert bind == '[fe80::1]:7777'
+
+
+def test_fetch_ipv6_no_aaaa_fallback(monkeypatch):
+  monkeypatch.setattr(socket, 'getaddrinfo', lambda *args, **kwargs: [])
+
+  bind = fetch_ipv6_bind_address('hue-no-aaaa.local', '8000')
+  assert bind == 'hue-no-aaaa.local:8000'

+ 21 - 8
desktop/core/src/desktop/management/commands/rungunicornserver.py

@@ -41,6 +41,7 @@ from six import iteritems
 
 import desktop.log
 from desktop import conf
+from desktop.lib.ip_utils import fetch_ipv6_bind_address
 from desktop.lib.paths import get_desktop_root
 from filebrowser.utils import parse_broker_url
 
@@ -171,15 +172,27 @@ class StandaloneApplication(gunicorn.app.base.BaseApplication):
 
 def argprocessing(args=[], options={}):
   global PID_FILE
-  if options['bind']:
-    http_port = "8888"
-    bind_addr = options['bind']
-    if ":" in bind_addr:
-      http_port = bind_addr.split(":")[1]
-    PID_FILE = "/tmp/hue_%s.pid" % (http_port)
+
+  ipv6_enabled = conf.ENABLE_IPV6.get()  # This is already a bool
+
+  if options.get('bind'):
+      http_port = "8888"
+      bind_addr = options['bind']
+      if ":" in bind_addr:
+          http_port = bind_addr.split(":")[-1]  # Use last part in case of IPv6
+      PID_FILE = f"/tmp/hue_{http_port}.pid"
   else:
-    bind_addr = conf.HTTP_HOST.get() + ":" + str(conf.HTTP_PORT.get())
-    PID_FILE = "/tmp/hue_%s.pid" % (conf.HTTP_PORT.get())
+      http_host = conf.HTTP_HOST.get()
+      http_port = str(conf.HTTP_PORT.get())
+
+      if ipv6_enabled:
+          bind_addr = fetch_ipv6_bind_address(http_host, http_port)
+      else:
+          bind_addr = f"{http_host}:{http_port}"
+          logging.info(f"IPv6 disabled, using standard format: {bind_addr}")
+
+      PID_FILE = f"/tmp/hue_{http_port}.pid"
+
   options['bind_addr'] = bind_addr
 
   # Currently gunicorn does not support passphrase suppored SSL Keyfile