Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release-4.7]Bug 1958873: Fix disk replacement during second replacement #9083

Merged
merged 3 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"replacement disallowed: disk {{diskName}} is {{replacingDiskStatus}}": "replacement disallowed: disk {{diskName}} is {{replacingDiskStatus}}",
"replacement disallowed: disk {{diskName}} is {{replacementStatus}}": "replacement disallowed: disk {{diskName}} is {{replacementStatus}}",
"Disk Replacement": "Disk Replacement",
"This action will start preparing the disk for replacement.": "This action will start preparing the disk for replacement.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ import {
SecretKind,
} from '@console/internal/module/k8s';
import { TemplateModel, TemplateInstanceModel, SecretModel } from '@console/internal/models';
import { DiskMetadata } from '@console/local-storage-operator-plugin/src/components/disks-list/types';
import { CEPH_STORAGE_NAMESPACE, OSD_REMOVAL_TEMPLATE, DASHBOARD_LINK } from '../../../constants';
import { OCSDiskList, OCSColumnStateAction, ActionType, Status } from './state-reducer';
import {
OCSColumnStateAction,
ActionType,
Status,
ReplacedDisk,
OCSColumnState,
} from './state-reducer';

const createTemplateSecret = async (template: TemplateKind, osdId: string) => {
const parametersSecret: SecretKind = {
Expand All @@ -40,17 +47,23 @@ const createTemplateInstance = async (
parametersSecret: SecretKind,
template: TemplateKind,
osd: string,
disk: string,
nodeName: string,
disk: DiskMetadata,
) => {
const { path, deviceID, serial } = disk;
const templateInstance: TemplateInstanceKind = {
apiVersion: apiVersionForModel(TemplateInstanceModel),
kind: TemplateInstanceModel.kind,
metadata: {
generateName: `${OSD_REMOVAL_TEMPLATE}-${osd}-`,
namespace: CEPH_STORAGE_NAMESPACE,
/* Adding annotations to uniquely identify a disk after replacement. */
annotations: {
disk,
osd,
devicePath: path,
deviceOsd: osd,
deviceNode: nodeName,
deviceID,
deviceSerial: serial,
},
},
spec: {
Expand All @@ -63,20 +76,23 @@ const createTemplateInstance = async (
return k8sCreate(TemplateInstanceModel, templateInstance);
};

const instantiateTemplate = async (osdId: string, diskName: string) => {
const instantiateTemplate = async (osdId: string, nodeName: string, disk: DiskMetadata) => {
const osdRemovalTemplate = await k8sGet(
TemplateModel,
OSD_REMOVAL_TEMPLATE,
CEPH_STORAGE_NAMESPACE,
);
const templateSecret = await createTemplateSecret(osdRemovalTemplate, osdId);
await createTemplateInstance(templateSecret, osdRemovalTemplate, osdId, diskName);
await createTemplateInstance(templateSecret, osdRemovalTemplate, osdId, nodeName, disk);
};

const DiskReplacementAction = (props: DiskReplacementActionProps) => {
const { t } = useTranslation();

const { diskName, alertsMap, replacementMap, isRebalancing, dispatch, cancel, close } = props;
const { disk, nodeName, ocsState, dispatch, cancel, close } = props;

const { path: diskName, deviceID: diskID, serial: diskSerial } = disk;
const { alertsMap, replacingDiskList, replacedDiskList, isRebalancing } = ocsState;

const [inProgress, setProgress] = React.useState(false);
const [errorMessage, setError] = React.useState('');
Expand All @@ -85,11 +101,38 @@ const DiskReplacementAction = (props: DiskReplacementActionProps) => {
event.preventDefault();
setProgress(true);
try {
const { osd } = alertsMap[diskName];
const replacementStatus = replacementMap[diskName]?.status;
let replacingDiskIndex = -1;
const replacingDiskStatus = Status.PreparingToReplace;
const { osd } = alertsMap?.[diskName] || { osd: '' };
const replacedDisk = replacedDiskList?.find((rd: ReplacedDisk) => {
const diskInfo = rd?.disk || { id: '', serial: '', path: '', node: '' };
return (
rd?.node === nodeName &&
diskInfo.path === diskName &&
diskInfo?.id === diskID &&
diskInfo?.serial === diskSerial
);
});
const { status: replacementStatus } = replacedDisk || {};

if (replacingDiskList.length)
replacingDiskIndex = replacingDiskList.findIndex(
(rd) => rd?.id === diskID && rd?.serial === diskSerial && rd?.path === diskName,
);

if (replacingDiskIndex !== -1) {
throw new Error(
t(
'ceph-storage-plugin~replacement disallowed: disk {{diskName}} is {{replacingDiskStatus}}',
{ diskName, replacingDiskStatus },
),
);
}

if (
replacementStatus === Status.PreparingToReplace ||
replacementStatus === Status.ReplacementReady
replacementStatus === Status.ReplacementReady ||
replacementStatus === Status.ReplacementFailed
)
throw new Error(
t(
Expand All @@ -98,10 +141,16 @@ const DiskReplacementAction = (props: DiskReplacementActionProps) => {
),
);
else {
instantiateTemplate(osd, diskName);
instantiateTemplate(osd, nodeName, disk);
dispatch({
type: ActionType.SET_REPLACEMENT_MAP,
payload: { [diskName]: { osd, status: Status.PreparingToReplace } },
type: ActionType.SET_REPLACING_DISK_LIST,
payload: [
{
id: diskID,
serial: diskSerial,
path: diskName,
},
],
});
}
close();
Expand All @@ -117,7 +166,7 @@ const DiskReplacementAction = (props: DiskReplacementActionProps) => {
<ModalTitle>{t('ceph-storage-plugin~Disk Replacement')}</ModalTitle>
<ModalBody>
<p>{t('ceph-storage-plugin~This action will start preparing the disk for replacement.')}</p>
{isRebalancing && alertsMap[diskName].status !== Status.Offline && (
{isRebalancing && alertsMap?.[diskName]?.status !== Status.Offline && (
<Alert
variant="info"
className="co-alert"
Expand Down Expand Up @@ -148,9 +197,8 @@ const DiskReplacementAction = (props: DiskReplacementActionProps) => {
export const diskReplacementModal = createModalLauncher(DiskReplacementAction);

export type DiskReplacementActionProps = {
diskName: string;
isRebalancing: boolean;
alertsMap: OCSDiskList;
replacementMap: OCSDiskList;
disk: DiskMetadata;
nodeName: string;
ocsState: OCSColumnState;
dispatch: React.Dispatch<OCSColumnStateAction>;
} & ModalComponentProps;
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
OSD_DOWN_ALERT,
OSD_DOWN_AND_OUT_ALERT,
} from '../../../constants/index';
import { getAnnotations } from '@console/shared/src';
import { OCSKebabOptions } from './ocs-kebab-options';
import { OCSStatus } from './ocs-status-column';
import {
Expand All @@ -52,6 +53,7 @@ import {
OCSColumnStateAction,
Status,
OCSDiskStatus,
ReplacedDisk,
} from './state-reducer';

const getTiBasedStatus = (status: string): OCSDiskStatus => {
Expand Down Expand Up @@ -101,13 +103,19 @@ const diskRow: RowFunction<DiskMetadata, OCSMetadata> = ({
style,
customData,
}) => {
const { ocsState, dispatch } = customData;
const diskName = obj.path;
const { ocsState, nodeName, dispatch } = customData;
return (
<TableRow id={obj.deviceID} index={index} trKey={key} style={style}>
<TableData className={tableColumnClasses[0]}>{obj.path}</TableData>
<TableData className={tableColumnClasses[1]}>{obj.status.state}</TableData>
<OCSStatus ocsState={ocsState} diskName={diskName} className={tableColumnClasses[1]} />
<OCSStatus
ocsState={ocsState}
nodeName={nodeName}
diskName={obj.path}
diskID={obj.deviceID}
diskSerial={obj.serial}
className={tableColumnClasses[1]}
/>
<TableData className={tableColumnClasses[2]}>{obj.type || '-'}</TableData>
<TableData className={cx(tableColumnClasses[3], 'co-break-word')}>
{obj.model || '-'}
Expand All @@ -116,25 +124,20 @@ const diskRow: RowFunction<DiskMetadata, OCSMetadata> = ({
{humanizeBinaryBytes(obj.size).string || '-'}
</TableData>
<TableData className={tableColumnClasses[5]}>{obj.fstype || '-'}</TableData>
<OCSKebabOptions
diskName={diskName}
alertsMap={ocsState.alertsMap}
replacementMap={ocsState.replacementMap}
isRebalancing={ocsState.isRebalancing}
dispatch={dispatch}
/>
<OCSKebabOptions disk={obj} nodeName={nodeName} ocsState={ocsState} dispatch={dispatch} />
</TableRow>
);
};

const OCSDisksList: React.FC<TableProps> = React.memo((props) => {
const { t } = useTranslation();

const [ocsState, dispatch] = React.useReducer(reducer, initialState);

const nodeName = props.customData.node;

const [cephDiskData, cephDiskLoadError, cephDiskLoading] = usePrometheusPoll({
endpoint: PrometheusEndpoint.QUERY,
query: osdDiskInfoMetric({ nodeName: props.customData.node }),
query: osdDiskInfoMetric({ nodeName }),
});
const [progressData, progressLoadError, progressLoading] = usePrometheusPoll({
endpoint: PrometheusEndpoint.QUERY,
Expand All @@ -158,14 +161,15 @@ const OCSDisksList: React.FC<TableProps> = React.memo((props) => {
ocsDiskList[metric.device] = {
osd: metric.ceph_daemon,
status: Status.Online,
node: metric.exported_instance,
};
return ocsDiskList;
}, {});
const newAlertsMap: OCSDiskList = alertsData.reduce(
(ocsDiskList: OCSDiskList, alert: Alert) => {
const { rule, labels } = alert;
const status = getAlertsBasedStatus(rule.name);
if (status) ocsDiskList[labels.device] = { osd: labels.disk, status };
if (status) ocsDiskList[labels.device] = { osd: labels.disk, status, node: labels.host };
return ocsDiskList;
},
{},
Expand All @@ -184,20 +188,49 @@ const OCSDisksList: React.FC<TableProps> = React.memo((props) => {
}

if (tiLoaded && !tiLoadError && tiData.length) {
const newData: OCSDiskList = tiData.reduce((data, ti) => {
const disk = ti.metadata.annotations?.disk;
const osd = ti.metadata.annotations?.osd;
if (osd && disk) {
data[disk] = {
osd,
const newData: ReplacedDisk[] = tiData.reduce((data: ReplacedDisk[], ti) => {
const { devicePath, deviceID, deviceOsd, deviceNode, deviceSerial } =
getAnnotations(ti) || {};
if (devicePath && deviceOsd && deviceNode === nodeName) {
data.push({
osd: deviceOsd,
disk: {
id: deviceID,
path: devicePath,
serial: deviceSerial,
},
node: nodeName,
status: getTiBasedStatus(ti.status.conditions?.[0].type),
};
});
}
return data;
}, {});
if (!_.isEqual(newData, ocsState.replacementMap)) {
}, []);
if (!_.isEqual(newData, ocsState.replacedDiskList)) {
dispatch({
type: ActionType.SET_REPLACED_DISK_LIST,
payload: newData,
});
}
}

if (ocsState.replacingDiskList.length !== 0) {
const replacedDiskIndexList = ocsState.replacingDiskList.reduce((indexes, disk, index) => {
const hasReplaced = ocsState.replacedDiskList?.some((rd: ReplacedDisk) => {
const diskInfo = rd?.disk;
return (
diskInfo?.path === disk.path &&
diskInfo?.id === disk.id &&
diskInfo?.serial === disk.serial
);
});
if (hasReplaced) indexes.push(index);
return indexes;
}, []);
if (replacedDiskIndexList.length) {
const newData = [...ocsState.replacingDiskList];
replacedDiskIndexList.forEach((index) => newData.splice(index, 1));
dispatch({
type: ActionType.SET_REPLACEMENT_MAP,
type: ActionType.SET_REPLACING_DISK_LIST,
payload: newData,
});
}
Expand Down Expand Up @@ -261,7 +294,7 @@ const OCSDisksList: React.FC<TableProps> = React.memo((props) => {
aria-label={t('ceph-storage-plugin~Disks List')}
Header={diskHeader}
Row={diskRow}
customData={{ ocsState, dispatch }}
customData={{ ocsState, dispatch, nodeName }}
NoDataEmptyMsg={props.customData.EmptyMsg}
virtualize
/>
Expand All @@ -273,6 +306,7 @@ export const OCSNodesDiskListPage = (props: NodesDisksListPageProps) => (
);

type OCSMetadata = {
nodeName: string;
ocsState: OCSColumnState;
dispatch: React.Dispatch<OCSColumnStateAction>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@ import { useTranslation } from 'react-i18next';
import { TableData } from '@console/internal/components/factory';
import { KebabOption, Kebab } from '@console/internal/components/utils';
import { diskReplacementModal } from './disk-replacement-modal';
import { OCSDiskList, OCSColumnStateAction } from './state-reducer';
import { OCSColumnState, OCSColumnStateAction } from './state-reducer';
import { DiskMetadata } from '@console/local-storage-operator-plugin/src/components/disks-list/types';

export const OCSKebabOptions: React.FC<OCSKebabOptionsProps> = React.memo(
({ diskName, alertsMap, replacementMap, isRebalancing, dispatch }) => {
({ nodeName, disk, ocsState, dispatch }) => {
const { t } = useTranslation();

const { alertsMap } = ocsState;

const kebabOptions: KebabOption[] = [
{
// t('ceph-storage-plugin~Start Disk Replacement')
labelKey: t('ceph-storage-plugin~Start Disk Replacement'),
callback: () =>
diskReplacementModal({
diskName,
alertsMap,
replacementMap,
isRebalancing,
nodeName,
disk,
ocsState,
dispatch,
}),
},
Expand All @@ -27,16 +29,15 @@ export const OCSKebabOptions: React.FC<OCSKebabOptionsProps> = React.memo(
return (
<TableData className={Kebab.columnClass}>
{/* Enables the options for the disk with failures */}
<Kebab options={kebabOptions} isDisabled={!alertsMap[diskName]} />
<Kebab options={kebabOptions} isDisabled={alertsMap?.[disk.path]?.node !== nodeName} />
</TableData>
);
},
);

type OCSKebabOptionsProps = {
diskName: string;
alertsMap: OCSDiskList;
replacementMap: OCSDiskList;
isRebalancing: boolean;
disk: DiskMetadata;
nodeName: string;
ocsState: OCSColumnState;
dispatch: React.Dispatch<OCSColumnStateAction>;
};