Skip to content

Commit

Permalink
Add a plan details item to edit boot disk
Browse files Browse the repository at this point in the history
Signed-off-by: yaacov <[email protected]>
  • Loading branch information
yaacov committed Jun 18, 2024
1 parent c16b171 commit a682f81
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@
"Assessment": "Assessment",
"Authentication type": "Authentication type",
"Bandwidth": "Bandwidth",
"Boot from first root device": "Boot from first root device",
"Boot from the first hard drive": "Boot from the first hard drive",
"Boot from the first partition on the first hard drive": "Boot from the first partition on the first hard drive",
"Boot from the first partition on the second hard drive": "Boot from the first partition on the second hard drive",
"Boot from the second hard drive": "Boot from the second hard drive",
"Boot from the second partition on the first hard drive": "Boot from the second partition on the first hard drive",
"Boot from the second partition on the second hard drive": "Boot from the second partition on the second hard drive",
"CA certificate": "CA certificate",
"CA certificate - disabled when 'Skip certificate validation' is selected": "CA certificate - disabled when 'Skip certificate validation' is selected",
"CA certificate - leave empty to use system CA certificates": "CA certificate - leave empty to use system CA certificates",
Expand All @@ -60,6 +67,7 @@
"Cannot retrieve certificate": "Cannot retrieve certificate",
"Category": "Category",
"Certificate change detected": "Certificate change detected",
"Choose the root filesystem to be converted.": "Choose the root filesystem to be converted.",
"Clear all filters": "Clear all filters",
"Click the pencil for setting provider web UI link": "Click the pencil for setting provider web UI link",
"Click the update credentials button to save your changes, button is disabled until a change is detected.": "Click the update credentials button to save your changes, button is disabled until a change is detected.",
Expand Down Expand Up @@ -142,6 +150,7 @@
"Edit Provider Credentials": "Edit Provider Credentials",
"Edit provider credentials.\n Use this link to edit the providers credentials instead of editing the secret directly.": "Edit provider credentials.\n Use this link to edit the providers credentials instead of editing the secret directly.",
"Edit provider web UI link": "Edit provider web UI link",
"Edit root device": "Edit root device",
"Edit Snapshot polling interval (seconds)": "Edit Snapshot polling interval (seconds)",
"Edit StorageMap": "Edit StorageMap",
"Edit target namespace": "Edit target namespace",
Expand Down Expand Up @@ -179,6 +188,7 @@
"Filter by template": "Filter by template",
"Filter by tenant": "Filter by tenant",
"Filter provider": "Filter provider",
"First root device": "First root device",
"Flavor": "Flavor",
"Folder": "Folder",
"GPUs/Host Devices": "GPUs/Host Devices",
Expand Down Expand Up @@ -382,6 +392,7 @@
"Restore default columns": "Restore default columns",
"Return to the providers list page": "Return to the providers list page",
"Reveal values": "Reveal values",
"Root device": "Root device",
"Run the migration plan.": "Run the migration plan.",
"Running": "Running",
"Running virtual machines": "Running virtual machines",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DescriptionList } from '@patternfly/react-core';
import {
PreserveClusterCpuModelDetailsItem,
PreserveStaticIPsDetailsItem,
RootDiskDetailsItem,
SetLUKSEncryptionPasswordsDetailsItem,
TargetNamespaceDetailsItem,
TransferNetworkDetailsItem,
Expand Down Expand Up @@ -75,6 +76,10 @@ export const SettingsSectionInternal: React.FC<SettingsSectionProps> = ({ obj, p
{['vsphere'].includes(sourceProvider?.spec?.type) && (
<SetLUKSEncryptionPasswordsDetailsItem resource={obj} canPatch={permissions.canPatch} />
)}

{['vsphere'].includes(sourceProvider?.spec?.type) && (
<RootDiskDetailsItem resource={obj} canPatch={permissions.canPatch} />
)}
</DescriptionList>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { useModal } from 'src/modules/Providers/modals';
import { DetailsItem } from 'src/modules/Providers/utils';
import { useForkliftTranslation } from 'src/utils/i18n';

import { Label } from '@patternfly/react-core';

import { PlanDetailsItemProps } from '../../DetailsSection';
import { VIRT_V2V_HELP_LINK } from '../modals';
import { getRootDiskLabelByKey } from '../modals/EditRootDisk';
import { EditRootDisk } from '../modals/EditRootDisk/EditRootDisk';

export const RootDiskDetailsItem: React.FC<PlanDetailsItemProps> = ({
resource,
canPatch,
helpContent,
}) => {
const { t } = useForkliftTranslation();
const { showModal } = useModal();

const defaultHelpContent = t(`Choose the root filesystem to be converted.`);

const rootDisk = resource?.spec?.vms?.[0].rootDisk;

return (
<DetailsItem
title={t('Root device')}
content={getDiskLabel(rootDisk)}
helpContent={helpContent ?? defaultHelpContent}
moreInfoLink={VIRT_V2V_HELP_LINK}
crumbs={['spec', 'vms', 'rootDisk']}
onEdit={canPatch && (() => showModal(<EditRootDisk resource={resource} />))}
/>
);
};

/**
* Generates a label component for the given disk key.
* @param {string} diskKey - The key representing the disk option.
* @returns {JSX.Element} The label component for the disk.
*/
const getDiskLabel = (diskKey: string) => {
const diskLabel = getRootDiskLabelByKey(diskKey);

// First root disk is the default option
const color = !diskKey ? 'green' : 'grey';

return (
<Label isCompact color={color}>
{diskLabel}
</Label>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @index(['./*', /style/g], f => `export * from '${f.path}';`)
export * from './PreserveClusterCpuModelDetailsItem';
export * from './PreserveStaticIPsDetailsItem';
export * from './RootDiskDetailsItem';
export * from './SetLUKSEncryptionPasswordsDetailsItem';
export * from './TargetNamespaceDetailsItem';
export * from './TransferNetworkDetailsItem';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React from 'react';
import { FilterableSelect } from 'src/components/FilterableSelect/FilterableSelect';
import {
EditModal,
EditModalProps,
ModalInputComponentType,
OnConfirmHookType,
} from 'src/modules/Providers/modals';
import { useForkliftTranslation } from 'src/utils/i18n';

import { Modify, PlanModel, V1beta1Plan } from '@kubev2v/types';
import { K8sModel, k8sPatch } from '@openshift-console/dynamic-plugin-sdk';
import { HelperText, HelperTextItem, Text } from '@patternfly/react-core';

import { editRootDiskModalAlert } from './editRootDiskModalAlert';
import { editRootDiskModalBody } from './editRootDiskModalBody';
import { diskOptions, getRootDiskLabelByKey } from './getRootDiskLabelByKey';

const onConfirm: OnConfirmHookType = async ({ resource, model, newValue }) => {
const plan = resource as V1beta1Plan;

const resourceValue = plan?.spec?.vms;
const op = resourceValue ? 'replace' : 'add';
const newVMs = resourceValue.map((vm) => ({
...vm,
rootDisk: newValue || undefined,
}));

const obj = await k8sPatch({
model: model,
resource: resource,
data: [
{
op,
path: '/spec/vms',
value: newVMs || undefined,
},
],
});

return obj;
};

interface DropdownRendererProps {
value: string | number;
onChange: (string) => void;
}

const RootDiskInputFactory: () => ModalInputComponentType = () => {
const DropdownRenderer: React.FC<DropdownRendererProps> = ({ value, onChange }) => {
const { t } = useForkliftTranslation();
const options = diskOptions(t);

const dropdownItems = options.map((option) => ({
itemId: option.key,
children: (
<>
<Text>{getRootDiskLabelByKey(option.key)}</Text>
{option.description && (
<HelperText>
<HelperTextItem variant="indeterminate">{option.description}</HelperTextItem>
</HelperText>
)}
</>
),
}));

return (
<FilterableSelect
selectOptions={dropdownItems}
value={value as string}
onSelect={onChange}
canCreate
placeholder={t('First root device')}
></FilterableSelect>
);
};

return DropdownRenderer;
};

export const EditRootDisk: React.FC<EditRootDiskProps> = (props) => {
const { t } = useForkliftTranslation();

const plan = props.resource;
const rootDisk = plan.spec.vms?.[0]?.rootDisk;
const allVMsHasMatchingRootDisk = plan.spec.vms.every((vm) => vm?.rootDisk === rootDisk);

return (
<EditModal
{...props}
jsonPath={(obj: V1beta1Plan) => obj?.spec?.vms?.[0]?.rootDisk}
title={props?.title || t('Edit root device')}
label={props?.label || t('Root device')}
model={PlanModel}
onConfirmHook={onConfirm}
body={
<>
{editRootDiskModalBody}
{!allVMsHasMatchingRootDisk && editRootDiskModalAlert}
</>
}
InputComponent={RootDiskInputFactory()}
/>
);
};

export type EditRootDiskProps = Modify<
EditModalProps,
{
resource: V1beta1Plan;
title?: string;
label?: string;
model?: K8sModel;
jsonPath?: string | string[];
}
>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { AlertMessageForModals } from 'src/modules/Providers/modals';

export const editRootDiskModalAlert = (
<AlertMessageForModals
variant="warning"
title={'The plan rootDisk keys was manually configured'}
message={
<>
<p>Warning: not all virtual machines are configures using the same root disk number,</p>
<p>updating the root disk number will override the current configuration.</p>
</>
}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { ForkliftTrans } from 'src/utils';

import { ExternalLink } from '@kubev2v/common';

import { VIRT_V2V_HELP_LINK } from '../EditLUKSEncryptionPasswords';

export const editRootDiskModalBody = (
<>
<ForkliftTrans>
<p>Choose the root filesystem to be converted.</p>
<br />
<p>
Default behavior is to choose the first root device in the case of a multi-boot operating
system. Since this is a heuristic, it may sometimes choose the wrong one.
</p>
<br />
<p>
When using a multi-boot VM, you can also name a specific root device, eg.{' '}
<strong>/dev/sda2</strong> would mean to use the second partition on the first hard drive.
If the named root device does not exist or was not detected as a root device, the migration
will fail.{' '}
<ExternalLink isInline href={VIRT_V2V_HELP_LINK}>
Learn more
</ExternalLink>
.
</p>
</ForkliftTrans>
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Type definition for DiskOption.
* @typedef {Object} DiskOption
* @property {string} key - The key representing the disk option.
* @property {string} description - The description of the disk option.
*/
export type DiskOption = {
key: string;
description: string;
};

/**
* Generates an array of disk options.
* @param {Function} [t=(text: string) => text] - Translation function.
* @returns {DiskOption[]} Array of disk options.
*/
export const diskOptions = (t = (text: string) => text): DiskOption[] => [
{ key: '', description: t('Boot from first root device') },
{ key: '/dev/sda', description: t('Boot from the first hard drive') },
{
key: '/dev/sda1',
description: t('Boot from the first partition on the first hard drive'),
},
{
key: '/dev/sda2',
description: t('Boot from the second partition on the first hard drive'),
},
{ key: '/dev/sdb', description: t('Boot from the second hard drive') },
{
key: '/dev/sdb1',
description: t('Boot from the first partition on the second hard drive'),
},
{
key: '/dev/sdb2',
description: t('Boot from the second partition on the second hard drive'),
},
];

/**
* Gets the label for a root disk by its key.
* @param {string | number} key_ - The key representing the disk option.
* @returns {string} The label for the root disk.
*/
export const getRootDiskLabelByKey = (key_: string | number): string => {
const diskLetters = 'abcdefghijklmnopqrstuvwxyz';
const partitionNumbers = '0123456789';

// Default is first root disk
if (!key_) {
return 'First root device';
}

const key = key_.toString();

if (key.startsWith('/dev/sd') && key.length >= 8) {
const diskLetter = key[7];
const partitionNumber = key.length > 8 ? key.slice(8) : '';

const diskIndex = diskLetters.indexOf(diskLetter);
if (diskIndex === -1 || (partitionNumber && !partitionNumbers.includes(partitionNumber[0]))) {
// If format is unrecognized, just return the key as label
return key;
}

const diskPosition = [
'First',
'Second',
'Third',
'Fourth',
'Fifth',
'Sixth',
'Seventh',
'Eighth',
'Ninth',
'Tenth',
][diskIndex];
const partitionPosition = partitionNumber ? `${partitionNumber} partition` : '';

return `${diskPosition} HD${partitionPosition ? ` ${partitionPosition}` : ''} (${key})`;
} else {
// If format is unrecognized, just return the key as label
return key;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @index(['./*', /style/g], f => `export * from '${f.path}';`)
export * from './EditRootDisk';
export * from './editRootDiskModalAlert';
export * from './editRootDiskModalBody';
export * from './getRootDiskLabelByKey';
// @endindex

0 comments on commit a682f81

Please sign in to comment.