Skip to content

Commit c1f916f

Browse files
committed
StoragePool and StorageSystem changes for new 2 Nodes+Arbiter cluster
When we detect a 2 Nodes + 1 Arbiter cluster, we should only show, '2-way Replication' in 'Data protection policy' list How is '2 Nodes + 1 Arbiter' cluster detected? There is an entry in 'Infrastructure' CR's `controlPlaneTopology` status, value should be `HighlyAvailableArbiter`. Added changes for 'StorageSystem' creation steps. Signed-off-by: Arun Kumar Mohan <[email protected]>
1 parent 9e88174 commit c1f916f

File tree

16 files changed

+368
-50
lines changed

16 files changed

+368
-50
lines changed

locales/en/plugin__odf-console.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
"A Local Volume Set allows you to filter a set of disks, group them and create a dedicated StorageClass to consume storage from them.": "A Local Volume Set allows you to filter a set of disks, group them and create a dedicated StorageClass to consume storage from them.",
133133
"A LocalVolumeSet will be created to allow you to filter a set of disks, group them and create a dedicated StorageClass to consume storage from them.": "A LocalVolumeSet will be created to allow you to filter a set of disks, group them and create a dedicated StorageClass to consume storage from them.",
134134
"A method is required.": "A method is required.",
135-
"A minimum of 3 nodes are required for the initial deployment. Only {{nodes}} node match to the selected filters. Please adjust the filters to include more nodes.": "A minimum of 3 nodes are required for the initial deployment. Only {{nodes}} node match to the selected filters. Please adjust the filters to include more nodes.",
135+
"A minimum of {{minNodes}} nodes are required for the initial deployment. Only {{nodes}} node match to the selected filters. Please adjust the filters to include more nodes.": "A minimum of {{minNodes}} nodes are required for the initial deployment. Only {{nodes}} node match to the selected filters. Please adjust the filters to include more nodes.",
136136
"A mirror peer configuration already exists for one or more of the selected clusters, either from an existing or deleted DR policy. To create a new DR policy with these clusters, delete any existing mirror peer configurations associated with them and try again.": "A mirror peer configuration already exists for one or more of the selected clusters, either from an existing or deleted DR policy. To create a new DR policy with these clusters, delete any existing mirror peer configurations associated with them and try again.",
137137
"A namespace controls access to the OBC and ties the buckets to a specific project.": "A namespace controls access to the OBC and ties the buckets to a specific project.",
138138
"A PEM-encoded CA certificate file used to verify the Vault server's SSL certificate.": "A PEM-encoded CA certificate file used to verify the Vault server's SSL certificate.",
@@ -1821,6 +1821,7 @@
18211821
"This selection determines the storage capabilities of your cluster. Once configured it cannot be changed.": "This selection determines the storage capabilities of your cluster. Once configured it cannot be changed.",
18221822
"This selection is exclusive and cannot be used with other device types.": "This selection is exclusive and cannot be used with other device types.",
18231823
"This selection will delete current and all previous versions of the object from the bucket permanently. This object will be lost forever and cannot be restored.": "This selection will delete current and all previous versions of the object from the bucket permanently. This object will be lost forever and cannot be restored.",
1824+
"This setup spans across 3 zones with 2 data nodes and 1 arbiter. The arbiter maintains quorum without storing data. This setup uses replica-2, which tolerates only one node failure; data loss can occur if multiple nodes fail. Review your failure domain and recovery plan before proceeding.": "This setup spans across 3 zones with 2 data nodes and 1 arbiter. The arbiter maintains quorum without storing data. This setup uses replica-2, which tolerates only one node failure; data loss can occur if multiple nodes fail. Review your failure domain and recovery plan before proceeding.",
18241825
"This status is shown exclusively for default storage classes.": "This status is shown exclusively for default storage classes.",
18251826
"This token is for one-time use only and is valid for 48 hours.": "This token is for one-time use only and is valid for 48 hours.",
18261827
"This URL is used to access and manage secrets, keys, and certificates stored in Azure Key Vault.": "This URL is used to access and manage secrets, keys, and certificates stored in Azure Key Vault.",

packages/ocs/storage-pool/body.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@ import * as React from 'react';
22
import { AttachStorageAction } from '@odf/core/components/attach-storage-storagesystem/state';
33
import { checkArbiterCluster } from '@odf/core/utils';
44
import {
5+
DEFAULT_INFRASTRUCTURE,
56
fieldRequirementsTranslations,
67
formSettings,
78
} from '@odf/shared/constants';
89
import { useK8sGet } from '@odf/shared/hooks';
910
import { TextInputWithFieldRequirements } from '@odf/shared/input-with-requirements';
10-
import { StorageClusterModel } from '@odf/shared/models';
11+
import { InfrastructureModel, StorageClusterModel } from '@odf/shared/models';
1112
import { getNamespace } from '@odf/shared/selectors';
1213
import {
14+
InfraTopologyMode,
15+
InfrastructureKind,
1316
ListKind,
1417
StorageClusterKind,
1518
CephClusterKind,
1619
} from '@odf/shared/types';
1720
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
21+
import { getInfrastructureControlPlaneTopology } from '@odf/shared/utils';
1822
import validationRegEx from '@odf/shared/utils/validation';
1923
import { useYupValidationResolver } from '@odf/shared/yup-validation-resolver';
2024
import {
@@ -83,6 +87,12 @@ export const StoragePoolStatus: React.FC<StoragePoolStatusProps> = ({
8387
);
8488
};
8589

90+
export const isTwoNodePlusArbiterTopology = (
91+
infra: InfrastructureKind
92+
): boolean =>
93+
getInfrastructureControlPlaneTopology(infra) ===
94+
InfraTopologyMode.HighlyAvailableArbiter;
95+
8696
export type StoragePoolStatusProps = {
8797
status: string;
8898
name?: string;
@@ -125,6 +135,9 @@ export const StoragePoolBody: React.FC<StoragePoolBodyProps> = ({
125135
const [storageCluster, storageClusterLoaded, storageClusterLoadError] =
126136
useK8sGet<ListKind<StorageClusterKind>>(StorageClusterModel, null, poolNs);
127137

138+
const [infrastructure, infrastructureLoaded, infrastructureError] =
139+
useK8sGet<InfrastructureKind>(InfrastructureModel, DEFAULT_INFRASTRUCTURE);
140+
128141
const [isReplicaOpen, setReplicaOpen] = React.useState(false);
129142

130143
const poolNameMaxLength = 253;
@@ -188,6 +201,15 @@ export const StoragePoolBody: React.FC<StoragePoolBodyProps> = ({
188201
});
189202
}, [poolName, dispatch, errors?.newPoolName, prefixName, usePrefix]);
190203

204+
// TwoNodeOneArbiterCluster detection
205+
React.useEffect(() => {
206+
if (infrastructureLoaded && !infrastructureError)
207+
dispatch({
208+
type: StoragePoolActionType.SET_POOL_TWO_NODE_ONE_ARBITER,
209+
payload: isTwoNodePlusArbiterTopology(infrastructure),
210+
});
211+
}, [infrastructure, infrastructureLoaded, infrastructureError, dispatch]);
212+
191213
// Failure Domain
192214
React.useEffect(() => {
193215
if (storageClusterLoaded && !storageClusterLoadError)
@@ -225,9 +247,15 @@ export const StoragePoolBody: React.FC<StoragePoolBodyProps> = ({
225247
]);
226248

227249
const replicaList: string[] = _.keys(OCS_DEVICE_REPLICA).filter(
228-
(replica: string) =>
229-
(state.isArbiterCluster && replica === '4') ||
230-
(!state.isArbiterCluster && replica !== '4')
250+
(replica: string) => {
251+
if (state.isTwoNodeOneArbiterCluster) {
252+
return replica === '2';
253+
} else if (state.isArbiterCluster) {
254+
return replica === '4';
255+
} else {
256+
return replica !== '4';
257+
}
258+
}
231259
);
232260

233261
const replicaDropdownItems = replicaList.map((replica) => {

packages/ocs/storage-pool/reducer.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type StoragePoolState = {
44
replicaSize: string;
55
isCompressed: boolean;
66
isArbiterCluster: boolean;
7+
isTwoNodeOneArbiterCluster: boolean;
78
failureDomain: string;
89
inProgress: boolean;
910
errorMessage: string;
@@ -15,6 +16,7 @@ export enum StoragePoolActionType {
1516
SET_POOL_REPLICA_SIZE = 'SET_POOL_REPLICA_SIZE',
1617
SET_POOL_COMPRESSED = 'SET_POOL_COMPRESSED',
1718
SET_POOL_ARBITER = 'SET_POOL_ARBITER',
19+
SET_POOL_TWO_NODE_ONE_ARBITER = 'SET_POOL_TWO_NODE_ONE_ARBITER',
1820
SET_FAILURE_DOMAIN = 'SET_FAILURE_DOMAIN',
1921
SET_INPROGRESS = 'SET_INPROGRESS',
2022
SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE',
@@ -26,6 +28,7 @@ export const blockPoolInitialState: StoragePoolState = {
2628
replicaSize: '',
2729
isCompressed: false,
2830
isArbiterCluster: false,
31+
isTwoNodeOneArbiterCluster: false,
2932
failureDomain: '',
3033
inProgress: false,
3134
errorMessage: '',
@@ -37,6 +40,10 @@ export type StoragePoolAction =
3740
| { type: StoragePoolActionType.SET_POOL_REPLICA_SIZE; payload: string }
3841
| { type: StoragePoolActionType.SET_POOL_COMPRESSED; payload: boolean }
3942
| { type: StoragePoolActionType.SET_POOL_ARBITER; payload: boolean }
43+
| {
44+
type: StoragePoolActionType.SET_POOL_TWO_NODE_ONE_ARBITER;
45+
payload: boolean;
46+
}
4047
| { type: StoragePoolActionType.SET_FAILURE_DOMAIN; payload: string }
4148
| { type: StoragePoolActionType.SET_INPROGRESS; payload: boolean }
4249
| { type: StoragePoolActionType.SET_ERROR_MESSAGE; payload: string };
@@ -76,6 +83,12 @@ export const storagePoolReducer = (
7683
isArbiterCluster: action.payload,
7784
};
7885
}
86+
case StoragePoolActionType.SET_POOL_TWO_NODE_ONE_ARBITER: {
87+
return {
88+
...state,
89+
isTwoNodeOneArbiterCluster: action.payload,
90+
};
91+
}
7992
case StoragePoolActionType.SET_FAILURE_DOMAIN: {
8093
return {
8194
...state,

packages/odf/components/create-storage-system/create-steps.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const createSteps = (
2121
state: WizardState,
2222
dispatch: WizardDispatch,
2323
infraType: InfraProviders,
24+
hasTwoNodesOneArbiter: boolean,
2425
hasOCS: boolean,
2526
supportedExternalStorage: ExternalStorage[],
2627
hasMultipleClusters: boolean
@@ -48,6 +49,7 @@ export const createSteps = (
4849
<CapacityAndNodes
4950
dispatch={dispatch}
5051
infraType={infraType}
52+
isTwoNodesOneArbiterCluster={hasTwoNodesOneArbiter}
5153
state={capacityAndNodes}
5254
storageClass={storageClass}
5355
volumeSetName={createLocalVolumeSet.volumeSetName}
@@ -146,6 +148,7 @@ export const createSteps = (
146148
nodes={nodes}
147149
stepIdReached={stepIdReached}
148150
isMCG={isMCG}
151+
isThisTwoNodesOneArbiterCluster={hasTwoNodesOneArbiter}
149152
systemNamespace={systemNamespace}
150153
/>
151154
),

packages/odf/components/create-storage-system/create-storage-system-steps/capacity-and-nodes-step/capacity-and-nodes-step.tsx

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
TextVariants,
5757
TextContent,
5858
TextInput,
59+
Alert,
5960
} from '@patternfly/react-core';
6061
import { ValidationMessage } from '../../../utils/common-odf-install-el';
6162
import { ErrorHandler } from '../../error-handler';
@@ -239,6 +240,7 @@ const SelectedCapacityAndNodes: React.FC<SelectedCapacityAndNodesProps> = ({
239240
nodes,
240241
systemNamespace,
241242
isLSOPreConfigured,
243+
isTwoNodesOneArbiterClusterEnabled,
242244
}) => {
243245
const { t } = useCustomTranslation();
244246

@@ -249,6 +251,7 @@ const SelectedCapacityAndNodes: React.FC<SelectedCapacityAndNodesProps> = ({
249251
const [hasStrechClusterEnabled, setHasStrechClusterEnabled] =
250252
React.useState(false);
251253
const [zones, setZones] = React.useState([]);
254+
const [arbiterNode, setArbiterNode] = React.useState<WizardNodeState[]>([]);
252255

253256
const pvsBySc = React.useMemo(
254257
() => getSCAvailablePVs(pvs, storageClassName),
@@ -281,12 +284,43 @@ const SelectedCapacityAndNodes: React.FC<SelectedCapacityAndNodesProps> = ({
281284
);
282285
const nodesData = createWizardNodeState(filteredNodes);
283286
dispatch({ type: 'wizard/setNodes', payload: nodesData });
287+
288+
// get the arbiter node details
289+
if (isTwoNodesOneArbiterClusterEnabled) {
290+
const tnArbiter = allNodes.filter((node) =>
291+
node.spec.taints?.some(
292+
(someTaint) =>
293+
someTaint['key'] === 'node-role.kubernetes.io/arbiter'
294+
)
295+
);
296+
if (!!tnArbiter && tnArbiter.length === 1) {
297+
const tnArbiterNodeData = createWizardNodeState(tnArbiter);
298+
// TODO need to check whether there is more than one arbiter set,
299+
// raise an error according to it
300+
setArbiterNode(tnArbiterNodeData);
301+
dispatch({
302+
type: 'wizard/setTwoNodesOneArbiterCluster',
303+
payload: isTwoNodesOneArbiterClusterEnabled,
304+
});
305+
}
306+
}
284307
}
285-
}, [dispatch, allNodeLoadError, allNodeLoaded, allNodes, pvsBySc]);
308+
}, [
309+
dispatch,
310+
allNodeLoadError,
311+
allNodeLoaded,
312+
allNodes,
313+
pvsBySc,
314+
isTwoNodesOneArbiterClusterEnabled,
315+
]);
286316

287317
React.useEffect(() => {
288-
// Validates stretch cluster topology
289-
if (allNodes.length && nodes.length) {
318+
// Validates stretch cluster topology if this is not a TNA cluster
319+
if (
320+
allNodes.length &&
321+
nodes.length &&
322+
!isTwoNodesOneArbiterClusterEnabled
323+
) {
290324
const allZones = getZonesFromNodesKind(allNodes);
291325
const nodesPerZoneMap: NodesPerZoneMap =
292326
getPVAssociatedNodesPerZone(nodes);
@@ -298,7 +332,7 @@ const SelectedCapacityAndNodes: React.FC<SelectedCapacityAndNodesProps> = ({
298332
setHasStrechClusterEnabled(isValidStretchCluster);
299333
setZones(allZones);
300334
}
301-
}, [allNodes, nodes]);
335+
}, [allNodes, nodes, isTwoNodesOneArbiterClusterEnabled]);
302336

303337
// Skipping validation if LSO was configured as part of the SS deployment (in the previous LVS creation wizard step), as that step already have all the required validations
304338
// These validations are needed if LSO was already configured before even starting with the SS deployment
@@ -394,8 +428,21 @@ const SelectedCapacityAndNodes: React.FC<SelectedCapacityAndNodesProps> = ({
394428
/>
395429
</GridItem>
396430
<GridItem span={10}>
397-
<SelectedNodesTable data={nodes} />
431+
<SelectedNodesTable data={nodes.concat(arbiterNode)} />
398432
</GridItem>
433+
{isTwoNodesOneArbiterClusterEnabled && (
434+
<GridItem span={10}>
435+
<Alert
436+
title="2-Nodes and 1-Arbiter setup detected"
437+
variant="warning"
438+
ouiaId="tna-cluster-details"
439+
>
440+
{t(
441+
'This setup spans across 3 zones with 2 data nodes and 1 arbiter. The arbiter maintains quorum without storing data. This setup uses replica-2, which tolerates only one node failure; data loss can occur if multiple nodes fail. Review your failure domain and recovery plan before proceeding.'
442+
)}
443+
</Alert>
444+
</GridItem>
445+
)}
399446
</Grid>
400447
</>
401448
</ErrorHandler>
@@ -412,6 +459,7 @@ type SelectedCapacityAndNodesProps = {
412459
systemNamespace: WizardState['backingStorage']['systemNamespace'];
413460
volumeValidationType: VolumeTypeValidation;
414461
isLSOPreConfigured: boolean;
462+
isTwoNodesOneArbiterClusterEnabled: boolean;
415463
};
416464

417465
export const CapacityAndNodes: React.FC<CapacityAndNodesProps> = ({
@@ -422,6 +470,7 @@ export const CapacityAndNodes: React.FC<CapacityAndNodesProps> = ({
422470
nodes,
423471
systemNamespace,
424472
infraType,
473+
isTwoNodesOneArbiterCluster: isThisTwoNodesOneArbiterCluster,
425474
}) => {
426475
const {
427476
capacity,
@@ -452,6 +501,7 @@ export const CapacityAndNodes: React.FC<CapacityAndNodesProps> = ({
452501
nodes,
453502
state,
454503
isNoProvisioner,
504+
isThisTwoNodesOneArbiterCluster,
455505
osdAmount
456506
);
457507
const onProfileChange = React.useCallback(
@@ -488,6 +538,7 @@ export const CapacityAndNodes: React.FC<CapacityAndNodesProps> = ({
488538
capacity={capacity}
489539
systemNamespace={systemNamespace}
490540
isLSOPreConfigured={isLSOPreConfigured}
541+
isTwoNodesOneArbiterClusterEnabled={isThisTwoNodesOneArbiterCluster}
491542
/>
492543
) : (
493544
<SelectCapacityAndNodes
@@ -543,5 +594,6 @@ type CapacityAndNodesProps = {
543594
volumeSetName: WizardState['createLocalVolumeSet']['volumeSetName'];
544595
dispatch: WizardDispatch;
545596
infraType: InfraProviders;
597+
isTwoNodesOneArbiterCluster: boolean;
546598
systemNamespace: WizardState['backingStorage']['systemNamespace'];
547599
};

packages/odf/components/create-storage-system/create-storage-system-steps/capacity-and-nodes-step/selected-nodes-table.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import {
88
getConvertedUnits,
99
} from '@odf/shared/utils';
1010
import {
11+
TableColumn,
1112
TableData,
1213
useActiveColumns,
1314
VirtualizedTable,
1415
} from '@openshift-console/dynamic-plugin-sdk';
1516
import classNames from 'classnames';
17+
import { Label } from '@patternfly/react-core';
1618
import { sortable } from '@patternfly/react-table';
1719
import { WizardNodeState } from '../../reducer';
1820
import { SelectNodesTableFooter } from '../../select-nodes-table/select-nodes-table-footer';
@@ -55,6 +57,8 @@ const tableColumnClasses = [
5557

5658
const SelectedNodesTableRow = ({ obj, activeColumnIDs }) => {
5759
const { cpu, memory, zone, name, roles } = obj;
60+
const rolesArr = roles as string[];
61+
const isArbiter = rolesArr.some((r) => r === 'arbiter');
5862
return (
5963
<>
6064
<TableData {...tableColumnClasses[0]} activeColumnIDs={activeColumnIDs}>
@@ -63,6 +67,11 @@ const SelectedNodesTableRow = ({ obj, activeColumnIDs }) => {
6367
resourceModel={NodeModel}
6468
resourceName={name}
6569
/>
70+
{isArbiter && (
71+
<Label color="green" variant="filled">
72+
Arbiter
73+
</Label>
74+
)}
6675
</TableData>
6776
<TableData {...tableColumnClasses[1]} activeColumnIDs={activeColumnIDs}>
6877
{roles.join(', ') ?? '-'}
@@ -87,7 +96,7 @@ export const SelectedNodesTable: React.FC<SelectedNodesTableProps> = ({
8796
const { t } = useCustomTranslation();
8897

8998
const SelectedNodesTableColumns = React.useMemo(
90-
() => [
99+
(): TableColumn<WizardNodeState>[] => [
91100
{
92101
title: t('Name'),
93102
sort: 'name',
@@ -120,7 +129,7 @@ export const SelectedNodesTable: React.FC<SelectedNodesTableProps> = ({
120129
);
121130

122131
const [columns] = useActiveColumns({
123-
columns: SelectedNodesTableColumns as any, // Todo(bipuladh): Update once sdk is updated
132+
columns: SelectedNodesTableColumns,
124133
showNamespaceOverride: false,
125134
columnManagementID: 'SELECTED_NODES',
126135
});

0 commit comments

Comments
 (0)