Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro";

import { StatusMessage } from "components/status/statusMessage";

import { ParallelMutationError, SyncCallErrors } from "utils/meshWideSyncCall";
import { RemoteNodeCallError, SyncCallErrors } from "utils/meshWideSyncCall";

export const ParallelErrors = ({ errors }: { errors: SyncCallErrors }) => {
return (
Expand All @@ -13,7 +13,7 @@ export const ParallelErrors = ({ errors }: { errors: SyncCallErrors }) => {
</StatusMessage>
<div className={"flex flex-col gap-3 mt-4 overflow-auto gap-4"}>
{errors.map((error, key) => {
if (error instanceof ParallelMutationError) {
if (error instanceof RemoteNodeCallError) {
return (
<div key={key}>
<strong>{error.ip}</strong>: {error.message}
Expand Down
34 changes: 29 additions & 5 deletions plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeApi.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
MeshWideRPCReturnTypes,
MeshWideUpgradeInfo,
NodeMeshUpgradeInfo,
} from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes";
import {
MeshUpgradeApiError,
callToRemoteNode,
meshUpgradeApiCall,
} from "plugins/lime-plugin-mesh-wide-upgrade/src/utils/api";

import api from "utils/uhttpd.service";
import api, { UhttpdService } from "utils/uhttpd.service";

export const getMeshWideUpgradeInfo = async () => {
const res = await api.call("shared-state-async", "get", {
Expand Down Expand Up @@ -42,11 +43,34 @@ export const setAbort = async () => {
};

// Remote API calls

export async function remoteScheduleUpgrade({ ip }: { ip: string }) {
return await callToRemoteNode({ ip, apiMethod: "start_safe_upgrade" });
return await callToRemoteNode({
ip,
apiCall: (customApi) =>
meshUpgradeApiCall("start_safe_upgrade", customApi),
});
}

export async function remoteConfirmUpgrade({ ip }: { ip: string }) {
return await callToRemoteNode({ ip, apiMethod: "confirm_boot_partition" });
return await callToRemoteNode({
ip,
apiCall: (customApi) =>
meshUpgradeApiCall("confirm_boot_partition", customApi),
});
}

const meshUpgradeApiCall = async (
method: string,
customApi?: UhttpdService
) => {
const httpService = customApi || api;
const res = (await httpService.call(
"lime-mesh-upgrade",
method,
{}
)) as MeshWideRPCReturnTypes;
if (res.error) {
throw new MeshUpgradeApiError(res.error, res.code);
}
return res.code;
};
85 changes: 36 additions & 49 deletions plugins/lime-plugin-mesh-wide-upgrade/src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
import {
MeshUpgradeApiErrorTypes,
MeshWideRPCReturnTypes,
MeshWideUpgradeInfo,
UpgradeStatusType,
} from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes";

import { ParallelMutationError } from "utils/meshWideSyncCall";
import { RemoteNodeCallError } from "utils/meshWideSyncCall";
import { login } from "utils/queries";
import api, { UhttpdService } from "utils/uhttpd.service";

export const meshUpgradeApiCall = async (
method: string,
customApi?: UhttpdService
) => {
const httpService = customApi || api;
const res = (await httpService.call(
"lime-mesh-upgrade",
method,
{}
)) as MeshWideRPCReturnTypes;
if (res.error) {
throw new MeshUpgradeApiError(res.error, res.code);
}
return res.code;
};
import { UhttpdService } from "utils/uhttpd.service";

export class MeshUpgradeApiError extends Error {
message: string;
Expand All @@ -38,61 +21,65 @@ export class MeshUpgradeApiError extends Error {
}

/**
* Wrapper that tries to call a remote node and returns the result or throws an error
* From a MeshWideUpgradeInfo nodes it returns the ips of the nodes that are in certain status provided
* @param nodes the nodes to check
* @param status the status to check the criteria
*/
export const getNodeIpsByStatus = (
nodes: MeshWideUpgradeInfo,
status: UpgradeStatusType
) => {
if (!nodes) return [];
return Object.values(nodes)
.filter(
(node) =>
node.node_ip !== null &&
node.node_ip !== undefined &&
node.node_ip.trim() !== "" &&
node.upgrade_state === status
)
.map((node) => node.node_ip as string); // 'as string' is safe here due to the filter condition
};

/**
* Wrapper to do calls to a remote node and returns the result or throws an error
*
* First it tries to login and if success do a specific call to the remote node
* @param ip
* @param apiMethod
*/
export async function callToRemoteNode({
export async function callToRemoteNode<T>({
ip,
apiMethod,
apiCall,
username = "lime-app",
password = "generic",
}: {
ip: string;
apiMethod: string;
apiCall: (customApi: UhttpdService) => Promise<T>;
username?: string;
password?: string;
}) {
const customApi = new UhttpdService(ip);
try {
await login({ username: "lime-app", password: "generic", customApi });
await login({ username, password, customApi });
} catch (error) {
throw new ParallelMutationError(
throw new RemoteNodeCallError(
`Cannot login`,
customApi.customIp,
error
);
}
try {
return await meshUpgradeApiCall(apiMethod, customApi);
return await apiCall(customApi);
} catch (error) {
let additionalInfo = "";
if (error instanceof MeshUpgradeApiError) {
additionalInfo = `: ${error.message}`;
}
throw new ParallelMutationError(
`Cannot startSafeUpgrade${additionalInfo}`,
throw new RemoteNodeCallError(
`Cannot do remote call${additionalInfo}`,
ip,
error
);
}
}

/**
* From a MeshWideUpgradeInfo nodes it returns the ips of the nodes that are in certain status provided
* @param nodes the nodes to check
* @param status the status to check the criteria
*/
export const getNodeIpsByStatus = (
nodes: MeshWideUpgradeInfo,
status: UpgradeStatusType
) => {
if (!nodes) return [];
return Object.values(nodes)
.filter(
(node) =>
node.node_ip !== null &&
node.node_ip !== undefined &&
node.node_ip.trim() !== "" &&
node.upgrade_state === status
)
.map((node) => node.node_ip as string); // 'as string' is safe here due to the filter condition
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Trans } from "@lingui/macro";

import { StatusAndButton } from "plugins/lime-plugin-mesh-wide/src/components/Components";
import RemoteRebootBtn from "plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/RebootNodeBtn";
import { useSetReferenceState } from "plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/SetReferenceStateBtn";
import {
Row,
Expand Down Expand Up @@ -39,10 +40,7 @@ const NodeDetails = ({ actual, reference, name }: NodeMapFeature) => {
<div>
<Row>
<div className={"text-3xl"}>{name}</div>
{/*todo(kon): implement safe_reboot*/}
{/*<Button color={"danger"} outline={true} size={"sm"}>*/}
{/* <PowerIcon />*/}
{/*</Button>*/}
<RemoteRebootBtn node={nodeToShow} />
</Row>
<Row>
{!isDown ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Trans } from "@lingui/macro";
import { useMutation } from "@tanstack/react-query";
import { useEffect, useState } from "preact/hooks";
import { useCallback } from "react";

import { useModal } from "components/Modal/Modal";
import { Button } from "components/buttons/button";
import { ErrorMsg } from "components/form";
import Loading from "components/loading";

import { callToRemoteNode } from "plugins/lime-plugin-mesh-wide-upgrade/src/utils/api";
import { PowerIcon } from "plugins/lime-plugin-mesh-wide/src/icons/power";
import { INodeInfo } from "plugins/lime-plugin-mesh-wide/src/meshWideTypes";

interface IRemoteRebotProps {
ip: string;
password: string;
}

export async function remoteReboot({ ip, password }: IRemoteRebotProps) {
return await callToRemoteNode({
ip,
apiCall: (customApi) =>
customApi.call("system", "reboot", {}).then(() => true),
username: "root",
password,
});
}

const useRemoteReboot = (opts?) => {
return useMutation((props: IRemoteRebotProps) => remoteReboot(props), {
mutationKey: ["system", "reboot"],
...opts,
});
};

const useRebootNodeModal = ({ node }: { node: INodeInfo }) => {
const { toggleModal, setModalState, isModalOpen } = useModal();
const [password, setPassword] = useState("");
const { mutate, isLoading, error } = useRemoteReboot({
onSuccess: () => {
toggleModal();
},
});

function changePassword(e) {
setPassword(e.target.value || "");
}

const doLogin = useCallback(() => {
mutate({ ip: node.ipv4, password });
}, [mutate, node.ipv4, password]);

const updateModalState = useCallback(() => {
setModalState({
title: <Trans>Reboot node {node.hostname}</Trans>,
content: (
<div>
<Trans>
Are you sure you want to reboot this node? This action
will disconnect the node from the network for a few
minutes. <br />
Add shared password or let it empty if no password is
set.
</Trans>
{isLoading && <Loading />}
{!isLoading && (
<div className={"mt-4"}>
<label htmlFor={"password"}>Node password</label>
<input
type="password"
id={"password"}
value={password}
onInput={changePassword}
/>
{error && (
<ErrorMsg>
<Trans>
Error performing reboot: {error}
</Trans>
</ErrorMsg>
)}
</div>
)}
</div>
),
successCb: doLogin,
successBtnText: <Trans>Reboot</Trans>,
});
}, [doLogin, error, isLoading, node.hostname, password, setModalState]);

const rebootModal = useCallback(() => {
updateModalState();
toggleModal();
}, [toggleModal, updateModalState]);

// Update modal state with mutation result
useEffect(() => {
if (isModalOpen) {
updateModalState();
}
}, [isLoading, error, isModalOpen, updateModalState]);

return { rebootModal, toggleModal, isModalOpen };
};

const RemoteRebootBtn = ({ node }: { node: INodeInfo }) => {
const { rebootModal, isModalOpen } = useRebootNodeModal({
node,
});

return (
<Button
color={"danger"}
outline={true}
size={"sm"}
onClick={() => !isModalOpen && rebootModal()}
>
<PowerIcon />
</Button>
);
};

export default RemoteRebootBtn;
2 changes: 1 addition & 1 deletion src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const Modal = ({

<div
onClick={stopPropagation}
className="flex flex-col px-6 justify-between w-full md:w-10/12 h-96 md:mx-24 self-center bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all "
className="flex flex-col px-6 justify-between w-full min-h-96 md:w-10/12 md:mx-24 self-center bg-white rounded-lg overflow-auto text-left shadow-xl transform transition-all "
>
<div className="bg-white pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="mt-3 text-start sm:mt-0 sm:text-left">
Expand Down
2 changes: 1 addition & 1 deletion src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const App = () => {
}, [session, login]);

if (!session?.username || !boardData) {
return <div>"Loading..."</div>;
return <div>Loading...</div>;
}

return (
Expand Down
Loading