diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json index b28c510363a4..4dc4879d29d5 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json @@ -94,7 +94,7 @@ }, { "hostname": "localhost1.storage.enterprise.com", - "uuid": "b5907812-a5f2-11ea-bb37-0242ac130011", + "uuid": "4712ba3d-4bb2-477a-9211-d9b50c013055", "state": "HEALTHY", "opState": "DECOMMISSIONING", "lastHeartbeat": 1574724876059, @@ -162,7 +162,7 @@ }, { "hostname": "localhost3.storage.enterprise.com", - "uuid": "b5907812-a5f2-11ea-bb37-0242ac130013", + "uuid": "4712ba3d-4bb2-477a-9211-d9b50c013056", "state": "HEALTHY", "opState": "ENTERING_MAINTENANCE", "lastHeartbeat": 1574724876059, @@ -6738,6 +6738,201 @@ ], "status": "OK" }, + "decommissioninfo": { + "DatanodesDecommissionInfo":[ + { + "datanodeDetails": { + "level": 3, + "parent": null, + "cost": 0, + "uuid": "4712ba3d-4bb2-477a-9211-d9b50c013055", + "uuidString": "4712ba3d-4bb2-477a-9211-d9b50c013055", + "ipAddress": "172.18.0.13", + "hostName": "ozone-ha-datanode-6.ozone-ha_default", + + "certSerialId": null, + "version": null, + "setupTime": 0, + "revision": null, + "buildDate": null, + "persistedOpState": "IN_SERVICE", + "persistedOpStateExpiryEpochSec": 0, + "initialVersion": 0, + "currentVersion": 1, + "decommissioned": false, + "maintenance": false, + "ipAddressAsByteString": { + "string": "172.18.0.13", + "bytes": { + "validUtf8": true, + "empty": false + } + }, + "hostNameAsByteString": { + "string": "ozone-ha-datanode-6.ozone-ha_default", + "bytes": { + "validUtf8": true, + "empty": false + } + }, + "networkName": "4712ba3d-4bb2-477a-9211-d9b50c013055", + "networkLocation": "/default-rack", + "networkFullPath": "/default-rack/4712ba3d-4bb2-477a-9211-d9b50c013055", + "numOfLeaves": 1, + "networkNameAsByteString": { + "string": "4712ba3d-4bb2-477a-9211-d9b50c013055", + "bytes": { + "validUtf8": true, + "empty": false + } + }, + "networkLocationAsByteString": { + "string": "/default-rack", + "bytes": { + "validUtf8": true, + "empty": false + } + } + }, + "metrics": null, + "containers": {} + }, + { + "datanodeDetails": { + "level": 3, + "parent": null, + "cost": 0, + "uuid": "4712ba3d-4bb2-477a-9211-d9b50c013056", + "uuidString": "4712ba3d-4bb2-477a-9211-d9b50c013056", + "ipAddress": "172.18.0.13", + "hostName": "ozone-ha-datanode-6.ozone-ha_default", + "certSerialId": null, + "version": null, + "setupTime": 0, + "revision": null, + "buildDate": null, + "persistedOpState": "IN_SERVICE", + "persistedOpStateExpiryEpochSec": 0, + "initialVersion": 0, + "currentVersion": 1, + "decommissioned": false, + "maintenance": false, + "networkName": "4712ba3d-4bb2-477a-9211-d9b50c013055", + "networkLocation": "/default-rack", + "networkFullPath": "/default-rack/4712ba3d-4bb2-477a-9211-d9b50c013055", + "numOfLeaves": 1 + }, + "metrics": null, + "containers": {} + } + ] + }, + "DatanodesDecommissionInfo": { + "DatanodesDecommissionInfo": [ + { + "datanodeDetails": { + "level": 3, + "parent": null, + "cost": 0, + "uuid": "adc72ef9-0850-49d7-935d-e5a4e0d5be3c", + "uuidString": "adc72ef9-0850-49d7-935d-e5a4e0d5be3c", + "ipAddress": "172.18.0.10", + "hostName": "ozone-ha-datanode-2.ozone-ha_default", + "ports": [ + { + "name": "HTTP", + "value": 9882 + }, + { + "name": "CLIENT_RPC", + "value": 19864 + }, + { + "name": "REPLICATION", + "value": 9886 + }, + { + "name": "RATIS", + "value": 9858 + }, + { + "name": "RATIS_ADMIN", + "value": 9857 + }, + { + "name": "RATIS_SERVER", + "value": 9856 + }, + { + "name": "RATIS_DATASTREAM", + "value": 9855 + }, + { + "name": "STANDALONE", + "value": 9859 + } + ], + "certSerialId": null, + "version": null, + "setupTime": 0, + "revision": null, + "buildDate": null, + "persistedOpState": "DECOMMISSIONING", + "persistedOpStateExpiryEpochSec": 0, + "initialVersion": 0, + "currentVersion": 1, + "decommissioned": true, + "maintenance": false, + "ipAddressAsByteString": { + "string": "172.18.0.10", + "bytes": { + "validUtf8": true, + "empty": false + } + }, + "hostNameAsByteString": { + "string": "ozone-ha-datanode-2.ozone-ha_default", + "bytes": { + "validUtf8": true, + "empty": false + } + }, + "networkName": "adc72ef9-0850-49d7-935d-e5a4e0d5be3c", + "networkLocation": "/default-rack", + "networkFullPath": "/default-rack/adc72ef9-0850-49d7-935d-e5a4e0d5be3c", + "numOfLeaves": 1, + "networkNameAsByteString": { + "string": "adc72ef9-0850-49d7-935d-e5a4e0d5be3c", + "bytes": { + "validUtf8": true, + "empty": false + } + }, + "networkLocationAsByteString": { + "string": "/default-rack", + "bytes": { + "validUtf8": true, + "empty": false + } + } + }, + "metrics": { + "decommissionStartTime": "23/05/2024 07:28:35 UTC", + "numOfUnclosedPipelines": 2, + "numOfUnderReplicatedContainers": 0.0, + "numOfUnclosedContainers": 0.0 + }, + "containers": { + "UnderReplicated": [], + "UnClosed": [ + "#6", + "#10", + "#17" + ] + } + } + ] + }, "datanodesRemove": { "selectedRowKeys": [ "b5907812-a5f2-11ea-bb37-0242ac130011" diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json index b2136d074b07..b4481c2854fc 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json @@ -50,5 +50,7 @@ "/containers/mismatch/deleted?limit=*": "/deleted", "/keys/deletePending/dirs?limit=*": "/dirdeletePending", + "/datanodes/decommission/info": "/decommissioninfo", + "/datanodes/decommission/info/datanode?uuid=*": "/DatanodesDecommissionInfo", "/datanodes/remove": "/datanodesRemove" } \ No newline at end of file diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/overviewCard/overviewCard.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/overviewCard/overviewCard.tsx index 78ba56e54891..c8e19db14c8c 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/overviewCard/overviewCard.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/overviewCard/overviewCard.tsx @@ -27,7 +27,8 @@ import { FolderOpenOutlined, FileTextOutlined, QuestionCircleOutlined, - DeleteOutlined + DeleteOutlined, + HourglassOutlined } from '@ant-design/icons'; import { RouteComponentProps } from 'react-router'; import { withRouter, Link } from 'react-router-dom'; @@ -70,7 +71,8 @@ const IconSelector = ({ iconType, ...extras }: { iconType: string }) => { 'inbox': , 'folder-open': , 'file-text': , - 'delete': + 'delete': , + 'hourglass': } const selectIcon = (type: string) => { diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx index c4c9bf129c8d..47fe41dd7b31 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx @@ -48,10 +48,10 @@ import { ColumnSearch } from '@/utils/columnSearch'; import { ReplicationIcon } from '@/utils/themeIcons'; import { AutoReloadHelper } from '@/utils/autoReloadHelper'; import { AxiosGetHelper, AxiosPutHelper } from '@/utils/axiosRequestHelper'; +import DecommissionSummary from './decommissionSummary'; import './datanodes.less'; - interface IDatanodeResponse { hostname: string; state: DatanodeState; @@ -190,7 +190,6 @@ const COLUMNS = [ render: (text: DatanodeOpState) => renderDatanodeOpState(text), sorter: (a: IDatanode, b: IDatanode) => a.opState.localeCompare(b.opState) }, - { title: 'Uuid', dataIndex: 'uuid', @@ -198,7 +197,14 @@ const COLUMNS = [ isVisible: true, isSearchable: true, sorter: (a: IDatanode, b: IDatanode) => a.uuid.localeCompare(b.uuid), - defaultSortOrder: 'ascend' as const + defaultSortOrder: 'ascend' as const, + render: (uuid: IDatanode, record: IDatanode) => { + return ( + //1. Compare Decommission Api's UUID with all UUID in table and show Decommission Summary + (decommissionUuids && decommissionUuids.includes(record.uuid) && record.opState !== 'DECOMMISSIONED') ? + : {uuid} + ); + } }, { title: 'Storage Capacity', @@ -356,6 +362,17 @@ const defaultColumns: IOption[] = COLUMNS.map(column => ({ })); let cancelSignal: AbortController; +let cancelDecommissionSignal: AbortController; +let decommissionUuids: string | string[] =[]; +const COLUMN_UPDATE_DECOMMISSIONING = 'DECOMMISSIONING'; + +type DatanodeDetails = { + uuid: string; +} + +type DatanodeDecomissionInfo = { +datanodeDetails: DatanodeDetails +} export class Datanodes extends React.Component, IDatanodesState> { autoReload: AutoReloadHelper; @@ -389,24 +406,34 @@ export class Datanodes extends React.Component, IDatanode return selectedColumns; }; - _loadData = () => { - this.setState(prevState => ({ - loading: true, - selectedColumns: this._getSelectedColumns(prevState.selectedColumns) - })); - - const { request, controller } = AxiosGetHelper('/api/v1/datanodes', cancelSignal); - cancelSignal = controller; - request.then(response => { - const datanodesResponse: IDatanodesResponse = response.data; + _loadData = async () => { + // Need to call decommission API on each interval to get updated status before datanode API to compare UUID's + // update Operation State Column in table Manually before rendering + try { + let decomissionResponse = await this._loadDecommisionAPI(); + decommissionUuids = decomissionResponse.data?.DatanodesDecommissionInfo?.map((item: DatanodeDecomissionInfo) => item.datanodeDetails.uuid); + } + catch (error: any) + { + this.setState({ + loading: false + }); + decommissionUuids = []; + showDataFetchError(error.toString()); + } + try { + // Call Datanode API Synchronously after completing Decommission API to render Operation Status Column + let datanodeApiResponse = await this._loadDataNodeAPI(); + const datanodesResponse: IDatanodesResponse = datanodeApiResponse.data; const totalCount = datanodesResponse.totalCount; const datanodes: IDatanodeResponse[] = datanodesResponse.datanodes; const dataSource: IDatanode[] = datanodes && datanodes.map(datanode => { + let decommissionCondition = decommissionUuids && decommissionUuids.includes(datanode.uuid) && datanode.opState !== 'DECOMMISSIONED'; return { hostname: datanode.hostname, uuid: datanode.uuid, state: datanode.state, - opState: datanode.opState, + opState: decommissionCondition ? COLUMN_UPDATE_DECOMMISSIONING : datanode.opState, lastHeartbeat: datanode.lastHeartbeat, storageUsed: datanode.storageReport.used, storageTotal: datanode.storageReport.capacity, @@ -423,19 +450,40 @@ export class Datanodes extends React.Component, IDatanode networkLocation: datanode.networkLocation }; }); - this.setState({ loading: false, dataSource, totalCount, lastUpdated: Number(moment()) }); - }).catch(error => { + } + catch (error: any) { this.setState({ loading: false }); + decommissionUuids = []; showDataFetchError(error.toString()); + } + }; + + _loadDecommisionAPI = async () => { + this.setState({ + loading: true }); + decommissionUuids = []; + const { request, controller } = await AxiosGetHelper('/api/v1/datanodes/decommission/info', cancelDecommissionSignal); + cancelDecommissionSignal = controller; + return request + }; + + _loadDataNodeAPI = async () => { + this.setState(prevState => ({ + loading: true, + selectedColumns: this._getSelectedColumns(prevState.selectedColumns) + })); + const { request, controller } = await AxiosGetHelper('/api/v1/datanodes', cancelSignal); + cancelSignal = controller; + return request; }; removeDatanode = async (selectedRowKeys: any) => { @@ -462,6 +510,7 @@ export class Datanodes extends React.Component, IDatanode componentWillUnmount(): void { this.autoReload.stopPolling(); cancelSignal && cancelSignal.abort(); + cancelDecommissionSignal && cancelDecommissionSignal.abort(); } onShowSizeChange = (current: number, pageSize: number) => { diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/decommissionSummary.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/decommissionSummary.tsx new file mode 100644 index 000000000000..28527c0816e0 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/decommissionSummary.tsx @@ -0,0 +1,121 @@ +/* + * 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. + */ + +import React from 'react'; +import { Descriptions, Popover, Tooltip } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { withRouter } from 'react-router-dom'; +import { RouteComponentProps } from 'react-router'; +import axios from 'axios'; +import { showDataFetchError } from '@/utils/common'; + +interface IDecommissionSummaryProps extends RouteComponentProps { + uuid: string; + isLoading: boolean; + summaryData: string[]; + record: string[]; +} + +class DecommissionSummary extends React.Component { + constructor(props = {}) { + super(props); + this.state = { + uuid: this.props.uuid, + record: this.props.record, + isLoading: false, + summaryData: [] + }; + } + + componentDidMount(): void { + this.setState({ + isLoading: true + }); + if (this.state?.record && this.state?.summaryData) { + this.fetchDecommissionSummary(this.state.uuid); + } + } + + fetchDecommissionSummary = async (selectedUuid: any) => { + try { + const datanodeEndpoint = `/api/v1/datanodes/decommission/info/datanode?uuid=${selectedUuid}`; + let infoDatanodeResponse = await axios.get(datanodeEndpoint); + let DatanodesDecommissionInfo = []; + DatanodesDecommissionInfo = infoDatanodeResponse?.data?.DatanodesDecommissionInfo[0]; + this.setState({ + loading: false, + summaryData: DatanodesDecommissionInfo + }); + } + catch (error) { + this.setState({ + loading: false, + summaryData: [] + }); + showDataFetchError(error.toString()); + } + }; + + render() { + const { summaryData, uuid } = this.state; + let content; + + if ( summaryData && summaryData.length !== 0 && summaryData !== null && summaryData !== undefined && summaryData.datanodeDetails) { + const { datanodeDetails, containers, metrics } = summaryData; + content = ( + + {datanodeDetails.uuid} + ({datanodeDetails.networkLocation}/{datanodeDetails.ipAddress}/{datanodeDetails.hostname}) + {metrics !== null && metrics !== undefined && Object.keys(metrics).length !== 0 && + <> + {{metrics.decommissionStartTime}} + {{metrics.numOfUnclosedPipelines}} + {{metrics.numOfUnclosedContainers}} + {{metrics.numOfUnderReplicatedContainers}} + + } + { + containers && Object.keys(containers).length !== 0 && + <> + {containers.UnderReplicated && containers.UnderReplicated.length > 0 && {containers.UnderReplicated}} + {containers.UnClosed && containers.UnClosed.length > 0 && {containers.UnClosed}} + + } + + ); + } + // Need to check summarydata is not empty + return ( + <> + { (summaryData && summaryData.length !== 0) ? + <> + + + + +  {uuid} + + : uuid + } + + + ); + } +} + +export default withRouter(DecommissionSummary); diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/overview/overview.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/overview/overview.tsx index bb2e5e02f2c6..a83d12408b3a 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/overview/overview.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/overview/overview.tsx @@ -73,6 +73,7 @@ interface IOverviewState { deletePendingSummarytotalUnrepSize: number | string; deletePendingSummarytotalRepSize: number | string; deletePendingSummarytotalDeletedKeys: number | string; + decommissionInfoCount: number | string; scmServiceId: string; omServiceId: string; } @@ -111,7 +112,8 @@ export class Overview extends React.Component, IOverviewS openSummarytotalOpenKeys: 0, deletePendingSummarytotalUnrepSize: 0, deletePendingSummarytotalRepSize: 0, - deletePendingSummarytotalDeletedKeys: 0 + deletePendingSummarytotalDeletedKeys: 0, + decommissionInfoCount: 0 }; this.autoReload = new AutoReloadHelper(this._loadData); } @@ -131,7 +133,8 @@ export class Overview extends React.Component, IOverviewS '/api/v1/clusterState', '/api/v1/task/status', '/api/v1/keys/open/summary', - '/api/v1/keys/deletePending/summary' + '/api/v1/keys/deletePending/summary', + '/api/v1/datanodes/decommission/info' ], cancelOverviewSignal); cancelOverviewSignal = controller; @@ -139,13 +142,15 @@ export class Overview extends React.Component, IOverviewS clusterStateResponse: Awaited>, taskstatusResponse: Awaited>, openResponse: Awaited>, - deletePendingResponse: Awaited> + deletePendingResponse: Awaited>, + decommissionResponse: Awaited> ) => { let responseError = [ clusterStateResponse, taskstatusResponse, openResponse, - deletePendingResponse + deletePendingResponse, + decommissionResponse ].filter((resp) => resp.status === 'rejected'); if (responseError.length !== 0) { @@ -186,8 +191,8 @@ export class Overview extends React.Component, IOverviewS taskName: 'N/A', lastUpdatedTimestamp: 0, lastUpdatedSeqNumber: 0 }]; const missingContainersCount = clusterState.missingContainers; - const omDBDeltaObject = taskStatus && taskStatus.find((item: any) => item.taskName === 'OmDeltaRequest'); - const omDBFullObject = taskStatus && taskStatus.find((item: any) => item.taskName === 'OmSnapshotRequest'); + const omDBDeltaObject = taskStatus && taskStatus.find((item:any) => item.taskName === 'OmDeltaRequest'); + const omDBFullObject = taskStatus && taskStatus.find((item:any) => item.taskName === 'OmSnapshotRequest'); this.setState({ loading: false, @@ -212,6 +217,7 @@ export class Overview extends React.Component, IOverviewS deletePendingSummarytotalUnrepSize: deletePendingResponse.value?.data?.totalUnreplicatedDataSize, deletePendingSummarytotalRepSize: deletePendingResponse.value?.data?.totalReplicatedDataSize, deletePendingSummarytotalDeletedKeys: deletePendingResponse.value?.data?.totalDeletedKeys, + decommissionInfoCount: decommissionResponse.value?.data?.DatanodesDecommissionInfo.length, scmServiceId: clusterState.scmServiceId, omServiceId: clusterState.omServiceId }); @@ -267,10 +273,12 @@ export class Overview extends React.Component, IOverviewS const { loading, datanodes, pipelines, storageReport, containers, volumes, buckets, openSummarytotalUnrepSize, openSummarytotalRepSize, openSummarytotalOpenKeys, deletePendingSummarytotalUnrepSize, deletePendingSummarytotalRepSize, deletePendingSummarytotalDeletedKeys, keys, missingContainersCount, lastRefreshed, lastUpdatedOMDBDelta, lastUpdatedOMDBFull, - omStatus, openContainers, deletedContainers, scmServiceId, omServiceId } = this.state; + omStatus, openContainers, deletedContainers, scmServiceId, omServiceId, decommissionInfoCount } = this.state; let openKeysError: boolean = false; let pendingDeleteKeysError: boolean = false; + let decommissionInfoError: boolean = false; + if ([ openSummarytotalRepSize, openSummarytotalUnrepSize, @@ -280,6 +288,12 @@ export class Overview extends React.Component, IOverviewS openKeysError = true; } + if ([decommissionInfoCount].some( + (data) => data === undefined + )) { + decommissionInfoError = true; + } + if ([ deletePendingSummarytotalRepSize, deletePendingSummarytotalUnrepSize, @@ -427,6 +441,10 @@ export class Overview extends React.Component, IOverviewS } + + + );