Skip to content
This repository was archived by the owner on May 8, 2026. It is now read-only.

Commit 5cd769b

Browse files
committed
feat(bigtable): populate reasons on why direct access was not accessible
1 parent 77952d2 commit 5cd769b

5 files changed

Lines changed: 293 additions & 3 deletions

File tree

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.api.core.InternalApi;
2020
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer;
21+
import java.net.InetAddress;
2122
import java.util.logging.Level;
2223
import java.util.logging.Logger;
2324
import javax.annotation.Nullable;
@@ -52,8 +53,55 @@ public String getValue() {
5253
public static void investigateAndReport(
5354
DirectPathCompatibleTracer tracer, @Nullable Throwable originalError) {
5455
try {
55-
// TODO: Implement checks in a future PR.
56-
// For now, default to returning "unknown".
56+
// 1. Perform GCE Check
57+
if (!GCECheck.isRunningOnGCP()) {
58+
recordAndLog(
59+
tracer,
60+
FailureReason.NOT_IN_GCP,
61+
"Direct Access investigation: Not running on GCP.",
62+
originalError);
63+
return;
64+
}
65+
66+
// 2. Perform MetadataServer Check
67+
if (!MetadataServer.isReachable()) {
68+
recordAndLog(
69+
tracer,
70+
FailureReason.METADATA_UNREACHABLE,
71+
"Direct Access investigation: Metadata server unreachable.",
72+
originalError);
73+
return;
74+
}
75+
76+
// 3. Check for Assigned IPs
77+
InetAddress ipv4 = MetadataServer.getIPv4();
78+
InetAddress ipv6 = MetadataServer.getIPv6();
79+
if (ipv4 == null && ipv6 == null) {
80+
recordAndLog(
81+
tracer,
82+
FailureReason.NO_IP_ASSIGNED,
83+
"Direct Access investigation: No IP assigned.",
84+
originalError);
85+
return;
86+
}
87+
88+
// 4. Perform Loopback Check
89+
boolean loopbackUp = false;
90+
try {
91+
loopbackUp = LoopBackInterface.isUp();
92+
} catch (Exception e) {
93+
LOG.log(Level.FINE, "Exception while checking loopback interfaces", e);
94+
}
95+
96+
if (!loopbackUp) {
97+
recordAndLog(
98+
tracer,
99+
FailureReason.LOOPBACK_DOWN,
100+
"Direct Access investigation: Loopback misconfigured.",
101+
originalError);
102+
return;
103+
}
104+
// Default fallback if investigation could not determine a specific issue recordAndLog(
57105
recordAndLog(
58106
tracer,
59107
FailureReason.UNKNOWN,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.dp;
17+
18+
import com.google.api.core.InternalApi;
19+
import com.google.common.annotations.VisibleForTesting;
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.nio.file.Files;
23+
import java.nio.file.Paths;
24+
25+
@InternalApi
26+
class GCECheck {
27+
private static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google";
28+
private static final String GCE_PRODUCTION_NAME_AFTER_2016 = "Google Compute Engine";
29+
30+
@VisibleForTesting static String systemProductName = null;
31+
32+
static boolean isRunningOnGCP() {
33+
String osName = System.getProperty("os.name");
34+
if ("Linux".equals(osName)) {
35+
String productName = getSystemProductName();
36+
return productName.contains(GCE_PRODUCTION_NAME_PRIOR_2016)
37+
|| productName.contains(GCE_PRODUCTION_NAME_AFTER_2016);
38+
}
39+
return false;
40+
}
41+
42+
private static String getSystemProductName() {
43+
if (systemProductName != null) {
44+
return systemProductName;
45+
}
46+
try {
47+
return new String(
48+
Files.readAllBytes(Paths.get("/sys/class/dmi/id/product_name")),
49+
StandardCharsets.UTF_8)
50+
.trim();
51+
} catch (IOException e) {
52+
return "";
53+
}
54+
}
55+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.dp;
17+
18+
import com.google.api.core.InternalApi;
19+
import java.net.InetAddress;
20+
import java.net.NetworkInterface;
21+
import java.util.Enumeration;
22+
23+
/**
24+
* This class verifies two main things: The OS has a functioning loopback interface (lo) with
25+
* standard localhost IPs configured.
26+
*/
27+
@InternalApi
28+
class LoopBackInterface {
29+
30+
static boolean isUp() throws Exception {
31+
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
32+
while (interfaces.hasMoreElements()) {
33+
NetworkInterface iface = interfaces.nextElement();
34+
if (iface.isLoopback() && iface.isUp()) {
35+
return true;
36+
}
37+
}
38+
return false;
39+
}
40+
41+
/**
42+
* Verifies that the standard IPv4 localhost address (127.0.0.1) is bound to a loopback interface.
43+
*/
44+
static boolean hasLocalIpv4Loopback() throws Exception {
45+
return checkLocalLoopbackAddress("127.0.0.1");
46+
}
47+
48+
/** Verifies that the standard IPv6 localhost address (::1) is bound to a loopback interface. */
49+
static boolean hasLocalIpv6Loopback() throws Exception {
50+
return checkLocalLoopbackAddress("::1");
51+
}
52+
53+
static boolean isIpPlumbed(InetAddress expectedIp) throws Exception {
54+
if (expectedIp == null) {
55+
return false;
56+
}
57+
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
58+
while (interfaces.hasMoreElements()) {
59+
NetworkInterface iface = interfaces.nextElement();
60+
if (!iface.isLoopback() && iface.isUp()) {
61+
Enumeration<InetAddress> addrs = iface.getInetAddresses();
62+
while (addrs.hasMoreElements()) {
63+
if (addrs.nextElement().equals(expectedIp)) {
64+
return true;
65+
}
66+
}
67+
}
68+
}
69+
return false;
70+
}
71+
72+
private static boolean checkLocalLoopbackAddress(String expectedIp) throws Exception {
73+
InetAddress expected = InetAddress.getByName(expectedIp);
74+
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
75+
while (interfaces.hasMoreElements()) {
76+
NetworkInterface iface = interfaces.nextElement();
77+
if (iface.isLoopback() && iface.isUp()) {
78+
Enumeration<InetAddress> addrs = iface.getInetAddresses();
79+
while (addrs.hasMoreElements()) {
80+
if (addrs.nextElement().equals(expected)) {
81+
return true;
82+
}
83+
}
84+
}
85+
}
86+
return false;
87+
}
88+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.dp;
17+
18+
import com.google.api.core.InternalApi;
19+
import java.io.BufferedReader;
20+
import java.io.InputStreamReader;
21+
import java.net.HttpURLConnection;
22+
import java.net.InetAddress;
23+
import java.net.URL;
24+
import java.nio.charset.StandardCharsets;
25+
26+
/**
27+
* Verifies that the VM can reach the GCP metadata server and checks whether GCP has successfully
28+
* assigned DirectPath-eligible IPv4 or IPv6 addresses to the instance's primary network interface
29+
* (nic0).
30+
*/
31+
@InternalApi
32+
class MetadataServer {
33+
private static final String METADATA_BASE_URL =
34+
"http://metadata.google.internal/computeMetadata/v1/";
35+
private static final String METADATA_IPV4_URL =
36+
"http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip";
37+
private static final String METADATA_IPV6_URL =
38+
"http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s";
39+
40+
private static final int REACHABILITY_TIMEOUT_MS = 2000;
41+
private static final int FETCH_IP_TIMEOUT_MS = 5000;
42+
43+
static boolean isReachable() {
44+
HttpURLConnection conn = null;
45+
try {
46+
conn = createConnection(METADATA_BASE_URL, REACHABILITY_TIMEOUT_MS);
47+
return conn.getResponseCode() == HttpURLConnection.HTTP_OK;
48+
} catch (Exception e) {
49+
return false;
50+
} finally {
51+
if (conn != null) {
52+
conn.disconnect();
53+
}
54+
}
55+
}
56+
57+
static InetAddress getIPv4() {
58+
return fetchIP(METADATA_IPV4_URL);
59+
}
60+
61+
static InetAddress getIPv6() {
62+
return fetchIP(METADATA_IPV6_URL);
63+
}
64+
65+
private static InetAddress fetchIP(String urlStr) {
66+
HttpURLConnection conn = null;
67+
try {
68+
conn = createConnection(urlStr, FETCH_IP_TIMEOUT_MS);
69+
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
70+
try (BufferedReader br =
71+
new BufferedReader(
72+
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
73+
String ipStr = br.readLine();
74+
if (ipStr != null && !ipStr.isEmpty()) {
75+
return InetAddress.getByName(ipStr.trim());
76+
}
77+
}
78+
}
79+
} catch (Exception e) {
80+
// investigator handles the exception
81+
} finally {
82+
if (conn != null) {
83+
conn.disconnect();
84+
}
85+
}
86+
return null;
87+
}
88+
89+
/** Helper to consistently configure the HttpURLConnection for the GCE Metadata Server. */
90+
private static HttpURLConnection createConnection(String urlStr, int readTimeout)
91+
throws Exception {
92+
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
93+
conn.setConnectTimeout(MetadataServer.REACHABILITY_TIMEOUT_MS);
94+
conn.setReadTimeout(readTimeout);
95+
conn.setRequestProperty("Metadata-Flavor", "Google");
96+
return conn;
97+
}
98+
}

google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ public void testInvestigationTriggeredOnFailure() {
149149

150150
// Execute the captured runnable to ensure it safely calls the tracer
151151
runnableCaptor.getValue().run();
152-
verify(mockTracer).recordFailure(DirectAccessInvestigator.FailureReason.UNKNOWN);
152+
// just make sure it matches with any FailureReason.
153+
verify(mockTracer).recordFailure(any(DirectAccessInvestigator.FailureReason.class));
153154
}
154155
}

0 commit comments

Comments
 (0)