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