This is an automated email from the ASF dual-hosted git repository.
JiaLiangC pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git
The following commit(s) were added to refs/heads/trunk by this push:
new a01b6745cf AMBARI-26662: Fix HDFS NameNode/ZKFC summary labels and
incorrect federation detection (#4223)
a01b6745cf is described below
commit a01b6745cf618b20accde2e670b7c78115c21367
Author: Himanshu Maurya <[email protected]>
AuthorDate: Wed Sep 23 04:50:22 2026 +0530
AMBARI-26662: Fix HDFS NameNode/ZKFC summary labels and incorrect
federation detection (#4223)
---
.../latest/src/hooks/useHDFSConfigUpdater.ts | 99 +++++++++++++------
ambari-web/latest/src/locales/en/translation.json | 1 +
ambari-web/latest/src/locales/zh/translation.json | 1 +
.../screens/Services/ServiceComponents.test.tsx | 91 +++++++++++++++++
.../src/screens/Services/ServiceComponents.tsx | 108 +++++++++------------
ambari-web/latest/src/screens/messages.ts | 1 +
6 files changed, 211 insertions(+), 90 deletions(-)
diff --git a/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
b/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
index 39a31a1a04..1135bd0bf9 100644
--- a/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
@@ -82,6 +82,34 @@ export const useHDFSConfigUpdater = () => {
}
}, [JSON.stringify(configsData?.items)]);
+ // Group by the namespaces the installed NameNodes report, the way Ember's
HDFS
+ // masterComponentGroups does. Deriving them from dfs.nameservices instead
counts a
+ // declared-but-unpopulated nameservice, which flips the summary into the
federated
+ // layout on a cluster that only added an observer NameNode.
+ const buildNameNodeGroups = (nameNodes: any[], nameSpacePath: string) => {
+ const groups: any[] = [];
+ nameNodes.forEach((nameNode: any) => {
+ const nameSpace = get(nameNode, nameSpacePath) || "default";
+ const hostName =
+ get(nameNode, "host_name") || get(nameNode, "HostRoles.host_name");
+ let group = find(groups, ["name", nameSpace]);
+ if (!group) {
+ group = {
+ name: nameSpace,
+ title: nameSpace,
+ hosts: [],
+ components: ["NAMENODE", "ZKFC"],
+ clusterId: "default",
+ };
+ groups.push(group);
+ }
+ if (hostName && !group.hosts.includes(hostName)) {
+ group.hosts.push(hostName);
+ }
+ });
+ return groups;
+ };
+
function inferNamespace() {
const hdfsModel = cloneDeep(allServiceModels["hdfs"]);
const isHAEnabled = hdfsModel?.isNameNodeHaEnabled;
@@ -138,16 +166,10 @@ export const useHDFSConfigUpdater = () => {
}
});
- // Create masterComponentGroups for federation detection
- const masterComponentGroups = nameSpaces
- .filter((ns: any) => ns && ns.nameSpace && ns.hostNames)
- .map((ns: any) => ({
- name: ns.nameSpace,
- title: ns.nameSpace,
- hosts: ns.hostNames.filter((host: string) => host), // Filter
out null/undefined hosts
- components: ["NAMENODE", "ZKFC"],
- clusterId: "default"
- }));
+ const masterComponentGroups = buildNameNodeGroups(
+ allNameNodes,
+ "haNameSpace"
+ );
hdfsModel.updateConfig({
namespaces: nameSpaces,
@@ -314,34 +336,51 @@ export const useHDFSConfigUpdater = () => {
currentConfig[ServiceComponentFields.HDFS.journalNodes] =
componentHosts("JOURNALNODE");
- // Update masterComponentGroups when master components change
+ // Rebuilt on every poll, so it has to use the same NameNode-derived
grouping as
+ // inferNamespace or it reintroduces the nameservice that was just
discarded.
if (isHAEnabled && configsData?.items) {
const hdfsSiteConfigs = find(configsData?.items, ["type", "hdfs-site"]);
if (hdfsSiteConfigs) {
const properties = get(hdfsSiteConfigs, "properties", {});
const nameSpaceProperty = properties["dfs.nameservices"];
if (nameSpaceProperty) {
- const nameSpaces = nameSpaceProperty.split(",");
- const masterComponentGroups = nameSpaces.map((nameSpace: string) => {
- const nameNodeIdsProperty =
properties[`dfs.ha.namenodes.${nameSpace}`];
- let hosts: string[] = [];
- if (nameNodeIdsProperty) {
- const nameNodeIds = nameNodeIdsProperty.split(",");
- hosts = nameNodeIds.map((id: any) => {
- const propertyValue =
properties[`dfs.namenode.http-address.${nameSpace}.${id}`];
- const matches = propertyValue &&
propertyValue.match(/([\D\d]+)\:\d+$/);
- return matches && matches[1];
- }).filter((host: string) => host); // Filter out null/undefined
hosts
+ const nameSpaces = nameSpaceProperty
+ .split(",")
+ .map((nameSpace: string) => {
+ const nameNodeIdsProperty =
properties[`dfs.ha.namenodes.${nameSpace}`];
+ let hostNames: string[] = [];
+ if (nameNodeIdsProperty) {
+ const nameNodeIds = nameNodeIdsProperty.split(",");
+ hostNames = nameNodeIds.map((id: any) => {
+ const propertyValue =
properties[`dfs.namenode.http-address.${nameSpace}.${id}`];
+ const matches = propertyValue &&
propertyValue.match(/([\D\d]+)\:\d+$/);
+ return matches && matches[1];
+ }).filter((host: string) => host); // Filter out
null/undefined hosts
+ }
+ return { nameSpace, hostNames };
+ });
+
+ const nameNodeComponent = masterComponents.find(
+ (comp: any) => comp.componentName === "NAMENODE"
+ );
+ const allNameNodes = map(
+ nameNodeComponent?.hostComponents,
+ "HostRoles"
+ );
+ allNameNodes.forEach((component: any) => {
+ const nameSpaceObject = nameSpaces.find(
+ (ns: any) =>
+ ns && ns.hostNames &&
ns.hostNames.includes(component.host_name)
+ );
+ if (nameSpaceObject) {
+ set(component, "haNameSpace", nameSpaceObject.nameSpace);
}
- return {
- name: nameSpace,
- title: nameSpace,
- hosts: hosts,
- components: ["NAMENODE", "ZKFC"],
- clusterId: "default"
- };
});
- currentConfig.federationNamespaces = masterComponentGroups;
+
+ currentConfig.federationNamespaces = buildNameNodeGroups(
+ allNameNodes,
+ "haNameSpace"
+ );
}
}
} else {
diff --git a/ambari-web/latest/src/locales/en/translation.json
b/ambari-web/latest/src/locales/en/translation.json
index cbb313a808..8dce984461 100644
--- a/ambari-web/latest/src/locales/en/translation.json
+++ b/ambari-web/latest/src/locales/en/translation.json
@@ -2155,6 +2155,7 @@
"services.service.summary.nameNode":"NameNode Web UI",
"services.service.summary.nameNode.active":"Active NameNode",
"services.service.summary.nameNode.standby":"Standby NameNode",
+ "services.service.summary.nameNode.observer":"Observer NameNode",
"services.service.summary.jobTracker":"JobTracker",
"services.service.summary.jobTrackerWebUI":"JobTracker Web UI",
"services.service.summary.hbaseMaster":"HBase Master Web UI",
diff --git a/ambari-web/latest/src/locales/zh/translation.json
b/ambari-web/latest/src/locales/zh/translation.json
index f9de83342d..cb2d1c4794 100644
--- a/ambari-web/latest/src/locales/zh/translation.json
+++ b/ambari-web/latest/src/locales/zh/translation.json
@@ -2029,6 +2029,7 @@
"services.service.summary.nameNode": "NameNode Web UI",
"services.service.summary.nameNode.active": "活动 NameNode",
"services.service.summary.nameNode.standby": "备用 NameNode",
+ "services.service.summary.nameNode.observer": "观察者 NameNode",
"services.service.summary.jobTracker": "JobTracker",
"services.service.summary.jobTrackerWebUI": "JobTracker Web UI",
"services.service.summary.hbaseMaster": "HBase Master Web UI",
diff --git a/ambari-web/latest/src/screens/Services/ServiceComponents.test.tsx
b/ambari-web/latest/src/screens/Services/ServiceComponents.test.tsx
index f6b3254a14..737c161568 100644
--- a/ambari-web/latest/src/screens/Services/ServiceComponents.test.tsx
+++ b/ambari-web/latest/src/screens/Services/ServiceComponents.test.tsx
@@ -104,6 +104,41 @@ function renderComponents(data: any[]) {
);
}
+function renderHDFSSummary(hdfsModel: any) {
+ return render(
+ <MemoryRouter>
+ <HostsListStateProvider>
+ <ServiceContext.Provider
+ value={
+ {
+ allServiceModels: { hdfs: hdfsModel },
+ } as unknown as ComponentProps<
+ typeof ServiceContext.Provider
+ >["value"]
+ }
+ >
+ <ServiceComponents serviceName="HDFS" alerts={[]} />
+ </ServiceContext.Provider>
+ </HostsListStateProvider>
+ </MemoryRouter>
+ );
+}
+
+function nameNodeHostComponent(hostName: string, haStatus: string) {
+ return {
+ HostRoles: { host_name: hostName, component_name: "NAMENODE" },
+ state: "STARTED",
+ haStatus,
+ };
+}
+
+function zkfcHostComponent(hostName: string) {
+ return {
+ HostRoles: { host_name: hostName, component_name: "ZKFC" },
+ state: "STARTED",
+ };
+}
+
describe("generic service summary", () => {
afterEach(cleanup);
@@ -123,3 +158,59 @@ describe("generic service summary", () => {
expect(screen.getByText("No components to display")).toBeTruthy();
});
});
+
+describe("HDFS NameNode/ZKFC summary", () => {
+ afterEach(cleanup);
+
+ it("labels each NameNode by HA state and pairs it with its host's ZKFC", ()
=> {
+ renderHDFSSummary({
+ isNameNodeHaEnabled: true,
+ federationNamespaces: [{ name: "default", title: "default", hosts: [],
components: [], clusterId: "default" }],
+ masterComponents: [
+ {
+ componentName: "NAMENODE",
+ hostComponents: [
+ nameNodeHostComponent("nn1.example.com", "active"),
+ nameNodeHostComponent("nn2.example.com", "observer"),
+ ],
+ },
+ ],
+ slaveComponents: [
+ {
+ componentName: "ZKFC",
+ displayName: "ZKFailoverController",
+ hostComponents: [
+ zkfcHostComponent("nn1.example.com"),
+ zkfcHostComponent("nn2.example.com"),
+ ],
+ },
+ ],
+ });
+
+ expect(screen.getByText("Active NameNode")).toBeTruthy();
+ expect(screen.getByText("Observer NameNode")).toBeTruthy();
+ expect(screen.getAllByText("ZKFailoverController")).toHaveLength(2);
+ });
+
+ it("stays out of the federated layout when only one namespace has an
installed NameNode", () => {
+ renderHDFSSummary({
+ isNameNodeHaEnabled: true,
+ // A declared-but-unpopulated second namespace must not flip isFederated.
+ federationNamespaces: [
+ { name: "ns1", title: "ns1", hosts: ["nn1.example.com"], components:
["NAMENODE", "ZKFC"], clusterId: "default" },
+ ],
+ masterComponents: [
+ {
+ componentName: "NAMENODE",
+ hostComponents: [nameNodeHostComponent("nn1.example.com", "active")],
+ },
+ ],
+ slaveComponents: [
+ { componentName: "ZKFC", displayName: "ZKFailoverController",
hostComponents: [] },
+ ],
+ });
+
+ expect(screen.getByText("Active NameNode")).toBeTruthy();
+ expect(screen.queryByText(/Namespace:/)).toBeNull();
+ });
+});
diff --git a/ambari-web/latest/src/screens/Services/ServiceComponents.tsx
b/ambari-web/latest/src/screens/Services/ServiceComponents.tsx
index 42b8b8e6e5..2a17ef215c 100644
--- a/ambari-web/latest/src/screens/Services/ServiceComponents.tsx
+++ b/ambari-web/latest/src/screens/Services/ServiceComponents.tsx
@@ -20,6 +20,7 @@ import {
cloneDeep,
filter,
find,
+ get,
isEmpty,
isObject,
lowerCase,
@@ -34,7 +35,7 @@ import { Badge, Col, Row, Stack } from "react-bootstrap";
import { useContext, useEffect } from "react";
import { ServiceContext } from "../../store/ServiceContext";
import Spinner from "../../components/Spinner";
-import { pluralize } from "../../Utils/Utility";
+import { pluralize, translate } from "../../Utils/Utility";
import modalManager from "../../store/ModalManager";
import { AlertsModal } from "./ServiceAlerts";
import useClusterNavigate from "../../hooks/useClusterNavigate";
@@ -73,6 +74,20 @@ const getComponentDisplayName = (componentName: string):
string => {
return componentDisplayNames[componentName] || componentName;
};
+// Mirrors Ember's display_name_advanced; observer is a third HA state.
+const getNameNodeLabel = (haStatus?: string) => {
+ switch ((haStatus || "").toLowerCase()) {
+ case "active":
+ return translate("services.service.summary.nameNode.active");
+ case "standby":
+ return translate("services.service.summary.nameNode.standby");
+ case "observer":
+ return translate("services.service.summary.nameNode.observer");
+ default:
+ return getComponentDisplayName("NAMENODE");
+ }
+};
+
const TOOLTIP_MESSAGES = {
GENERAL: {
MAINTENANCE_MODE: 'Service is in maintenance mode',
@@ -103,6 +118,30 @@ function HDFSSummary({ alerts }: { alerts: any }) {
if (!hdfsModel) {
return <Spinner />;
}
+
+ // Each ZKFC follows the NameNode on its host, as Ember's
+ // getGroupedMasterComponents orders them.
+ const zkfcHostComponents = get(
+ find(slaveComponents, ["componentName", "ZKFC"]),
+ "hostComponents",
+ []
+ );
+ const nameNodeGroupComponents: any[] = [];
+ get(
+ find(masterComponents, ["componentName", "NAMENODE"]),
+ "hostComponents",
+ []
+ ).forEach((hostComponent: any) => {
+ nameNodeGroupComponents.push(hostComponent);
+ const zkfc = find(zkfcHostComponents, (candidate: any) =>
+ get(candidate, "HostRoles.host_name") ===
+ get(hostComponent, "HostRoles.host_name")
+ );
+ if (zkfc) {
+ nameNodeGroupComponents.push(zkfc);
+ }
+ });
+
return (
<>
{isFederated && (
@@ -113,10 +152,7 @@ function HDFSSummary({ alerts }: { alerts: any }) {
/>
)}
<Row>
- {!isFederated && find(masterComponents, [
- "componentName",
- "NAMENODE",
- ])?.hostComponents?.map((hostComponent: any) => {
+ {!isFederated && nameNodeGroupComponents.map((hostComponent: any) =>
{
const component = hostComponent.HostRoles.component_name;
const icon =
hostComponent.passiveState == "OFF"
@@ -180,7 +216,13 @@ function HDFSSummary({ alerts }: { alerts: any }) {
);
}}
>
- {hostComponent.haStatus} NAMENODE
+ {get(hostComponent, "HostRoles.component_name") ===
"ZKFC"
+ ? get(
+ find(slaveComponents, ["componentName", "ZKFC"]),
+ "displayName",
+ "ZKFC"
+ )
+ : getNameNodeLabel(hostComponent.haStatus)}
</div>
</Tooltip>
</Stack>
@@ -261,60 +303,6 @@ function HDFSSummary({ alerts }: { alerts: any }) {
</Col>
);
})}
- {!isFederated && find(slaveComponents, [
- "componentName",
- "ZKFC",
- ])?.hostComponents?.map((hostComponent: any) => {
- const icon =
- hostComponent.passiveState == "OFF"
- ? statusIconMap[lowerCase(hostComponent?.state)]
- : hostComponent?.passiveState
- ? statusIconMap["Maintenance"]
- : null;
- return (
- <Col md={2}>
- <Stack>
- <Stack direction="horizontal">
- <Tooltip
- message={hostComponent?.passiveState ?
TOOLTIP_MESSAGES.GENERAL.MAINTENANCE_MODE :
TOOLTIP_MESSAGES.GENERAL.COMPONENT_HEALTH}
- heading="Component Status"
- placement="top"
- >
- <FontAwesomeIcon
- icon={icon?.icon}
- className={`me-1 fw-bold fs-12 text-${icon?.color}`}
- />
- </Tooltip>
- <h3 className="text-dark mb-0">
- {startCase(hostComponent?.state?.toLowerCase()) ===
- "Installed"
- ? "Stopped"
- : startCase(hostComponent?.state?.toLowerCase())}
- </h3>
- </Stack>
-
- <Tooltip
- message={hostComponent.HostRoles.host_name}
- placement="top"
- >
- <div
- className="custom-link text-uppercase fs-12 text-nowrap
mt-2"
- onClick={() => {
- navigate(
-
`/main/hosts/${hostComponent.HostRoles.host_name}/summary`
- );
- }}
- >
- {
- find(slaveComponents, ["componentName", "ZKFC"])
- ?.displayName
- }
- </div>
- </Tooltip>
- </Stack>
- </Col>
- );
- })}
</Row>
<Row>
{slaveComponents.map((slaveComponent: any) => {
diff --git a/ambari-web/latest/src/screens/messages.ts
b/ambari-web/latest/src/screens/messages.ts
index 6203f8e8cc..dbf3a52501 100644
--- a/ambari-web/latest/src/screens/messages.ts
+++ b/ambari-web/latest/src/screens/messages.ts
@@ -2070,6 +2070,7 @@ const messages: any = {
'services.service.summary.nameNode':'NameNode Web UI',
'services.service.summary.nameNode.active':'Active NameNode',
'services.service.summary.nameNode.standby':'Standby NameNode',
+ 'services.service.summary.nameNode.observer':'Observer NameNode',
'services.service.summary.jobTracker':'JobTracker',
'services.service.summary.jobTrackerWebUI':'JobTracker Web UI',
'services.service.summary.hbaseMaster':'HBase Master Web UI',
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]