Skip to content

Commit

Permalink
Network clustering (#1053)
Browse files Browse the repository at this point in the history
## Done

- adjust network list to show cluster member specific networks
- split network list entries and detail pages to be one entry/page per
cluster member for physical interfaces
- add pattern for cluster specific inputs for a physical managed
networks parent. This is to be reused for other cluster specific inputs
like in server settings or for storage pool configuration


## QA

1. Run the LXD-UI:
- On the demo server via the link posted by @webteam-app below. This is
only available for PRs created by collaborators of the repo. Ask
@mas-who or @edlerd for access.
- With a local copy of this branch, [build and run as described in the
docs](../CONTRIBUTING.md#setting-up-for-development).
2. Perform the following QA steps:
- Browse the network list in an unclustered backend, check the filters
- Browse the network list in a clustered backend, use the filters and
clicking on the cluster member chips applies filtering
    - Create and edit a physical network in an unclustered backend
- Create and edit a physical network in a clustered backend, ensure the
connections diagram is updating and the chips in it linking correctly.
Ensure the chips in the "parent" selector link correctly. Try changing
and breaking the parent selector in the clustered backend when creating
or editing a physical network.
- Browse a physical unmanaged network in a clustered and unclustered
backend
  • Loading branch information
edlerd authored Jan 23, 2025
2 parents 8fbc8fc + af89f91 commit 8964b14
Show file tree
Hide file tree
Showing 38 changed files with 1,295 additions and 614 deletions.
4 changes: 0 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,12 @@
"@monaco-editor/react": "4.6.0",
"@tanstack/react-query": "5.63.0",
"axios": "1.7.9",
"cytoscape": "3.30.4",
"cytoscape-popper": "4.0.1",
"formik": "2.4.6",
"js-yaml": "4.1.0",
"lodash.isequal": "4.5.0",
"node-forge": "1.3.1",
"parse-prometheus-text-format": "1.1.1",
"react": "18.3.1",
"react-cytoscapejs": "2.0.0",
"react-dom": "18.3.1",
"react-router-dom": "7.1.1",
"react-useportal": "1.0.19",
Expand All @@ -59,7 +56,6 @@
"@canonical/typescript-config-react": "0.4.0-experimental.0",
"@playwright/test": "1.49.1",
"@types/convert-source-map": "2.0.3",
"@types/cytoscape-popper": "2.0.4",
"@types/dotenv": "8.2.3",
"@types/lodash.isequal": "4.5.8",
"@types/node-forge": "1.3.11",
Expand Down
17 changes: 8 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const InstanceList = lazy(() => import("pages/instances/InstanceList"));
const Login = lazy(() => import("pages/login/Login"));
const NetworkDetail = lazy(() => import("pages/networks/NetworkDetail"));
const NetworkList = lazy(() => import("./pages/networks/NetworkList"));
const NetworkMap = lazy(() => import("pages/networks/NetworkMap"));
const OperationList = lazy(() => import("pages/operations/OperationList"));
const ProfileDetail = lazy(() => import("pages/profiles/ProfileDetail"));
const ProfileList = lazy(() => import("pages/profiles/ProfileList"));
Expand Down Expand Up @@ -219,42 +218,42 @@ const App: FC = () => {
}
/>
<Route
path="/ui/project/:project/network/:name/:activeTab"
path="/ui/project/:project/member/:member/network/:name"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<NetworkDetail />} />}
/>
}
/>
<Route
path="/ui/project/:project/network/:name/:activeTab/:section"
path="/ui/project/:project/network/:name/:activeTab"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<NetworkDetail />} />}
/>
}
/>
<Route
path="/ui/project/:project/network/:network/forwards/create"
path="/ui/project/:project/network/:name/:activeTab/:section"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<CreateNetworkForward />} />}
outlet={<ProjectLoader outlet={<NetworkDetail />} />}
/>
}
/>
<Route
path="/ui/project/:project/network/:network/forwards/:forwardAddress/edit"
path="/ui/project/:project/network/:network/forwards/create"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<EditNetworkForward />} />}
outlet={<ProjectLoader outlet={<CreateNetworkForward />} />}
/>
}
/>
<Route
path="/ui/project/:project/networks/map"
path="/ui/project/:project/network/:network/forwards/:forwardAddress/edit"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<NetworkMap />} />}
outlet={<ProjectLoader outlet={<EditNetworkForward />} />}
/>
}
/>
Expand Down
167 changes: 144 additions & 23 deletions src/api/networks.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { handleEtagResponse, handleResponse } from "util/helpers";
import type { LxdNetwork, LxdNetworkState } from "types/network";
import type {
LxdNetwork,
LXDNetworkOnClusterMember,
LxdNetworkState,
} from "types/network";
import type { LxdApiResponse } from "types/apiResponse";
import type { LxdClusterMember } from "types/cluster";
import { areNetworksEqual } from "util/networks";
import type { ClusterSpecificValues } from "components/ClusterSpecificSelect";
import type { LxdClusterMember } from "types/cluster";

export const fetchNetworks = (project: string): Promise<LxdNetwork[]> => {
export const fetchNetworks = (
project: string,
target?: string,
): Promise<LxdNetwork[]> => {
const targetParam = target ? `&target=${target}` : "";
return new Promise((resolve, reject) => {
fetch(`/1.0/networks?project=${project}&recursion=1`)
fetch(`/1.0/networks?project=${project}&recursion=1${targetParam}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdNetwork[]>) => {
const filteredNetworks = data.metadata.filter(
Expand All @@ -20,24 +29,97 @@ export const fetchNetworks = (project: string): Promise<LxdNetwork[]> => {
});
};

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[],
): Promise<LXDNetworkOnClusterMember[]> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((member) => {
return fetchNetworks(project, member.server_name);
}),
)
.then((results) => {
const networksOnMembers: LXDNetworkOnClusterMember[] = [];
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") {
result.value.forEach((network) =>
networksOnMembers.push({ ...network, memberName }),
);
}
}
resolve(networksOnMembers);
})
.catch(reject);
});
};

export const fetchNetwork = (
name: string,
project: string,
target?: string,
): Promise<LxdNetwork> => {
const targetParam = target ? `&target=${target}` : "";
return new Promise((resolve, reject) => {
fetch(`/1.0/networks/${name}?project=${project}`)
fetch(`/1.0/networks/${name}?project=${project}${targetParam}`)
.then(handleEtagResponse)
.then((data) => resolve(data as LxdNetwork))
.catch(reject);
});
};

export const fetchNetworkFromClusterMembers = (
name: string,
project: string,
clusterMembers: LxdClusterMember[],
): Promise<LXDNetworkOnClusterMember[]> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((member) => {
return fetchNetwork(name, project, member.server_name);
}),
)
.then((results) => {
const networkOnMembers: LXDNetworkOnClusterMember[] = [];
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<LxdNetwork>;
networkOnMembers.push({ ...promise.value, memberName: memberName });
}
}
resolve(networkOnMembers);
})
.catch(reject);
});
};

export const fetchNetworkState = (
name: string,
project: string,
target?: string,
): Promise<LxdNetworkState> => {
const targetParam = target ? `&target=${target}` : "";
return new Promise((resolve, reject) => {
fetch(`/1.0/networks/${name}/state?project=${project}`)
fetch(`/1.0/networks/${name}/state?project=${project}${targetParam}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdNetworkState>) => resolve(data.metadata))
.catch(reject);
Expand All @@ -48,20 +130,19 @@ export const createClusterNetwork = (
network: Partial<LxdNetwork>,
project: string,
clusterMembers: LxdClusterMember[],
parentsPerClusterMember?: ClusterSpecificValues,
): Promise<void> => {
return new Promise((resolve, reject) => {
const memberNetwork = {
name: network.name,
description: network.description,
type: network.type,
config: {
parent: network.config?.parent,
},
};

Promise.allSettled(
clusterMembers.map(async (member) => {
await createNetwork(memberNetwork, project, member.server_name);
clusterMembers.map((member) => {
const memberNetwork = {
name: network.name,
type: network.type,
config: {
parent: parentsPerClusterMember?.[member.server_name],
},
};
return createNetwork(memberNetwork, project, member.server_name);
}),
)
.then((results) => {
Expand All @@ -72,6 +153,7 @@ export const createClusterNetwork = (
reject(error);
return;
}

// The network parent is cluster member specific, so we omit it on the cluster wide network configuration.
delete network.config?.parent;
createNetwork(network, project).then(resolve).catch(reject);
Expand Down Expand Up @@ -110,15 +192,20 @@ export const createNetwork = (
export const updateNetwork = (
network: Partial<LxdNetwork> & Required<Pick<LxdNetwork, "config">>,
project: string,
target?: string,
): Promise<void> => {
return new Promise((resolve, reject) => {
fetch(`/1.0/networks/${network.name ?? ""}?project=${project}`, {
method: "PUT",
body: JSON.stringify(network),
headers: {
"If-Match": network.etag ?? "invalid-etag",
const targetParam = target ? `&target=${target}` : "";
fetch(
`/1.0/networks/${network.name ?? ""}?project=${project}${targetParam}`,
{
method: "PUT",
body: JSON.stringify(network),
headers: {
"If-Match": network.etag ?? "",
},
},
})
)
.then(handleResponse)
.then(resolve)
.catch(async (e: Error) => {
Expand All @@ -135,6 +222,40 @@ export const updateNetwork = (
});
};

export const updateClusterNetwork = (
network: Partial<LxdNetwork> & Required<Pick<LxdNetwork, "config">>,
project: string,
parentsPerClusterMember: ClusterSpecificValues,
): Promise<void> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
Object.keys(parentsPerClusterMember).map((memberName) => {
const memberNetwork = {
name: network.name,
type: network.type,
config: {
parent: parentsPerClusterMember[memberName],
},
};
return updateNetwork(memberNetwork, project, memberName);
}),
)
.then((results) => {
const error = results.find((res) => res.status === "rejected")
?.reason as Error | undefined;

if (error) {
reject(error);
return;
}
updateNetwork({ ...network, etag: "" }, project)
.then(resolve)
.catch(reject);
})
.catch(reject);
});
};

export const renameNetwork = (
oldName: string,
newName: string,
Expand Down
Loading

0 comments on commit 8964b14

Please sign in to comment.