Skip to content

Commit 4be4ca7

Browse files
Provide option to enable DR support
Signed-off-by: Timothy Asir Jeyasingh <[email protected]>
1 parent d768d0b commit 4be4ca7

File tree

7 files changed

+247
-1
lines changed

7 files changed

+247
-1
lines changed

locales/en/plugin__odf-console.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,10 @@
441441
"Only showing PVCs that are being mounted on an active pod": "Only showing PVCs that are being mounted on an active pod",
442442
"This card shows the requested capacity for different Kubernetes resources. The figures shown represent the usable storage, meaning that data replication is not taken into consideration.": "This card shows the requested capacity for different Kubernetes resources. The figures shown represent the usable storage, meaning that data replication is not taken into consideration.",
443443
"Internal": "Internal",
444+
"Disaster recovery optimisation": "Disaster recovery optimisation",
445+
"Optimize": "Optimize",
446+
"Configure cluster for Regional-DR?": "Configure cluster for Regional-DR?",
447+
"Optimize the cluster for a Regional-DR setup by migrating OSDs. Migration may take some time depending on several factors. To learn more about OSD migration best practices and its consequences, refer to the documentation.": "Optimize the cluster for a Regional-DR setup by migrating OSDs. Migration may take some time depending on several factors. To learn more about OSD migration best practices and its consequences, refer to the documentation.",
444448
"Raw capacity is the absolute total disk space available to the array subsystem.": "Raw capacity is the absolute total disk space available to the array subsystem.",
445449
"Troubleshoot": "Troubleshoot",
446450
"Active health checks": "Active health checks",

packages/ocs/dashboards/persistent-external/details-card.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const DetailsCard: React.FC = () => {
6060
: t('Disabled');
6161

6262
const ocsName = getName(resourcesObj['ocs'].data?.[0]);
63+
//const disasterRecoveryStatus = getDRsetupStatus();
6364

6465
const [csv, csvLoaded, csvError] = useFetchCsv({
6566
specName: !isODF ? OCS_OPERATOR : ODF_OPERATOR,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
import { screen, render, fireEvent, waitFor } from '@testing-library/react';
3+
import '@testing-library/jest-dom/extend-expect';
4+
import * as migrationStatus from './constants/osd-migration';
5+
import OptimizeModal from './OptimizeModal';
6+
7+
jest.mock('@odf/shared/selectors', () => ({
8+
getName: jest.fn().mockReturnValue('test'),
9+
}));
10+
11+
// Mocking getMigrationStatus
12+
jest.mock('./constants/osd-migration');
13+
14+
jest.mock('@odf/shared/hooks/useK8sList', () => ({
15+
__esModule: true,
16+
useK8sList: () => [
17+
[
18+
{
19+
metadata: {
20+
name: 'test',
21+
},
22+
},
23+
],
24+
true,
25+
undefined,
26+
],
27+
}));
28+
29+
describe('OptimizeModal Component', () => {
30+
test('renders without errors', async () => {
31+
// Mock getMigrationStatus to return 'Completed' for this test
32+
migrationStatus.getMigrationStatus.mockReturnValue('Completed');
33+
34+
render(<OptimizeModal />);
35+
36+
// Wait for the component to finish rendering (use async/await)
37+
await waitFor(() => {
38+
expect(screen.queryByText('Optimize')).not.toBeInTheDocument();
39+
});
40+
});
41+
42+
test('renders with migration pending', async () => {
43+
// Mock getMigrationStatus to return 'Completed' for this test
44+
migrationStatus.getMigrationStatus.mockReturnValue('Pending');
45+
46+
render(<OptimizeModal />);
47+
48+
// Wait for the component to finish rendering (use async/await)
49+
await waitFor(() => {
50+
expect(screen.queryByText('Optimize')).toBeInTheDocument();
51+
});
52+
53+
fireEvent.click(screen.getByText(/Optimize/i));
54+
55+
// Wait for modal to be visible
56+
await waitFor(() => {
57+
expect(
58+
screen.queryByText('Configure cluster for Regional-DR?')
59+
).toBeInTheDocument();
60+
});
61+
62+
fireEvent.click(screen.getByText('Close'));
63+
64+
// Wait for modal to be closed
65+
await waitFor(() => {
66+
expect(
67+
screen.queryByText('Configure cluster for Regional-DR?')
68+
).not.toBeInTheDocument();
69+
});
70+
});
71+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as React from 'react';
2+
import { DISASTER_RECOVERY_TARGET_ANNOTATION } from '@odf/core/constants';
3+
import { CephClusterModel } from '@odf/shared/models';
4+
import { getName, getNamespace } from '@odf/shared/selectors';
5+
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
6+
import {
7+
K8sResourceKind,
8+
k8sPatch,
9+
} from '@openshift-console/dynamic-plugin-sdk';
10+
import {
11+
Modal,
12+
ModalVariant,
13+
Button,
14+
ModalBoxHeader,
15+
ModalBoxBody,
16+
Title,
17+
} from '@patternfly/react-core';
18+
import { MigrationStatus, getMigrationStatus } from './constants/osd-migration';
19+
20+
const OptimizeModal: React.FC<OptimizeModalProps> = ({ cephData }) => {
21+
const { t } = useCustomTranslation();
22+
const dRSetupStatus = getMigrationStatus(cephData);
23+
const [isOpen, setOpen] = React.useState(false);
24+
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
25+
26+
const openModal = () => setOpen(true);
27+
28+
const closeModal = () => {
29+
setOpen(false);
30+
setErrorMessage(null);
31+
};
32+
33+
const handleOptimize = () => {
34+
if (cephData && cephData.length > 0) {
35+
const ceph = cephData[0];
36+
const patch = [
37+
{
38+
op: 'add',
39+
path: `/metadata/annotations/${DISASTER_RECOVERY_TARGET_ANNOTATION}`,
40+
value: 'true',
41+
},
42+
];
43+
44+
k8sPatch({
45+
model: CephClusterModel,
46+
resource: {
47+
metadata: {
48+
name: getName(ceph),
49+
namespace: getNamespace(ceph),
50+
},
51+
},
52+
data: patch,
53+
})
54+
.then(() => {
55+
closeModal();
56+
})
57+
.catch((err) => {
58+
setErrorMessage(err.message);
59+
});
60+
}
61+
};
62+
63+
return (
64+
<div>
65+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
66+
<p>{t(dRSetupStatus)}</p>
67+
{dRSetupStatus === MigrationStatus.Pending && (
68+
<a onClick={openModal}>{t('Optimize')}</a>
69+
)}
70+
</div>
71+
<div>
72+
<Modal
73+
variant={ModalVariant.medium}
74+
title={t('Configure cluster for Regional-DR?')}
75+
isOpen={isOpen}
76+
onClose={closeModal}
77+
actions={[
78+
<Button key="close" variant="secondary" onClick={closeModal}>
79+
Close
80+
</Button>,
81+
<Button key="optimize" variant="primary" onClick={handleOptimize}>
82+
Optimize
83+
</Button>,
84+
]}
85+
>
86+
<p>
87+
{t(
88+
'Optimize the cluster for a Regional-DR setup by migrating OSDs. Migration may take some time depending on several factors. To learn more about OSD migration best practices and its consequences, refer to the documentation.'
89+
)}
90+
</p>
91+
{errorMessage && (
92+
<ModalBoxHeader>
93+
<Title headingLevel="h2" size="lg">
94+
Error
95+
</Title>
96+
</ModalBoxHeader>
97+
)}
98+
<ModalBoxBody>{errorMessage}</ModalBoxBody>
99+
</Modal>
100+
</div>
101+
</div>
102+
);
103+
};
104+
105+
export default OptimizeModal;
106+
107+
type OptimizeModalProps = {
108+
cephData?: K8sResourceKind[];
109+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { DISASTER_RECOVERY_TARGET_ANNOTATION } from '@odf/core/constants';
2+
import { getAnnotations } from '@odf/shared/selectors';
3+
4+
export enum MigrationStatus {
5+
InProgress = 'In Progress',
6+
Pending = 'Pending',
7+
Completed = 'Completed',
8+
Unknown = '',
9+
}
10+
11+
export const getMigrationStatus = (cephData) => {
12+
if (cephData && cephData.length > 0) {
13+
const ceph = cephData[0];
14+
const bluestoreCount = ceph?.status?.storage?.osd?.storeType?.['bluestore'];
15+
const bluestoreRdrCount =
16+
ceph?.status?.storage?.osd?.storeType?.['bluestore-rdr'];
17+
18+
const isDisasterRecoveryTarget =
19+
getAnnotations(cephData[0])?.[DISASTER_RECOVERY_TARGET_ANNOTATION] ===
20+
'true';
21+
22+
if (bluestoreCount > 0) {
23+
if (bluestoreRdrCount > 0 || isDisasterRecoveryTarget) {
24+
return MigrationStatus.InProgress;
25+
} else {
26+
return MigrationStatus.Pending;
27+
}
28+
} else if (bluestoreRdrCount > 0) {
29+
return MigrationStatus.Completed;
30+
}
31+
32+
return MigrationStatus.Unknown;
33+
}
34+
return MigrationStatus.Unknown;
35+
};

packages/ocs/dashboards/persistent-internal/details-card.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import { useK8sGet } from '@odf/shared/hooks/k8s-get-hook';
55
import { useFetchCsv } from '@odf/shared/hooks/use-fetch-csv';
66
import { useK8sList } from '@odf/shared/hooks/useK8sList';
77
import {
8+
CephClusterModel,
89
ClusterServiceVersionModel,
910
InfrastructureModel,
1011
} from '@odf/shared/models';
1112
import { getName } from '@odf/shared/selectors';
12-
import { K8sResourceKind, StorageClusterKind } from '@odf/shared/types';
13+
import {
14+
CephClusterKind,
15+
K8sResourceKind,
16+
StorageClusterKind,
17+
} from '@odf/shared/types';
1318
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
1419
import {
1520
getInfrastructurePlatform,
@@ -22,15 +27,22 @@ import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core';
2227
import { CEPH_NS } from '../../constants';
2328
import { StorageClusterModel } from '../../models';
2429
import { getNetworkEncryption } from '../../utils';
30+
import OptimizeModal from './OptimizeModal';
2531

2632
const DetailsCard: React.FC = () => {
2733
const { t } = useCustomTranslation();
34+
const [cephData, cephLoaded, cephLoadError] = useK8sList<CephClusterKind>(
35+
CephClusterModel,
36+
CEPH_NS
37+
);
38+
2839
const [infrastructure, infrastructureLoaded, infrastructureError] =
2940
useK8sGet<K8sResourceKind>(InfrastructureModel, 'cluster');
3041
const [ocsData, ocsLoaded, ocsError] = useK8sList<StorageClusterKind>(
3142
StorageClusterModel,
3243
CEPH_NS
3344
);
45+
3446
const [csv, csvLoaded, csvError] = useFetchCsv({
3547
specName: ODF_OPERATOR,
3648
namespace: CEPH_STORAGE_NAMESPACE,
@@ -100,6 +112,14 @@ const DetailsCard: React.FC = () => {
100112
>
101113
{inTransitEncryptionStatus}
102114
</DetailItem>
115+
<DetailItem
116+
key="dr_setup_state"
117+
title={t('Disaster recovery optimisation')}
118+
isLoading={!cephLoaded}
119+
error={cephLoadError}
120+
>
121+
<OptimizeModal cephData={cephData} />
122+
</DetailItem>
103123
</DetailsBody>
104124
</CardBody>
105125
</Card>

packages/shared/src/types/storage.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ type CephDeviceClass = {
105105
export type CephClusterKind = K8sResourceCommon & {
106106
status?: {
107107
storage: {
108+
osd: {
109+
storeType: {
110+
bluestore: number;
111+
'bluestore-rdr': number;
112+
};
113+
};
108114
deviceClasses: CephDeviceClass[];
109115
};
110116
ceph?: {

0 commit comments

Comments
 (0)