diff --git a/.env b/.env
index e3f26d2c4e..c5ee254d0f 100644
--- a/.env
+++ b/.env
@@ -6,4 +6,8 @@ VITE_BASENAME="/r"
VITE_APP_BASENAME=${BASENAME}
VITE_APP_VITE_BASENAME=${VITE_BASENAME}
VITE_APP_WEBSOCKET_DEBUG=false
-VITE_APP_USABILLA_ID=fd6cf482fbbb
\ No newline at end of file
+VITE_APP_USABILLA_ID=fd6cf482fbbb
+
+# Feature flags
+
+VITE_APP_DPU_PROVISIONING=false
\ No newline at end of file
diff --git a/src/app/base/components/NodeActionMenu/NodeActionMenu.tsx b/src/app/base/components/NodeActionMenu/NodeActionMenu.tsx
index aa39280fcf..4216f18d85 100644
--- a/src/app/base/components/NodeActionMenu/NodeActionMenu.tsx
+++ b/src/app/base/components/NodeActionMenu/NodeActionMenu.tsx
@@ -105,6 +105,16 @@ const getTakeActionLinks = (
if (excludeActions.includes(action)) {
return groupLinks;
}
+
+ // Only show "Power cycle" if the feature flag is enabled.
+ // TODO: Remove DPU provisioning feature flag https://warthogs.atlassian.net/browse/MAASENG-4186
+ if (
+ action === NodeActions.POWER_CYCLE &&
+ import.meta.env.VITE_APP_DPU_PROVISIONING !== "true"
+ ) {
+ return groupLinks;
+ }
+
// When nodes are not provided then counts should not be visible.
const count =
nodes?.reduce(
diff --git a/src/app/base/components/NodeActionMenuGroup/NodeActionMenuGroup.tsx b/src/app/base/components/NodeActionMenuGroup/NodeActionMenuGroup.tsx
index 960df88e22..75f337f36a 100644
--- a/src/app/base/components/NodeActionMenuGroup/NodeActionMenuGroup.tsx
+++ b/src/app/base/components/NodeActionMenuGroup/NodeActionMenuGroup.tsx
@@ -65,6 +65,7 @@ const actionGroups: ActionGroup[] = [
actions: [
NodeActions.ON,
NodeActions.OFF,
+ NodeActions.POWER_CYCLE,
NodeActions.SOFT_OFF,
NodeActions.CHECK_POWER,
],
@@ -127,6 +128,15 @@ const generateActionMenus = (
return groupLinks;
}
+ // Only show "Power cycle" if the feature flag is enabled.
+ // TODO: Remove DPU provisioning feature flag https://warthogs.atlassian.net/browse/MAASENG-4186
+ if (
+ action === NodeActions.POWER_CYCLE &&
+ import.meta.env.VITE_APP_DPU_PROVISIONING !== "true"
+ ) {
+ return groupLinks;
+ }
+
if (
singleNode &&
(action === NodeActions.LOCK || action === NodeActions.UNLOCK)
diff --git a/src/app/machines/components/MachineForms/MachineActionFormWrapper/MachineActionFormWrapper.tsx b/src/app/machines/components/MachineForms/MachineActionFormWrapper/MachineActionFormWrapper.tsx
index 1286021af9..b4fe919542 100644
--- a/src/app/machines/components/MachineForms/MachineActionFormWrapper/MachineActionFormWrapper.tsx
+++ b/src/app/machines/components/MachineForms/MachineActionFormWrapper/MachineActionFormWrapper.tsx
@@ -99,97 +99,147 @@ export const MachineActionForm = ({
if (!filter) {
return null;
}
- switch (action) {
- case NodeActions.CLONE:
- return (
-
- );
- case NodeActions.COMMISSION:
- return ;
- case NodeActions.DELETE:
- return (
- {
- dispatchForSelectedMachines(machineActions.delete);
- }}
- redirectURL={urls.machines.index}
- {...commonNodeFormProps}
- />
- );
- case NodeActions.DEPLOY:
- return ;
- case NodeActions.MARK_BROKEN:
- return ;
- case NodeActions.OVERRIDE_FAILED_TESTING:
- return ;
- case NodeActions.RELEASE:
- return ;
- case NodeActions.SET_POOL:
- return ;
- case NodeActions.SET_ZONE:
- return (
-
- onSubmit={(zoneID) => {
- dispatch(machineActions.cleanup());
- dispatchForSelectedMachines(machineActions.setZone, {
- zone_id: zoneID,
- });
- }}
- {...commonNodeFormProps}
- />
- );
- case NodeActions.TAG:
- case NodeActions.UNTAG:
- return ;
- case NodeActions.TEST:
- return (
-
- applyConfiguredNetworking={applyConfiguredNetworking}
- hardwareType={hardwareType}
- onTest={(args) => {
- dispatchForSelectedMachines(machineActions.test, {
- enable_ssh: args.enableSSH,
- script_input: args.scriptInputs,
- testing_scripts: args.scripts.map((script) => script.name),
- });
- }}
- {...commonNodeFormProps}
- />
- );
- case NodeActions.ABORT:
- case NodeActions.ACQUIRE:
- case NodeActions.EXIT_RESCUE_MODE:
- case NodeActions.LOCK:
- case NodeActions.MARK_FIXED:
- case NodeActions.ON:
- case NodeActions.RESCUE_MODE:
- case NodeActions.UNLOCK:
- return (
-
- );
- case NodeActions.OFF:
- case NodeActions.SOFT_OFF:
- return (
-
- );
- // No form should be opened for this, as it should only
- // be available for machine details, and will be dispatched
- // immediately on click.
- case NodeActions.CHECK_POWER:
- return null;
- }
+
+ return formComponents[action]();
+ };
+
+ const formComponents = {
+ [NodeActions.CLONE]: () => (
+
+ ),
+ [NodeActions.COMMISSION]: () => (
+
+ ),
+ [NodeActions.DELETE]: () => (
+ {
+ dispatchForSelectedMachines(machineActions.delete);
+ }}
+ redirectURL={urls.machines.index}
+ {...commonNodeFormProps}
+ />
+ ),
+ [NodeActions.DEPLOY]: () => ,
+ [NodeActions.MARK_BROKEN]: () => (
+
+ ),
+ [NodeActions.OVERRIDE_FAILED_TESTING]: () => (
+
+ ),
+ [NodeActions.RELEASE]: () => ,
+ [NodeActions.SET_POOL]: () => ,
+ [NodeActions.SET_ZONE]: () => (
+
+ onSubmit={(zoneID) => {
+ dispatch(machineActions.cleanup());
+ dispatchForSelectedMachines(machineActions.setZone, {
+ zone_id: zoneID,
+ });
+ }}
+ {...commonNodeFormProps}
+ />
+ ),
+ [NodeActions.TAG]: () => ,
+ [NodeActions.UNTAG]: () => ,
+ [NodeActions.TEST]: () => (
+
+ applyConfiguredNetworking={applyConfiguredNetworking}
+ hardwareType={hardwareType}
+ onTest={(args) => {
+ dispatchForSelectedMachines(machineActions.test, {
+ enable_ssh: args.enableSSH,
+ script_input: args.scriptInputs,
+ testing_scripts: args.scripts.map((script) => script.name),
+ });
+ }}
+ {...commonNodeFormProps}
+ />
+ ),
+ [NodeActions.ABORT]: () => (
+
+ ),
+ [NodeActions.ACQUIRE]: () => (
+
+ ),
+ [NodeActions.EXIT_RESCUE_MODE]: () => (
+
+ ),
+ [NodeActions.LOCK]: () => (
+
+ ),
+ [NodeActions.MARK_FIXED]: () => (
+
+ ),
+ [NodeActions.ON]: () => (
+
+ ),
+ [NodeActions.RESCUE_MODE]: () => (
+
+ ),
+ [NodeActions.UNLOCK]: () => (
+
+ ),
+ [NodeActions.POWER_CYCLE]: () => (
+
+ ),
+ [NodeActions.OFF]: () => (
+
+ ),
+ [NodeActions.SOFT_OFF]: () => (
+
+ ),
+ // No form should be opened for this, as it should only
+ // be available for machine details, and will be dispatched
+ // immediately on click.
+ [NodeActions.CHECK_POWER]: () => null,
};
if (selectedCountLoading) {
diff --git a/src/app/machines/constants.ts b/src/app/machines/constants.ts
index 8719fd00f8..fe843befcd 100644
--- a/src/app/machines/constants.ts
+++ b/src/app/machines/constants.ts
@@ -17,6 +17,7 @@ export const MachineActionSidePanelViews = {
POWER_OFF_MACHINE: ["machineActionForm", NodeActions.OFF],
POWER_OFF_MACHINE_SOFT: ["machineActionForm", NodeActions.SOFT_OFF],
POWER_ON_MACHINE: ["machineActionForm", NodeActions.ON],
+ POWER_CYCLE_MACHINE: ["machineActionForm", NodeActions.POWER_CYCLE],
OVERRIDE_FAILED_TESTING_MACHINE: [
"machineActionForm",
NodeActions.OVERRIDE_FAILED_TESTING,
diff --git a/src/app/store/machine/slice.ts b/src/app/store/machine/slice.ts
index 3918f4ea49..522068b5cc 100644
--- a/src/app/store/machine/slice.ts
+++ b/src/app/store/machine/slice.ts
@@ -1360,6 +1360,11 @@ const machineSlice = createSlice({
[`${NodeActions.RELEASE}Error`]: statusHandlers.release.error,
[`${NodeActions.RELEASE}Start`]: statusHandlers.release.start,
[`${NodeActions.RELEASE}Success`]: statusHandlers.release.success,
+ // TODO: Add actual functionality here once the backend is ready https://warthogs.atlassian.net/browse/MAASENG-4185
+ powerCycle: {
+ prepare: () => ({ payload: null }),
+ reducer: () => {},
+ },
removeRequest: {
prepare: (callId: string) =>
({
diff --git a/src/app/store/types/node.ts b/src/app/store/types/node.ts
index a274c7c1b4..efff2a0aa2 100644
--- a/src/app/store/types/node.ts
+++ b/src/app/store/types/node.ts
@@ -179,6 +179,7 @@ export enum NodeActions {
OFF = "off",
ON = "on",
OVERRIDE_FAILED_TESTING = "override-failed-testing",
+ POWER_CYCLE = "power-cycle", // TODO: Verify string with backend https://warthogs.atlassian.net/browse/MAASENG-4185
RELEASE = "release",
RESCUE_MODE = "rescue-mode",
SET_POOL = "set-pool",
diff --git a/src/app/store/utils/node/base.ts b/src/app/store/utils/node/base.ts
index 7bd156c276..9bc4df85d9 100644
--- a/src/app/store/utils/node/base.ts
+++ b/src/app/store/utils/node/base.ts
@@ -63,15 +63,16 @@ export const getNodeActionTitle = (actionName: NodeActions): string => {
[NodeActions.MARK_FIXED]: "Mark fixed",
[NodeActions.OFF]: "Power off",
[NodeActions.ON]: "Power on",
- [NodeActions.SOFT_OFF]: "Soft power off",
[NodeActions.OVERRIDE_FAILED_TESTING]: "Override failed testing",
+ [NodeActions.POWER_CYCLE]: "Power cycle",
[NodeActions.RELEASE]: "Release",
[NodeActions.RESCUE_MODE]: "Enter rescue mode",
[NodeActions.SET_POOL]: "Set pool",
[NodeActions.SET_ZONE]: "Set zone",
+ [NodeActions.SOFT_OFF]: "Soft power off",
[NodeActions.TAG]: "Tag",
- [NodeActions.UNTAG]: "Untag",
[NodeActions.TEST]: "Test",
+ [NodeActions.UNTAG]: "Untag",
[NodeActions.UNLOCK]: "Unlock",
};
@@ -128,6 +129,10 @@ export const getNodeActionLabel = (
`Override failed tests for ${modelString}`,
`Overriding failed tests for ${modelString}`,
],
+ [NodeActions.POWER_CYCLE]: [
+ `Power cycle ${modelString}`,
+ `Power cycling ${modelString}`,
+ ],
[NodeActions.RELEASE]: [
`Release ${modelString}`,
`Releasing ${modelString}`,
diff --git a/src/app/utils/isValidPortNumber.test.ts b/src/app/utils/isValidPortNumber.test.ts
index c311d856b8..4303d6f680 100644
--- a/src/app/utils/isValidPortNumber.test.ts
+++ b/src/app/utils/isValidPortNumber.test.ts
@@ -1,9 +1,13 @@
import { isValidPortNumber } from ".";
it("returns true for any number between 0 and 65535", () => {
- for (let i = 0; i <= 65535; i++) {
- expect(isValidPortNumber(i)).toBe(true);
- }
+ expect(isValidPortNumber(0)).toBe(true);
+ expect(isValidPortNumber(80)).toBe(true);
+ expect(isValidPortNumber(443)).toBe(true);
+ expect(isValidPortNumber(1234)).toBe(true);
+ expect(isValidPortNumber(5240)).toBe(true);
+ expect(isValidPortNumber(8400)).toBe(true);
+ expect(isValidPortNumber(65535)).toBe(true);
});
it("returns false for numbers larger than 65535", () => {