Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [WD-18264] CMS fields for storage pool source
Browse files Browse the repository at this point in the history
Signed-off-by: Nkeiruka <[email protected]>
Kxiru committed Jan 28, 2025

Verified

This commit was signed with the committer’s verified signature.
fpapon François Papon
1 parent cf5e5d2 commit 151aa39
Showing 11 changed files with 329 additions and 54 deletions.
15 changes: 5 additions & 10 deletions src/api/networks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { handleEtagResponse, handleResponse } from "util/helpers";
import {
constructMemberError,
handleEtagResponse,
handleResponse,
} from "util/helpers";
import type {
LxdNetwork,
LXDNetworkOnClusterMember,
@@ -29,15 +33,6 @@ export const fetchNetworks = (
});
};

const constructMemberError = (
result: PromiseRejectedResult,
member: string,
) => {
const reason = result.reason as Error;
const message = `Error from cluster member ${member}: ${reason.message}`;
return new Error(message);
};

export const fetchNetworksFromClusterMembers = (
project: string,
clusterMembers: LxdClusterMember[],
68 changes: 57 additions & 11 deletions src/api/storage-pools.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
constructMemberError,
handleEtagResponse,
handleResponse,
handleSettledResult,
} from "util/helpers";
import {
LxdStoragePool,
LXDStoragePoolOnClusterMember,
LxdStoragePoolResources,
LxdStorageVolume,
LxdStorageVolumeState,
@@ -14,6 +16,7 @@ import type { LxdApiResponse } from "types/apiResponse";
import type { LxdOperationResponse } from "types/operation";
import axios, { AxiosResponse } from "axios";
import type { LxdClusterMember } from "types/cluster";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";

export const fetchStoragePool = (
pool: string,
@@ -66,7 +69,7 @@ export const createPool = (
});
};

const getClusterAndMemberPools = (pool: Partial<LxdStoragePool>) => {
const getClusterAndMemberPoolPayload = (pool: Partial<LxdStoragePool>) => {
const memberSpecificConfigKeys = new Set([
"source",
"size",
@@ -85,27 +88,38 @@ const getClusterAndMemberPools = (pool: Partial<LxdStoragePool>) => {
}
}

const clusterPool = { ...pool, config: clusterConfig };
const memberPool = { ...pool, config: memberConfig };
const clusterPoolPayload = { ...pool, config: clusterConfig };
const memberPoolPayload = { ...pool, config: memberConfig };

return {
clusterPool,
memberPool,
clusterPoolPayload,
memberPoolPayload,
};
};

export const createClusteredPool = (
pool: LxdStoragePool,
clusterMembers: LxdClusterMember[],
sourcePerClusterMember?: ClusterSpecificValues,
): Promise<void> => {
const { memberPool, clusterPool } = getClusterAndMemberPools(pool);
const { memberPoolPayload, clusterPoolPayload } =
getClusterAndMemberPoolPayload(pool);
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((item) => createPool(memberPool, item.server_name)),
clusterMembers.map((item) => {
const clusteredMemberPool = {
...memberPoolPayload,
config: {
...memberPoolPayload.config,
source: sourcePerClusterMember?.[item.server_name],
},
};
return createPool(clusteredMemberPool, item.server_name);
}),
)
.then(handleSettledResult)
.then(() => {
return createPool(clusterPool);
return createPool(clusterPoolPayload);
})
.then(resolve)
.catch(reject);
@@ -132,15 +146,16 @@ export const updateClusteredPool = (
pool: Partial<LxdStoragePool>,
clusterMembers: LxdClusterMember[],
): Promise<void> => {
const { memberPool, clusterPool } = getClusterAndMemberPools(pool);
const { memberPoolPayload, clusterPoolPayload } =
getClusterAndMemberPoolPayload(pool);
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map(async (item) =>
updatePool(memberPool, item.server_name),
updatePool(memberPoolPayload, item.server_name),
),
)
.then(handleSettledResult)
.then(() => updatePool(clusterPool))
.then(() => updatePool(clusterPoolPayload))
.then(resolve)
.catch(reject);
});
@@ -174,6 +189,37 @@ export const deleteStoragePool = (pool: string): Promise<void> => {
});
};

export const fetchPoolFromClusterMembers = (
poolName: string,
clusterMembers: LxdClusterMember[],
): Promise<LXDStoragePoolOnClusterMember[]> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((member) => {
return fetchStoragePool(poolName, member.server_name);
}),
)
.then((results) => {
const poolOnMembers: LXDStoragePoolOnClusterMember[] = [];
for (let i = 0; i < clusterMembers.length; i++) {
const memberName = clusterMembers[i].server_name;
const result = results[i];
if (result.status === "rejected") {
reject(constructMemberError(result, memberName));
}
if (result.status === "fulfilled") {
const promise = results[
i
] as PromiseFulfilledResult<LxdStoragePool>;
poolOnMembers.push({ ...promise.value, memberName: memberName });
}
}
resolve(poolOnMembers);
})
.catch(reject);
});
};

export const fetchStorageVolumes = (
pool: string,
project: string,
137 changes: 137 additions & 0 deletions src/components/forms/ClusterSpecificInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { FC, Fragment, useEffect, useState } from "react";
import { CheckboxInput, Input } from "@canonical/react-components";
import ResourceLink from "components/ResourceLink";
import FormEditButton from "components/FormEditButton";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";

interface Props {
id: string;
isReadOnly: boolean;
onChange: (value: ClusterSpecificValues) => void;
toggleReadOnly: () => void;
memberNames: string[];
values?: ClusterSpecificValues;
canToggleSpecific?: boolean;
isDefaultSpecific?: boolean;
clusterMemberLinkTarget?: (member: string) => string;
disabled?: boolean;
helpText?: string;
}

const ClusterSpecificInput: FC<Props> = ({
values,
id,
isReadOnly,
memberNames,
onChange,
toggleReadOnly,
canToggleSpecific = true,
isDefaultSpecific = null,
clusterMemberLinkTarget = () => "/ui/cluster",
disabled = false,
helpText,
}) => {
const [isSpecific, setIsSpecific] = useState<boolean | null>(
isDefaultSpecific,
);
const firstValue = Object.values(values ?? {})[0];

useEffect(() => {
const rawValues = Object.values(values ?? {});
if (isSpecific === null && rawValues.length > 0) {
const newDefaultSpecific = rawValues.some(
(item) => item !== rawValues[0],
);
setIsSpecific(newDefaultSpecific);
}
}, [isSpecific, values]);

const setValueForAllMembers = (value: string) => {
const update: ClusterSpecificValues = {};
memberNames.forEach((member) => (update[member] = value));
onChange(update);
};

const setValueForMember = (value: string, member: string) => {
const update = {
...values,
[member]: value,
};
onChange(update);
};

return (
<div className="u-sv3">
{canToggleSpecific && !isReadOnly && (
<CheckboxInput
id={`${id}-same-for-all-toggle`}
label="Same for all cluster members"
checked={!isSpecific}
onChange={() => {
setIsSpecific((val) => !val);
}}
/>
)}
{isSpecific && (
<div className="cluster-specific-input">
{memberNames.map((item) => {
const activeValue = values?.[item];

return (
<Fragment key={item}>
<div className="cluster-specific-member">
<ResourceLink
type="cluster-member"
value={item}
to={clusterMemberLinkTarget(item)}
/>
</div>
<div className="cluster-specific-value">
{isReadOnly ? (
<>
{activeValue}
<FormEditButton toggleReadOnly={toggleReadOnly} />
</>
) : (
<Input
id={
memberNames.indexOf(item) === 0 ? id : `${id}-${item}`
}
type="text"
className="u-no-margin--bottom"
value={activeValue}
onChange={(e) => setValueForMember(e.target.value, item)}
disabled={disabled}
/>
)}
</div>
</Fragment>
);
})}
{helpText && <div className="p-form-help-text">{helpText}</div>}
</div>
)}
{!isSpecific && (
<div>
{isReadOnly ? (
<>
{firstValue}
<FormEditButton toggleReadOnly={toggleReadOnly} />
</>
) : (
<Input
id={id}
type="text"
value={firstValue}
onChange={(e) => setValueForAllMembers(e.target.value)}
disabled={disabled}
help={helpText}
/>
)}
</div>
)}
</div>
);
};

export default ClusterSpecificInput;
7 changes: 6 additions & 1 deletion src/pages/storage/CreateStoragePool.tsx
Original file line number Diff line number Diff line change
@@ -66,7 +66,12 @@ const CreateStoragePool: FC = () => {

const mutation =
clusterMembers.length > 0
? () => createClusteredPool(storagePool, clusterMembers)
? () =>
createClusteredPool(
storagePool,
clusterMembers,
values.sourcePerClusterMember,
)
: () => createPool(storagePool);

mutation()
28 changes: 23 additions & 5 deletions src/pages/storage/EditStoragePool.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FC, useState } from "react";
import { FC, useEffect, useState } from "react";
import { Button, useNotify } from "@canonical/react-components";
import { useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
fetchPoolFromClusterMembers,
fetchStoragePool,
updateClusteredPool,
updatePool,
@@ -49,11 +50,24 @@ const EditStoragePool: FC<Props> = ({ pool }) => {
const controllerState = useState<AbortController | null>(null);
const { data: clusterMembers = [] } = useClusterMembers();
const [version, setVersion] = useState(0);
const isClustered = clusterMembers.length > 0;

if (!project) {
return <>Missing project</>;
}

const { data: poolOnMembers = [], error } = useQuery({
queryKey: [queryKeys.storage, pool.name, queryKeys.cluster],
queryFn: () => fetchPoolFromClusterMembers(pool.name, clusterMembers),
enabled: isClustered,
});

useEffect(() => {
if (error) {
notify.failure("Loading storage pool from cluster members failed", error);
}
}, [error]);

const StoragePoolSchema = Yup.object().shape({
name: Yup.string()
.test(
@@ -67,7 +81,7 @@ const EditStoragePool: FC<Props> = ({ pool }) => {
});

const formik = useFormik<StoragePoolFormValues>({
initialValues: toStoragePoolFormValues(pool),
initialValues: toStoragePoolFormValues(pool, poolOnMembers),
validationSchema: StoragePoolSchema,
enableReinitialize: true,
onSubmit: (values) => {
@@ -95,7 +109,9 @@ const EditStoragePool: FC<Props> = ({ pool }) => {
);
const member = clusterMembers[0]?.server_name ?? undefined;
const updatedPool = await fetchStoragePool(values.name, member);
void formik.setValues(toStoragePoolFormValues(updatedPool));
void formik.setValues(
toStoragePoolFormValues(updatedPool, poolOnMembers),
);
})
.catch((e) => {
notify.failure("Storage pool update failed", e);
@@ -143,7 +159,9 @@ const EditStoragePool: FC<Props> = ({ pool }) => {
appearance="base"
onClick={() => {
setVersion((old) => old + 1);
void formik.setValues(toStoragePoolFormValues(pool));
void formik.setValues(
toStoragePoolFormValues(pool, poolOnMembers),
);
}}
>
Cancel
40 changes: 40 additions & 0 deletions src/pages/storage/forms/StoragePoolClusteredSourceSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Label } from "@canonical/react-components";
import ClusterSpecificInput from "components/forms/ClusterSpecificInput";
import { FormikProps } from "formik";
import { FC } from "react";
import { StoragePoolFormValues } from "./StoragePoolForm";
import { useClusterMembers } from "context/useClusterMembers";

interface Props {
formik: FormikProps<StoragePoolFormValues>;
helpText: string;
}

const StoragePoolClusteredSourceSelector: FC<Props> = ({
formik,
helpText,
}) => {
const { data: clusterMembers = [] } = useClusterMembers();
const memberNames = clusterMembers.map((member) => member.server_name);

return (
<>
<Label forId="sourcePerClusterMember">Source</Label>
<ClusterSpecificInput
values={formik.values.sourcePerClusterMember}
id="sourcePerClusterMember"
isReadOnly={false}
onChange={(value) => {
void formik.setFieldValue("sourcePerClusterMember", value);
}}
canToggleSpecific={formik.values.isCreating}
toggleReadOnly={() => {}}
memberNames={memberNames}
disabled={!formik.values.isCreating}
helpText={helpText}
/>
</>
);
};

export default StoragePoolClusteredSourceSelector;
22 changes: 12 additions & 10 deletions src/pages/storage/forms/StoragePoolForm.tsx
Original file line number Diff line number Diff line change
@@ -41,16 +41,10 @@ import StoragePoolFormZFS from "./StoragePoolFormZFS";
import { useSettings } from "context/useSettings";
import { ensureEditMode } from "util/instanceEdit";
import StoragePoolFormCephFS from "pages/storage/forms/StoragePoolFormCephFS";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";

export interface StoragePoolFormValues {
isCreating: boolean;
readOnly: boolean;
name: string;
description: string;
driver: string;
source: string;
entityType: "storagePool";
size?: string;
barePool?: LxdStoragePool;
ceph_cluster_name?: string;
ceph_osd_pg_num?: string;
ceph_rbd_clone_copy?: string;
@@ -62,6 +56,11 @@ export interface StoragePoolFormValues {
cephfs_osd_pg_num?: string;
cephfs_path?: string;
cephfs_user_name?: string;
description: string;
driver: string;
entityType: "storagePool";
isCreating: boolean;
name: string;
powerflex_clone_copy?: string;
powerflex_domain?: string;
powerflex_gateway?: string;
@@ -75,11 +74,14 @@ export interface StoragePoolFormValues {
pure_gateway?: string;
pure_gateway_verify?: string;
pure_mode?: string;
readOnly: boolean;
size?: string;
source: string;
sourcePerClusterMember?: ClusterSpecificValues;
yaml?: string;
zfs_clone_copy?: string;
zfs_export?: string;
zfs_pool_name?: string;
yaml?: string;
barePool?: LxdStoragePool;
}

interface Props {
39 changes: 23 additions & 16 deletions src/pages/storage/forms/StoragePoolFormMain.tsx
Original file line number Diff line number Diff line change
@@ -20,6 +20,8 @@ import { useSettings } from "context/useSettings";
import ScrollableForm from "components/ScrollableForm";
import { ensureEditMode } from "util/instanceEdit";
import { optionTrueFalse } from "util/instanceOptions";
import StoragePoolClusteredSourceSelector from "./StoragePoolClusteredSourceSelector";
import { isClusteredServer } from "util/settings";

interface Props {
formik: FormikProps<StoragePoolFormValues>;
@@ -46,6 +48,12 @@ const StoragePoolFormMain: FC<Props> = ({ formik }) => {
const isPowerFlexDriver = formik.values.driver === powerFlex;
const isPureDriver = formik.values.driver === pureStorage;
const storageDriverOptions = getStorageDriverOptions(settings);
const hasClusterWideSource = isCephDriver || isCephFSDriver;
const hasSource = !isPureDriver && !isPowerFlexDriver;

const helpText = formik.values.isCreating
? getSourceHelpForDriver(formik.values.driver)
: "Source can't be changed";

return (
<ScrollableForm>
@@ -124,22 +132,21 @@ const StoragePoolFormMain: FC<Props> = ({ formik }) => {
disabled={formik.values.driver === dirDriver}
/>
)}
{!isPureDriver && !isPowerFlexDriver && (
<Input
{...getFormProps("source")}
type="text"
disabled={
formik.values.driver === btrfsDriver ||
!formik.values.isCreating
}
help={
formik.values.isCreating
? getSourceHelpForDriver(formik.values.driver)
: "Source can't be changed"
}
label="Source"
/>
)}
{hasSource &&
(hasClusterWideSource || !isClusteredServer(settings) ? (
<Input
{...getFormProps("source")}
type="text"
disabled={!formik.values.isCreating}
help={helpText}
label="Source"
/>
) : (
<StoragePoolClusteredSourceSelector
formik={formik}
helpText={helpText}
/>
))}
{isPowerFlexDriver && (
<>
<Input
4 changes: 4 additions & 0 deletions src/types/storage.d.ts
Original file line number Diff line number Diff line change
@@ -93,3 +93,7 @@ export interface LxdVolumeSnapshot {
expires_at?: string;
description?: string;
}

export type LXDStoragePoolOnClusterMember = LxdStoragePool & {
memberName: string;
};
9 changes: 9 additions & 0 deletions src/util/helpers.tsx
Original file line number Diff line number Diff line change
@@ -351,3 +351,12 @@ export const base64EncodeObject = (data: object) => {
const jsonString = JSON.stringify(data);
return btoa(jsonString);
};

export const constructMemberError = (
result: PromiseRejectedResult,
member: string,
) => {
const reason = result.reason as Error;
const message = `Error from cluster member ${member}: ${reason.message}`;
return new Error(message);
};
14 changes: 13 additions & 1 deletion src/util/storagePoolForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import type { LxdStoragePool } from "types/storage";
import type {
LxdStoragePool,
LXDStoragePoolOnClusterMember,
} from "types/storage";
import { StoragePoolFormValues } from "pages/storage/forms/StoragePoolForm";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";

export const toStoragePoolFormValues = (
pool: LxdStoragePool,
poolOnMembers?: LXDStoragePoolOnClusterMember[],
): StoragePoolFormValues => {
const sourcePerClusterMember: ClusterSpecificValues = {};
poolOnMembers?.forEach(
(item) =>
(sourcePerClusterMember[item.memberName] = item.config?.source ?? ""),
);

return {
readOnly: true,
isCreating: false,
@@ -40,6 +51,7 @@ export const toStoragePoolFormValues = (
zfs_clone_copy: pool.config?.["zfs.clone_copy"],
zfs_export: pool.config?.["zfs.export"],
zfs_pool_name: pool.config?.["zfs.pool_name"],
sourcePerClusterMember,
barePool: pool,
};
};

0 comments on commit 151aa39

Please sign in to comment.