From 5cd769b70e2dc5b35241acf5e12581d1c771371d Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Fri, 24 Apr 2026 16:45:57 -0400 Subject: [PATCH] feat(bigtable): populate reasons on why direct access was not accessible --- .../internal/dp/DirectAccessInvestigator.java | 52 +++++++++- .../data/v2/internal/dp/GCECheck.java | 55 +++++++++++ .../v2/internal/dp/LoopBackInterface.java | 88 +++++++++++++++++ .../data/v2/internal/dp/MetadataServer.java | 98 +++++++++++++++++++ .../dp/ClassicDirectAccessCheckerTest.java | 3 +- 5 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java index 2a80672d82..1576331d41 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java @@ -18,6 +18,7 @@ import com.google.api.core.InternalApi; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import java.net.InetAddress; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -52,8 +53,55 @@ public String getValue() { public static void investigateAndReport( DirectPathCompatibleTracer tracer, @Nullable Throwable originalError) { try { - // TODO: Implement checks in a future PR. - // For now, default to returning "unknown". + // 1. Perform GCE Check + if (!GCECheck.isRunningOnGCP()) { + recordAndLog( + tracer, + FailureReason.NOT_IN_GCP, + "Direct Access investigation: Not running on GCP.", + originalError); + return; + } + + // 2. Perform MetadataServer Check + if (!MetadataServer.isReachable()) { + recordAndLog( + tracer, + FailureReason.METADATA_UNREACHABLE, + "Direct Access investigation: Metadata server unreachable.", + originalError); + return; + } + + // 3. Check for Assigned IPs + InetAddress ipv4 = MetadataServer.getIPv4(); + InetAddress ipv6 = MetadataServer.getIPv6(); + if (ipv4 == null && ipv6 == null) { + recordAndLog( + tracer, + FailureReason.NO_IP_ASSIGNED, + "Direct Access investigation: No IP assigned.", + originalError); + return; + } + + // 4. Perform Loopback Check + boolean loopbackUp = false; + try { + loopbackUp = LoopBackInterface.isUp(); + } catch (Exception e) { + LOG.log(Level.FINE, "Exception while checking loopback interfaces", e); + } + + if (!loopbackUp) { + recordAndLog( + tracer, + FailureReason.LOOPBACK_DOWN, + "Direct Access investigation: Loopback misconfigured.", + originalError); + return; + } + // Default fallback if investigation could not determine a specific issue recordAndLog( recordAndLog( tracer, FailureReason.UNKNOWN, diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java new file mode 100644 index 0000000000..051756144c --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed 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 + * + * https://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. + */ +package com.google.cloud.bigtable.data.v2.internal.dp; + +import com.google.api.core.InternalApi; +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +@InternalApi +class GCECheck { + private static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google"; + private static final String GCE_PRODUCTION_NAME_AFTER_2016 = "Google Compute Engine"; + + @VisibleForTesting static String systemProductName = null; + + static boolean isRunningOnGCP() { + String osName = System.getProperty("os.name"); + if ("Linux".equals(osName)) { + String productName = getSystemProductName(); + return productName.contains(GCE_PRODUCTION_NAME_PRIOR_2016) + || productName.contains(GCE_PRODUCTION_NAME_AFTER_2016); + } + return false; + } + + private static String getSystemProductName() { + if (systemProductName != null) { + return systemProductName; + } + try { + return new String( + Files.readAllBytes(Paths.get("/sys/class/dmi/id/product_name")), + StandardCharsets.UTF_8) + .trim(); + } catch (IOException e) { + return ""; + } + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java new file mode 100644 index 0000000000..503283ee38 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java @@ -0,0 +1,88 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed 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 + * + * https://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. + */ +package com.google.cloud.bigtable.data.v2.internal.dp; + +import com.google.api.core.InternalApi; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; + +/** + * This class verifies two main things: The OS has a functioning loopback interface (lo) with + * standard localhost IPs configured. + */ +@InternalApi +class LoopBackInterface { + + static boolean isUp() throws Exception { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + if (iface.isLoopback() && iface.isUp()) { + return true; + } + } + return false; + } + + /** + * Verifies that the standard IPv4 localhost address (127.0.0.1) is bound to a loopback interface. + */ + static boolean hasLocalIpv4Loopback() throws Exception { + return checkLocalLoopbackAddress("127.0.0.1"); + } + + /** Verifies that the standard IPv6 localhost address (::1) is bound to a loopback interface. */ + static boolean hasLocalIpv6Loopback() throws Exception { + return checkLocalLoopbackAddress("::1"); + } + + static boolean isIpPlumbed(InetAddress expectedIp) throws Exception { + if (expectedIp == null) { + return false; + } + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + if (!iface.isLoopback() && iface.isUp()) { + Enumeration addrs = iface.getInetAddresses(); + while (addrs.hasMoreElements()) { + if (addrs.nextElement().equals(expectedIp)) { + return true; + } + } + } + } + return false; + } + + private static boolean checkLocalLoopbackAddress(String expectedIp) throws Exception { + InetAddress expected = InetAddress.getByName(expectedIp); + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + if (iface.isLoopback() && iface.isUp()) { + Enumeration addrs = iface.getInetAddresses(); + while (addrs.hasMoreElements()) { + if (addrs.nextElement().equals(expected)) { + return true; + } + } + } + } + return false; + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java new file mode 100644 index 0000000000..d58b8f68e1 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java @@ -0,0 +1,98 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed 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 + * + * https://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. + */ +package com.google.cloud.bigtable.data.v2.internal.dp; + +import com.google.api.core.InternalApi; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +/** + * Verifies that the VM can reach the GCP metadata server and checks whether GCP has successfully + * assigned DirectPath-eligible IPv4 or IPv6 addresses to the instance's primary network interface + * (nic0). + */ +@InternalApi +class MetadataServer { + private static final String METADATA_BASE_URL = + "http://metadata.google.internal/computeMetadata/v1/"; + private static final String METADATA_IPV4_URL = + "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip"; + private static final String METADATA_IPV6_URL = + "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s"; + + private static final int REACHABILITY_TIMEOUT_MS = 2000; + private static final int FETCH_IP_TIMEOUT_MS = 5000; + + static boolean isReachable() { + HttpURLConnection conn = null; + try { + conn = createConnection(METADATA_BASE_URL, REACHABILITY_TIMEOUT_MS); + return conn.getResponseCode() == HttpURLConnection.HTTP_OK; + } catch (Exception e) { + return false; + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + static InetAddress getIPv4() { + return fetchIP(METADATA_IPV4_URL); + } + + static InetAddress getIPv6() { + return fetchIP(METADATA_IPV6_URL); + } + + private static InetAddress fetchIP(String urlStr) { + HttpURLConnection conn = null; + try { + conn = createConnection(urlStr, FETCH_IP_TIMEOUT_MS); + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (BufferedReader br = + new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + String ipStr = br.readLine(); + if (ipStr != null && !ipStr.isEmpty()) { + return InetAddress.getByName(ipStr.trim()); + } + } + } + } catch (Exception e) { + // investigator handles the exception + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return null; + } + + /** Helper to consistently configure the HttpURLConnection for the GCE Metadata Server. */ + private static HttpURLConnection createConnection(String urlStr, int readTimeout) + throws Exception { + HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); + conn.setConnectTimeout(MetadataServer.REACHABILITY_TIMEOUT_MS); + conn.setReadTimeout(readTimeout); + conn.setRequestProperty("Metadata-Flavor", "Google"); + return conn; + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java index 5ce624cf44..2ce79826c6 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java @@ -149,6 +149,7 @@ public void testInvestigationTriggeredOnFailure() { // Execute the captured runnable to ensure it safely calls the tracer runnableCaptor.getValue().run(); - verify(mockTracer).recordFailure(DirectAccessInvestigator.FailureReason.UNKNOWN); + // just make sure it matches with any FailureReason. + verify(mockTracer).recordFailure(any(DirectAccessInvestigator.FailureReason.class)); } }