Skip to content

Commit

Permalink
Provide option to enable DR support
Browse files Browse the repository at this point in the history
Signed-off-by: Timothy Asir Jeyasingh <[email protected]>
  • Loading branch information
TimothyAsirJeyasing committed Nov 28, 2023
1 parent ee80530 commit 464be9d
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 1 deletion.
5 changes: 5 additions & 0 deletions locales/en/plugin__odf-console.json
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@
"Only showing PVCs that are being mounted on an active pod": "Only showing PVCs that are being mounted on an active pod",
"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.",
"Internal": "Internal",
"Disaster recovery optimisation": "Disaster recovery optimisation",
"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.",
"Troubleshoot": "Troubleshoot",
"Active health checks": "Active health checks",
Expand All @@ -460,6 +461,10 @@
"Go To PVC List": "Go To PVC List",
"Save": "Save",
"BlockPool Update Form": "BlockPool Update Form",
"Optimise cluster for Regional-DR?": "Optimise cluster for Regional-DR?",
"Configure the cluster for a Regional-DR setup by migrating OSDs. Migration may take some time depending on several factors. To learn more about OSDs migration best practices and its consequences refer to the documentation.": "Configure the cluster for a Regional-DR setup by migrating OSDs. Migration may take some time depending on several factors. To learn more about OSDs migration best practices and its consequences refer to the documentation.",
"Optimise": "Optimise",
"Optimise cluster": "Optimise cluster",
"Filesystem name": "Filesystem name",
"Enter filesystem name": "Enter filesystem name",
"CephFS filesystem name into which the volume shall be created": "CephFS filesystem name into which the volume shall be created",
Expand Down
22 changes: 21 additions & 1 deletion packages/ocs/dashboards/persistent-internal/details-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import * as React from 'react';
import { useSafeK8sList } from '@odf/core/hooks';
import { useODFNamespaceSelector } from '@odf/core/redux';
import { getOperatorVersion } from '@odf/core/utils';
import { OSDMigrationDetails } from '@odf/ocs/modals/osd-migration/osdMigrationDetails';
import { ODF_OPERATOR } from '@odf/shared/constants';
import { useK8sGet } from '@odf/shared/hooks/k8s-get-hook';
import { useFetchCsv } from '@odf/shared/hooks/use-fetch-csv';
import {
CephClusterModel,
ClusterServiceVersionModel,
InfrastructureModel,
} from '@odf/shared/models';
import { getName } from '@odf/shared/selectors';
import { K8sResourceKind, StorageClusterKind } from '@odf/shared/types';
import {
CephClusterKind,
K8sResourceKind,
StorageClusterKind,
} from '@odf/shared/types';
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
import {
getInfrastructurePlatform,
Expand All @@ -34,6 +40,12 @@ const DetailsCard: React.FC = () => {
StorageClusterModel,
odfNamespace
);

const [cephData, cephLoaded, cephLoadError] = useSafeK8sList<CephClusterKind>(
CephClusterModel,
odfNamespace
);

const [csv, csvLoaded, csvError] = useFetchCsv({
specName: ODF_OPERATOR,
namespace: odfNamespace,
Expand Down Expand Up @@ -104,6 +116,14 @@ const DetailsCard: React.FC = () => {
>
{inTransitEncryptionStatus}
</DetailItem>
<DetailItem
key="osd_migration"
title={t('Disaster recovery optimisation')}
isLoading={!cephLoaded}
error={cephLoadError as any}
>
<OSDMigrationDetails cephData={cephData?.[0]} ocsData={cluster} />
</DetailItem>
</DetailsBody>
</CardBody>
</Card>
Expand Down
81 changes: 81 additions & 0 deletions packages/ocs/modals/osd-migration/osd-migration-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';
import { DISASTER_RECOVERY_TARGET_ANNOTATION } from '@odf/core/constants';
import { CommonModalProps, ModalBody, ModalFooter } from '@odf/shared/modals';
import { OCSStorageClusterModel } from '@odf/shared/models';
import { getName, getNamespace } from '@odf/shared/selectors';
import { StorageClusterKind } from '@odf/shared/types';
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
import { k8sPatch } from '@openshift-console/dynamic-plugin-sdk';
import { Modal, ModalVariant, Button, Alert } from '@patternfly/react-core';

export const OSDMigrationModal: React.FC<OSDMigrationModalProps> = ({
isOpen,
extraProps,
closeModal,
}) => {
const { t } = useCustomTranslation();
const ocsData = extraProps?.ocsData;
const [errorMessage, setErrorMessage] = React.useState<string>('');

const handleOptimize = () => {
const patch = [
{
op: 'add',
path: `metadata/annotations/${DISASTER_RECOVERY_TARGET_ANNOTATION}`,
value: 'true',
},
];

k8sPatch({
model: OCSStorageClusterModel,
resource: {
metadata: {
name: getName(ocsData),
namespace: getNamespace(ocsData),
},
},
data: patch,
})
.then(() => {
closeModal();
})
.catch((err) => {
setErrorMessage(err.message);
});
};

return (
<Modal
variant={ModalVariant.small}
title={t('Optimise cluster for Regional-DR?')}
isOpen={isOpen}
onClose={closeModal}
>
{t(
'Configure the cluster for a Regional-DR setup by migrating OSDs. Migration may take some time depending on several factors. To learn more about OSDs migration best practices and its consequences refer to the documentation.'
)}
{/* TODO: Show doc link once ViewDocumentation moved to shared */}
<ModalBody>
{!!errorMessage && (
<Alert isInline variant="danger" title={t('An error occurred')}>
{errorMessage}
</Alert>
)}
</ModalBody>
<ModalFooter>
<Button key="close" variant="secondary" onClick={closeModal}>
{t('Close')}
</Button>
<Button key="optimize" variant="primary" onClick={handleOptimize}>
{t('Optimise')}
</Button>
</ModalFooter>
</Modal>
);
};

type OSDMigrationModalProps = CommonModalProps<{
ocsData: StorageClusterKind;
}>;

export default OSDMigrationModal;
91 changes: 91 additions & 0 deletions packages/ocs/modals/osd-migration/osdMigrationDetails.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import { BLUESTORE, BLUESTORE_RDR } from '@odf/core/constants';
import { screen, render, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { MemoryRouter } from 'react-router-dom';
import { OSDMigrationModal } from '../../modals/osd-migration/osd-migration-modal';
import * as migrationStatus from '../../utils/osd-migration';

jest.mock('@odf/shared/selectors', () => ({
getName: jest.fn().mockReturnValue('test'),
}));

// Mocking getOSDMigrationStatus
jest.mock('../../utils/osd-migration');

jest.mock('@odf/shared/hooks/useK8sList', () => ({
__esModule: true,
useK8sList: () => [
[
{
metadata: {
name: 'test',
},
},
],
true,
undefined,
],
}));

describe('OptimizeModal Component', () => {
test('renders without errors', async () => {
const cephData = {
status: {
storage: {
osd: {
storeType: {
[BLUESTORE_RDR]: 5,
},
},
},
},
};

// Mock getOSDMigrationStatus to return 'Completed' for this test
migrationStatus.getOSDMigrationStatus.mockReturnValue('Completed');

render(
<MemoryRouter>
<OSDMigrationModal cephData={cephData} ocsData={null} isOpen={false} />
</MemoryRouter>
);

// Wait for the component to finish rendering (use async/await)
await waitFor(() => {
expect(screen.queryByText('Optimise')).not.toBeInTheDocument();
});
});

test('renders with migration pending', async () => {
const cephData = {
status: {
storage: {
osd: {
storeType: {
[BLUESTORE]: 5,
},
},
},
},
};

migrationStatus.getOSDMigrationStatus.mockReturnValue('Pending');
render(
<MemoryRouter>
<OSDMigrationModal cephData={cephData} ocsData={null} isOpen={true} />
</MemoryRouter>
);

await waitFor(() => {
expect(screen.queryByText('Optimise')).toBeInTheDocument();
});

// Wait for modal to be visible
await waitFor(() => {
expect(
screen.queryByText('Optimise cluster for Regional-DR?')
).toBeInTheDocument();
});
});
});
58 changes: 58 additions & 0 deletions packages/ocs/modals/osd-migration/osdMigrationDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';
import { OSDMigrationStatus } from '@odf/core/constants';
import { getOSDMigrationStatus } from '@odf/ocs/utils';
import { CephClusterKind, StorageClusterKind } from '@odf/shared/types';
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
import {
RedExclamationCircleIcon,
StatusIconAndText,
useModal,
} from '@openshift-console/dynamic-plugin-sdk';
import { Button, Flex, FlexItem } from '@patternfly/react-core';
import OSDMigrationModal from './osd-migration-modal';

export const OSDMigrationDetails: React.FC<OSDMigrationDetailsProps> = ({
cephData,
ocsData,
}) => {
const { t } = useCustomTranslation();
const osdMigrationStatus: string = getOSDMigrationStatus(cephData);
const launcher = useModal();

return (
<>
<Flex>
<FlexItem>
<StatusIconAndText
title={osdMigrationStatus}
icon={
osdMigrationStatus === OSDMigrationStatus.FAILED && (
<RedExclamationCircleIcon />
)
}
/>
</FlexItem>
<FlexItem>
{osdMigrationStatus === OSDMigrationStatus.PENDING && (
<Button
variant="link"
onClick={() =>
launcher(OSDMigrationModal, {
isOpen: true,
extraProps: { ocsData },
})
}
>
{t('Optimise cluster')}
</Button>
)}
</FlexItem>
</Flex>
</>
);
};

type OSDMigrationDetailsProps = {
cephData: CephClusterKind;
ocsData: StorageClusterKind;
};
1 change: 1 addition & 0 deletions packages/ocs/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './noobaa-health';
export * from './common';
export * from './metrics';
export * from './block-pool';
export * from './osd-migration';
39 changes: 39 additions & 0 deletions packages/ocs/utils/osd-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
BLUESTORE,
BLUESTORE_RDR,
OSDMigrationStatus,
} from '@odf/core/constants';
import { CephClusterKind } from '@odf/shared/types';

function getCephStoreType(ceph: CephClusterKind) {
return ceph.status?.storage?.osd?.storeType;
}

const getBluestoreCount = (ceph: CephClusterKind): number => {
return getCephStoreType(ceph)?.[BLUESTORE] || 0;
};

const getBluestoreRdrCount = (ceph: CephClusterKind): number => {
return getCephStoreType(ceph)?.[BLUESTORE_RDR] || 0;
};

export const getOSDMigrationStatus = (ceph: CephClusterKind) => {
if (!!ceph) {
const bluestoreCount = getBluestoreCount(ceph);
const bluestoreRdrCount = getBluestoreRdrCount(ceph);

if (bluestoreCount > 0) {
if (bluestoreRdrCount > 0) {
return OSDMigrationStatus.IN_PROGRESS;
} else {
return OSDMigrationStatus.PENDING;
}
} else if (bluestoreRdrCount > 0) {
return OSDMigrationStatus.COMPLETED;
}
} else {
return OSDMigrationStatus.FAILED;
} // TODO Add condition for migration failure

return '';
};
10 changes: 10 additions & 0 deletions packages/odf/constants/dataProtection.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
export const DISASTER_RECOVERY_TARGET_ANNOTATION =
'ocs.openshift.io/clusterIsDisasterRecoveryTarget';

export enum OSDMigrationStatus {
IN_PROGRESS = 'In Progress',
PENDING = 'Pending',
COMPLETED = 'Completed',
FAILED = 'Failed',
}

export const BLUESTORE_RDR = 'bluestore-rdr';
export const BLUESTORE = 'bluestore';
6 changes: 6 additions & 0 deletions packages/shared/src/types/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ type CephDeviceClass = {
export type CephClusterKind = K8sResourceCommon & {
status?: {
storage: {
osd: {
storeType: {
bluestore: number;
'bluestore-rdr': number;
};
};
deviceClasses: CephDeviceClass[];
};
ceph?: {
Expand Down

0 comments on commit 464be9d

Please sign in to comment.