diff --git a/ambari-web/latest/src/screens/Hosts/HostSummary.tsx b/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
index 3959dd39494..a5dd2e71092 100644
--- a/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
+++ b/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
@@ -86,6 +86,7 @@ import {
startComponent,
stopComponent,
executeCustomCommand,
+ installClients,
} from "./actions";
import { AppContext } from "../../store/context";
import IHost from "../../models/host";
@@ -501,7 +502,23 @@ export default function HostsSummary({
{
- //TODO: Will be implemented in future PR
+ if (isInit(component)) {
+ const data = {
+ allComponents,
+ clusterComponents,
+ services,
+ // getKDCSessionState, TODO: will be added in future PR.
+ host: allHostModels[0],
+ };
+ setSelectedActionData(
+ [component],
+ "install",
+ false,
+ installClients,
+ data
+ );
+ setShowConfirmationModal(true);
+ }
}}
className={isInit(component) ? "" : "disabled-btn"}
>
@@ -516,7 +533,20 @@ export default function HostsSummary({
{
- //TODO: Will be implemented in future PR
+ const data = {
+ allComponents,
+ clusterComponents,
+ services,
+ // getKDCSessionState, TODO: will be added in future PR.
+ };
+ setSelectedActionData(
+ [component],
+ "re-install",
+ false,
+ installClients,
+ data
+ );
+ setShowConfirmationModal(true);
}}
>
Re-Install
diff --git a/ambari-web/latest/src/screens/Hosts/actions.tsx b/ambari-web/latest/src/screens/Hosts/actions.tsx
index 9fb82d38dcc..86f95ddfe8b 100644
--- a/ambari-web/latest/src/screens/Hosts/actions.tsx
+++ b/ambari-web/latest/src/screens/Hosts/actions.tsx
@@ -16,13 +16,14 @@
* limitations under the License.
*/
-import { capitalize, cloneDeep, get, set } from "lodash";
+import { capitalize, cloneDeep, get, set, uniq } from "lodash";
import { HostsApi } from "../../api/hostsApi";
import {
doDecommissionRegionServer,
doRecommissionAndStart,
getComponentDisplayName,
getComponentName,
+ installHostComponentCall,
parseNnCheckPointTime,
showHbaseActiveWarning,
showRegionServerWarning,
@@ -40,6 +41,7 @@ import {
import ConfirmationModal from "../../components/ConfirmationModal";
import { IHost } from "../../models/host";
import { t } from "i18next";
+import { CompatibleComponent, ComponentDependency } from "./utils/ComponentDependency";
export const sendComponentCommand = async (
component: IHostComponent,
@@ -436,7 +438,185 @@ export const toggleMaintenanceMode = async (component: IHostComponent) => {
data
);
};
-export const refreshConfigs = async (component: IHostComponent) => {
+function assert(condition: any, message: any) {
+ if (!condition) {
+ throw new Error(message);
+ }
+}
+
+const checkComponentDependencies = (
+ data: any,
+ component: IHostComponent,
+ opt: any
+) => {
+ var opt = opt || {};
+ opt.scope = opt.scope || "*";
+ var installedComponents;
+ switch (opt.scope) {
+ case "host":
+ assert(
+ "You should pass at least `hostName` or `installedComponents` to options.",
+ opt.hostName || opt.installedComponents
+ );
+ installedComponents = opt.installedComponents || [];
+ break;
+ default:
+ installedComponents = opt.installedComponents || [];
+ break;
+ }
+ return missingDependencies(data, component, installedComponents, opt)?.map(
+ (componentDependency: { chooseCompatible: (arg0: any) => any }) => {
+ return componentDependency.chooseCompatible(data.services);
+ }
+ );
+};
+
+const missingDependencies = (
+ data: any,
+ component: IHostComponent,
+ installedComponents: any,
+ opt: any
+) => {
+ opt = opt || {};
+ opt.scope = opt.scope || "*";
+ var dependencies: any = get(component, "dependencies", []);
+ dependencies =
+ opt.scope === "*"
+ ? dependencies
+ : dependencies.filter((item: any) => {
+ return item.Dependencies.scope === opt.scope;
+ });
+ if (dependencies.length === 0) return [];
+
+ var missingComponents = dependencies.filter((dependency: any) => {
+ return !installedComponents.some((installedComponent: IHostComponent) => {
+ const dependencyComponent = data.allComponents.find(
+ (host: IHostComponent) => {
+ return host.componentName === dependency.Dependencies.component_name;
+ }
+ );
+ return compatibleWith(
+ installedComponent,
+ dependencyComponent.componentName,
+ dependencyComponent.componentType
+ );
+ });
+ });
+ return missingComponents.map((missingComponent: any) => {
+ var componentFound = data.allComponents.find(
+ (hostComponent: IHostComponent) => {
+ return (
+ hostComponent.componentName ===
+ missingComponent.Dependencies.component_name
+ );
+ }
+ );
+ const compatibleComponents: CompatibleComponent[] = componentFound
+ ? [
+ {
+ componentName: componentFound.componentName,
+ serviceName: componentFound.serviceName,
+ },
+ ]
+ : [];
+
+ return new ComponentDependency(
+ missingComponent.Dependencies.component_name,
+ compatibleComponents
+ );
+ });
+};
+
+const compatibleWith = (component: any, compName: string, compType: string) => {
+ return (
+ component.componentName === compName ||
+ (component.componentType && component.componentType === compType)
+ );
+};
+
+export const installClients = async (
+ components: IHostComponent[],
+ data: any
+) => {
+ var clientsToInstall: IHostComponent[] = [],
+ clientsToAdd: IHostComponent[] = [],
+ missedComponents: any = [],
+ dependentComponents: any = [];
+
+ components.forEach((component) => {
+ if (["INIT", "INSTALL_FAILED"].includes(get(component, "workStatus"))) {
+ clientsToInstall.push(component);
+ } else if (typeof get(component, "workStatus") == "undefined") {
+ clientsToAdd.push(component);
+ }
+ });
+ clientsToAdd.forEach((component, _index, array) => {
+ var dependencies;
+ try {
+ dependencies = checkComponentDependencies(data, component, {
+ scope: "host",
+ installedComponents: get(data, "host.hostComponents", []),
+ });
+ } catch (error) {
+ dependencies = array.map((component) => {
+ get(component, "componentName").includes(getComponentName(component));
+ });
+ }
+ if (dependencies && dependencies.length > 0) {
+ missedComponents.push(dependencies);
+ dependentComponents.push(getComponentDisplayName(component));
+ }
+ });
+
+ missedComponents = uniq(missedComponents);
+ if (missedComponents && missedComponents.length) {
+ var popupMessage = t(
+ "host.host.addComponent.popup.clients.dependedComponents.body"
+ )
+ .replace("{0}", dependentComponents.join(", "))
+ .replace(
+ "{1}",
+ missedComponents
+ .map((component: IHostComponent) => {
+ getComponentDisplayName(component);
+ })
+ .join(", ")
+ );
+ showAlertModal(
+ t("host.host.addComponent.popup.dependedComponents.header"),
+ popupMessage
+ );
+ } else {
+ await data.getKDCSessionState(async () => {
+ var sendInstallCommand = function () {
+ if (clientsToInstall && clientsToInstall.length) {
+ sendComponentCommand(
+ clientsToInstall[0],
+ t("host.host.details.installClients"),
+ "INSTALLED"
+ );
+ }
+ };
+
+ if (clientsToAdd && clientsToAdd.length) {
+ // var message = clientsToAdd.map((component: IHostComponent) => {
+ // return getComponentDisplayName(component)
+ // }).join(", ");
+ // var componentObject = Object.create({
+ // displayName: message
+ // });
+
+ // popup for add component modal.
+ sendInstallCommand();
+ clientsToAdd.forEach((component: IHostComponent) => {
+ installHostComponentCall(get(component, "hostName"), component, data, data?.setAllHostModels);
+ });
+ } else {
+ sendInstallCommand();
+ }
+ });
+ }
+};export const refreshConfigs = async (component: IHostComponent) => {
const message = t("rollingrestart.context.ClientOnSelectedHost")
.replace("{0}", getComponentDisplayName(component))
.replace("{1}", get(component, "hostName"));
diff --git a/ambari-web/latest/src/screens/Hosts/utils.tsx b/ambari-web/latest/src/screens/Hosts/utils.tsx
index c9170705dfd..26a70dc26c7 100644
--- a/ambari-web/latest/src/screens/Hosts/utils.tsx
+++ b/ambari-web/latest/src/screens/Hosts/utils.tsx
@@ -38,6 +38,8 @@ import {
//TODO: Uncomment the below import and its usage once BackgroundOperations component is available
// import BackgroundOperations from "../BackgroundOperations";
import { IHost } from "../../models/host.ts";
+import { HostsApi } from "../../api/hostsApi.ts";
+import { defaultSuccessCallbackWithoutReload } from "./batchUtils.tsx";
export const hostComponentCustomCommandMap = {
REFRESHQUEUES: {
@@ -184,6 +186,50 @@ export const addDeleteComponentsMap: any = {
},
};
+const serviceComponentMetrics = [
+ "host_components/metrics/jvm/memHeapUsedM",
+ "host_components/metrics/jvm/HeapMemoryMax",
+ "host_components/metrics/jvm/HeapMemoryUsed",
+ "host_components/metrics/jvm/memHeapCommittedM",
+ "host_components/metrics/mapred/jobtracker/trackers_decommissioned",
+ "host_components/metrics/cpu/cpu_wio",
+ "host_components/metrics/rpc/client/RpcQueueTime_avg_time",
+ "host_components/metrics/dfs/FSNamesystem/*",
+ "host_components/metrics/dfs/namenode/Version",
+ "host_components/metrics/dfs/namenode/LiveNodes",
+ "host_components/metrics/dfs/namenode/DeadNodes",
+ "host_components/metrics/dfs/namenode/DecomNodes",
+ "host_components/metrics/dfs/namenode/TotalFiles",
+ "host_components/metrics/dfs/namenode/UpgradeFinalized",
+ "host_components/metrics/dfs/namenode/Safemode",
+ "host_components/metrics/runtime/StartTime",
+];
+
+const serviceSpecificParams = {
+ FLUME: "host_components/processes/HostComponentProcess",
+ YARN:
+ "host_components/metrics/yarn/Queue," +
+ "host_components/metrics/yarn/ClusterMetrics/NumActiveNMs," +
+ "host_components/metrics/yarn/ClusterMetrics/NumLostNMs," +
+ "host_components/metrics/yarn/ClusterMetrics/NumUnhealthyNMs," +
+ "host_components/metrics/yarn/ClusterMetrics/NumRebootedNMs," +
+ "host_components/metrics/yarn/ClusterMetrics/NumDecommissionedNMs",
+ HBASE:
+ "host_components/metrics/hbase/master/IsActiveMaster," +
+ "host_components/metrics/hbase/master/MasterStartTime," +
+ "host_components/metrics/hbase/master/MasterActiveTime," +
+ "host_components/metrics/hbase/master/AverageLoad," +
+ "host_components/metrics/master/AssignmentManager/ritCount",
+ STORM:
+ "metrics/api/v1/cluster/summary,metrics/api/v1/topology/summary,metrics/api/v1/nimbus/summary",
+ HDFS: "host_components/metrics/dfs/namenode/ClusterId",
+ SSM: "host_components/processes/HostComponentProcess",
+};
+
+var requestsRunningStatus = {
+ updateServiceMetric: false,
+};
+
export const populateHostComponentModels = (hostComponent: any) => {
const hostComponentModel = new HostComponent({} as IHostComponent);
(
@@ -1177,4 +1223,261 @@ export const validateInteger = (
export const getClusterUpgradeStatusForHost = (upgradeState: string) => {
return upgradeState === "IN_PROGRESS" || upgradeState.includes("HOLDING");
+};
+
+
+export const installHostComponentCall = async (
+ hostName: any,
+ component: IHostComponent,
+ data: any,
+ setAllHostModels?: (
+ data: IHost[] | ((prevModels: IHost[]) => IHost[])
+ ) => void
+) => {
+ const componentName = getComponentName(component);
+ const displayName = getComponentDisplayName(component);
+ const clusterName = get(component, "clusterName", "");
+
+ // Ensure the component has the correct hostname before proceeding
+ const updatedComponent = { ...component, hostName: hostName };
+
+ try {
+ updateAndCreateServiceComponent(componentName, data, clusterName);
+ const payload = {
+ RequestInfo: {
+ context:
+ translate("requestInfo.installHostComponent") + " " + displayName,
+ },
+ Body: {
+ host_components: [
+ {
+ HostRoles: {
+ component_name: componentName,
+ },
+ },
+ ],
+ },
+ };
+ const res = await HostsApi.hostComponentAddNewComponent(
+ clusterName,
+ hostName,
+ payload
+ );
+ addNewComponentSuccessCallback(res, {}, { component: updatedComponent }, setAllHostModels);
+ } catch (error) {
+ console.log("error in updating and creating service component", error);
+ }
+};
+
+const addNewComponentSuccessCallback = async (
+ _data: any,
+ _opt: any,
+ params: any,
+ setAllHostModels?: (
+ data: IHost[] | ((prevModels: IHost[]) => IHost[])
+ ) => void
+) => {
+ const component = cloneDeep(params.component);
+ const hostName = get(component, "hostName");
+ const componentName = getComponentName(component);
+ const clusterName = get(component, "clusterName");
+ const serviceName = get(component, "serviceName");
+ const displayName = get(component, "displayName");
+ const context =
+ translate("requestInfo.installNewHostComponent") + " " + displayName;
+ const urlParams = "HostRoles/state=INIT";
+ const HostRoles = {
+ state: "INSTALLED",
+ };
+
+ const payload = {
+ RequestInfo: {
+ context: context,
+ operation_level: {
+ level: "HOST_COMPONENT",
+ cluster_name: clusterName,
+ host_name: hostName,
+ service_name: serviceName || null,
+ },
+ },
+ Body: {
+ HostRoles: HostRoles,
+ },
+ };
+ var response: any = await HostsApi.commonHostComponentUpdate(
+ clusterName,
+ hostName,
+ componentName,
+ urlParams,
+ payload
+ );
+ if (typeof response === "string") {
+ response = JSON.parse(response);
+ }
+ if (!response || !response.Requests || !response.Requests.id) {
+ return false;
+ }
+
+ if (setAllHostModels) {
+ setAllHostModels((prevModels: IHost[]) => {
+ return prevModels.map((host: IHost) => {
+ if (get(host, "hostName") === hostName) {
+ const hostModel = cloneDeep(host);
+ const hostComponents = get(
+ hostModel,
+ "hostComponents",
+ [] as IHostComponent[]
+ );
+ hostComponents.push(component);
+ set(
+ hostModel,
+ "hostComponents",
+ sortBasedOnMasterSlave(hostComponents, "componentCategory")
+ );
+ return hostModel;
+ }
+ return host;
+ });
+ });
+ }
+
+ const requestId = get(response, "Requests.id", -1);
+ defaultSuccessCallbackWithoutReload(requestId);
+};
+
+const updateAndCreateServiceComponent = async(
+ componentName: string,
+ data: any,
+ clusterName: string
+) => {
+ var url =
+ "/components/?fields=ServiceComponentInfo/service_name," +
+ "ServiceComponentInfo/category,ServiceComponentInfo/installed_count,ServiceComponentInfo/started_count,ServiceComponentInfo/init_count,ServiceComponentInfo/install_failed_count,ServiceComponentInfo/unknown_count,ServiceComponentInfo/total_count,ServiceComponentInfo/display_name,host_components/HostRoles/host_name&minimal_response=true";
+ try {
+ await HostsApi.updateComponentsState(clusterName, url);
+ updateServiceMetric(
+ componentName,
+ data,
+ createServiceComponent,
+ clusterName
+ );
+ } catch (error) {
+ console.log("error in updating and creating service component", error);
+ }
+};
+
+const getConditionalFields = (data: any) => {
+ let conditionalFields = serviceComponentMetrics.slice(0);
+ let serviceParams = cloneDeep(serviceSpecificParams);
+ set(serviceParams, "ONEFS", "metrics/*,");
+
+ data.services.forEach((service: any) => {
+ const urlParams = get(serviceParams, service.ServiceInfo.service_name);
+ if (urlParams) {
+ conditionalFields.push(urlParams);
+ }
+ });
+
+ return conditionalFields;
+};
+
+const isComponentPresent = (
+ componentName: string,
+ allServiceComponents: any
+) => {
+ return allServiceComponents.items?.some((item: any) => {
+ return get(item, "ServiceComponentInfo.component_name") === componentName;
+ });
+};
+
+const updateServiceMetric = async (
+ componentName: string,
+ data: any,
+ callback: Function,
+ clusterName: string
+) => {
+ const isATSPresent = isComponentPresent(
+ "APP_TIMELINE_SERVER",
+ data.clusterComponents
+ );
+ const isHaEnabled = false;
+
+ const conditionalFields = getConditionalFields(data);
+ const conditionalFieldsString =
+ conditionalFields.length > 0 ? "," + conditionalFields.join(",") : "";
+ const isFlumeInstalled = data.services.filter(
+ (service: any) => service.ServiceInfo.service_name === "FLUME"
+ );
+ const isATSInstalled =
+ data.services.filter(
+ (service: any) => service.ServiceInfo.service_name === "YARN"
+ ) && isATSPresent;
+ const flumeHandlerParam = isFlumeInstalled
+ ? "ServiceComponentInfo/component_name=FLUME_HANDLER|"
+ : "";
+ const atsHandlerParam = isATSInstalled
+ ? "ServiceComponentInfo/component_name=APP_TIMELINE_SERVER|"
+ : "";
+ const haComponents = isHaEnabled
+ ? "ServiceComponentInfo/component_name=JOURNALNODE|ServiceComponentInfo/component_name=ZKFC|"
+ : "";
+ const url =
+ "/components/?" +
+ flumeHandlerParam +
+ atsHandlerParam +
+ haComponents +
+ "ServiceComponentInfo/category.in(MASTER,CLIENT)&fields=" +
+ "ServiceComponentInfo/service_name," +
+ "host_components/HostRoles/display_name," +
+ "host_components/HostRoles/host_name," +
+ "host_components/HostRoles/public_host_name," +
+ "host_components/HostRoles/state," +
+ "host_components/HostRoles/maintenance_state," +
+ "host_components/HostRoles/stale_configs," +
+ "host_components/HostRoles/ha_state," +
+ "host_components/HostRoles/desired_admin_state," +
+ conditionalFieldsString +
+ "&minimal_response=true";
+
+ if (!requestsRunningStatus.updateServiceMetric) {
+ requestsRunningStatus.updateServiceMetric = true;
+ try {
+ await HostsApi.updateServiceMetric(clusterName, url);
+ requestsRunningStatus.updateServiceMetric = false;
+ callback(componentName, data, clusterName);
+ } catch (error) {
+ console.log("error in updating service metric", error);
+ }
+ } else {
+ callback(componentName, data, clusterName);
+ }
+};
+
+const createServiceComponent = (
+ componentName: string,
+ data: any,
+ clusterName: string
+) => {
+ const allServiceComponents = data.clusterComponents;
+
+ if (
+ allServiceComponents &&
+ isComponentPresent(componentName, allServiceComponents)
+ ) {
+ return;
+ } else {
+ const payload = {
+ components: [
+ {
+ ServiceComponentInfo: {
+ component_name: componentName,
+ },
+ },
+ ],
+ };
+ const serviceName = allServiceComponents.items.find((item: any) => {
+ return item.ServiceComponentInfo.component_name === componentName;
+ }).ServiceComponentInfo.service_name;
+ HostsApi.commonCreateComponent(clusterName, serviceName, payload);
+ }
};
\ No newline at end of file
diff --git a/ambari-web/latest/src/screens/Hosts/utils/ComponentDependency.ts b/ambari-web/latest/src/screens/Hosts/utils/ComponentDependency.ts
new file mode 100644
index 00000000000..d23400752c8
--- /dev/null
+++ b/ambari-web/latest/src/screens/Hosts/utils/ComponentDependency.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+export interface CompatibleComponent {
+ componentName: string;
+ serviceName: string;
+}
+
+export class ComponentDependency {
+ componentName: string;
+ compatibleComponents: CompatibleComponent[];
+
+ constructor(componentName: string, compatibleComponents: CompatibleComponent[] = []) {
+ this.componentName = componentName;
+ this.compatibleComponents = compatibleComponents;
+ }
+
+ /**
+ * Find the first compatible component which belongs to a service that is installed
+ */
+ chooseCompatible(services: any) {
+ const compatibleComponent = this.compatibleComponents.find(component => {
+ return services.some((service: any) => service.ServiceInfo.service_name === component.serviceName);
+ });
+
+ return (compatibleComponent || this.compatibleComponents[0]).componentName;
+ }
+}
\ No newline at end of file