[machine] Add button to clear the device dimensions.
Change-Id: Ie1839dc064e2c3eb57fe3bea1fa0de348076ba90
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/303001
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/machine/go/machineserver/main.go b/machine/go/machineserver/main.go
index 8b2110f..8fb2f24 100644
--- a/machine/go/machineserver/main.go
+++ b/machine/go/machineserver/main.go
@@ -248,6 +248,42 @@
w.WriteHeader(http.StatusOK)
}
+func (s *server) machineRemoveDeviceHandler(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := strings.TrimSpace(vars["id"])
+ if id == "" {
+ httputils.ReportError(w, skerr.Fmt("ID must be supplied."), "ID must be supplied.", http.StatusInternalServerError)
+ return
+ }
+
+ var ret machine.Description
+ err := s.store.Update(r.Context(), id, func(in machine.Description) machine.Description {
+ ret = in.Copy()
+
+ newDescription := machine.NewDescription()
+ ret.Dimensions = newDescription.Dimensions
+
+ ret.Annotation = machine.Annotation{
+ User: user(r),
+ Message: fmt.Sprintf("Requested device removal"),
+ Timestamp: time.Now(),
+ }
+ return ret
+ })
+ auditlog.Log(r, "remove-device", struct {
+ MachineID string
+ PodName string
+ }{
+ MachineID: id,
+ PodName: ret.PodName,
+ })
+ if err != nil {
+ httputils.ReportError(w, err, "Failed to update machine.", http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
// See baseapp.App.
func (s *server) AddHandlers(r *mux.Router) {
r.HandleFunc("/", s.mainHandler).Methods("GET")
@@ -255,6 +291,7 @@
r.HandleFunc("/_/machine/toggle_mode/{id:.+}", s.machineToggleModeHandler).Methods("GET")
r.HandleFunc("/_/machine/toggle_update/{id:.+}", s.machineToggleUpdateHandler).Methods("GET")
r.HandleFunc("/_/machine/toggle_powercycle/{id:.+}", s.machineTogglePowerCycleHandler).Methods("GET")
+ r.HandleFunc("/_/machine/remove_device/{id:.+}", s.machineRemoveDeviceHandler).Methods("GET")
r.HandleFunc("/loginstatus/", login.StatusHandler).Methods("GET")
}
diff --git a/machine/go/machineserver/main_test.go b/machine/go/machineserver/main_test.go
index 34f9fd0..4289bf2 100644
--- a/machine/go/machineserver/main_test.go
+++ b/machine/go/machineserver/main_test.go
@@ -239,3 +239,70 @@
assert.Equal(t, 404, w.Code)
}
+
+func TestMachineRemoveDeviceHandler_Success(t *testing.T) {
+ unittest.LargeTest(t)
+ *baseapp.Local = true
+
+ ctx, cfg := setupForTest(t)
+ store, err := store.New(ctx, true, cfg)
+ require.NoError(t, err)
+
+ const podName = "rpi-swarming-123456"
+ err = store.Update(ctx, "someid", func(in machine.Description) machine.Description {
+ ret := in.Copy()
+ ret.PodName = podName
+ ret.Dimensions = machine.SwarmingDimensions{
+ "android_devices": {"1"},
+ "device_os": {"Q", " QP1A.190711.020", "QP1A.190711.020_G980FXXU1ATB3"},
+ "device_os_flavor": {"samsung"},
+ }
+ return ret
+ })
+ require.NoError(t, err)
+
+ // Create our server.
+ s := &server{
+ store: store,
+ }
+
+ // Put a mux.Router in place so the request path gets parsed.
+ router := mux.NewRouter()
+ s.AddHandlers(router)
+
+ r := httptest.NewRequest("GET", "/_/machine/remove_device/someid", nil)
+ w := httptest.NewRecorder()
+
+ // Make the request.
+ router.ServeHTTP(w, r)
+
+ // Confirm the request was successful.
+ require.Equal(t, 200, w.Code)
+ machines, err := store.List(ctx)
+ require.NoError(t, err)
+ require.Len(t, machines, 1)
+ assert.Equal(t, podName, machines[0].PodName)
+ // Confirm the dimensions were cleared.
+ assert.Empty(t, machines[0].Dimensions)
+ assert.Contains(t, machines[0].Annotation.Message, "Requested device removal")
+ assert.Equal(t, machines[0].Annotation.User, "barney@example.org")
+}
+
+func TestMachineRemoveDeviceHandler_FailOnMissingID(t *testing.T) {
+ unittest.LargeTest(t)
+
+ // Create our server.
+ s := &server{}
+
+ // Put a mux.Router in place so the request path gets parsed.
+ router := mux.NewRouter()
+ s.AddHandlers(router)
+
+ r := httptest.NewRequest("GET", "/_/machine/remove_device/", nil)
+ w := httptest.NewRecorder()
+
+ // Make the request.
+ router.ServeHTTP(w, r)
+
+ assert.Equal(t, 404, w.Code)
+}
diff --git a/machine/modules/machine-server-sk/machine-server-sk.ts b/machine/modules/machine-server-sk/machine-server-sk.ts
index e55c64c..09d81a7 100644
--- a/machine/modules/machine-server-sk/machine-server-sk.ts
+++ b/machine/modules/machine-server-sk/machine-server-sk.ts
@@ -18,6 +18,7 @@
import '../../../infra-sk/modules/theme-chooser-sk';
import 'elements-sk/error-toast-sk';
import 'elements-sk/icon/cached-icon-sk';
+import 'elements-sk/icon/clear-icon-sk';
import 'elements-sk/icon/pause-icon-sk';
import 'elements-sk/icon/play-arrow-icon-sk';
import 'elements-sk/icon/power-settings-new-icon-sk';
@@ -44,10 +45,12 @@
<table>
${Object.entries(temperatures).map(
(pair) =>
- html`<tr>
- <td>${pair[0]}</td>
- <td>${pair[1]}</td>
- </tr>`
+ html`
+ <tr>
+ <td>${pair[0]}</td>
+ <td>${pair[1]}</td>
+ </tr>
+ `
)}
</table>
</details>
@@ -56,7 +59,9 @@
const isRunning = (machine: Description) =>
machine.RunningSwarmingTask
- ? html`<cached-icon-sk title="Running"></cached-icon-sk>`
+ ? html`
+ <cached-icon-sk title="Running"></cached-icon-sk>
+ `
: '';
const asList = (arr: string[]) => arr.join(' | ');
@@ -66,15 +71,17 @@
return '';
}
return html`
- <details>
+ <details class="dimensions">
<summary>Dimensions</summary>
<table>
${Object.entries(machine.Dimensions).map(
(pair) =>
- html`<tr>
- <td>${pair[0]}</td>
- <td>${asList(pair[1])}</td>
- </tr>`
+ html`
+ <tr>
+ <td>${pair[0]}</td>
+ <td>${asList(pair[1])}</td>
+ </tr>
+ `
)}
</table>
</details>
@@ -91,11 +98,17 @@
`;
};
-const update = (machine: Description) => {
- if (machine.ScheduledForDeletion) {
- return 'Waiting for update.';
- }
- return 'Update';
+const update = (ele: MachineServerSk, machine: Description) => {
+ const msg = machine.ScheduledForDeletion ? 'Waiting for update.' : 'Update';
+ return html`
+ <button
+ title="Force the pod to be killed and re-created"
+ class="update"
+ @click=${() => ele._toggleUpdate(machine.Dimensions.id)}
+ >
+ ${msg}
+ </button>
+ `;
};
const imageName = (machine: Description) => {
@@ -112,51 +125,62 @@
return parts[1];
};
-const powerCycle = (machine: Description) => {
+const powerCycle = (ele: MachineServerSk, machine: Description) => {
if (machine.PowerCycle) {
return 'Waiting for Power Cycle';
}
- return html`<power-settings-new-icon-sk></power-settings-new-icon-sk>`;
+ return html`
+ <power-settings-new-icon-sk
+ title="Powercycle the host"
+ @click=${() => ele._togglePowerCycle(machine.Dimensions.id)}
+ ></power-settings-new-icon-sk>
+ `;
+};
+
+const clearDevice = (ele: MachineServerSk, machine: Description) => {
+ return machine.RunningSwarmingTask
+ ? ''
+ : html`
+ <clear-icon-sk
+ title="Clear the dimensions for the bot"
+ @click=${() => ele._clearDevice(machine.Dimensions.id)}
+ ></clear-icon-sk>
+ `;
+};
+
+const toggleMode = (ele: MachineServerSk, machine: Description) => {
+ return html`
+ <button class="mode" @click=${() => ele._toggleMode(machine.Dimensions.id)}>
+ ${machine.Mode}
+ </button>
+ `;
+};
+
+const machineLink = (machine: Description) => {
+ return html`
+ <a
+ href="https://chromium-swarm.appspot.com/bot?id=${machine.Dimensions.id}"
+ >
+ ${machine.Dimensions.id}
+ </a>
+ `;
};
const rows = (ele: MachineServerSk) =>
ele._machines.map(
(machine) => html`
<tr id=${machine.Dimensions.id}>
- <td>
- <a
- href="https://chromium-swarm.appspot.com/bot?id=${machine.Dimensions
- .id}"
- >${machine.Dimensions.id}</a
- >
- </td>
+ <td>${machineLink(machine)}</td>
<td>${machine.PodName}</td>
<td>${machine.Dimensions.device_type}</td>
- <td>
- <button
- class="mode"
- @click=${() => ele._toggleMode(machine.Dimensions.id)}
- >${machine.Mode}</button
- >
- </td>
- <td>
- <button
- class="update"
- @click=${() => ele._toggleUpdate(machine.Dimensions.id)}
- >${update(machine)}</button
- >
- </td>
- <td
- class="powercycle"
- @click=${() => ele._togglePowerCycle(machine.Dimensions.id)}
- >${powerCycle(machine)}</td
- >
+ <td>${toggleMode(ele, machine)}</td>
+ <td>${update(ele, machine)}</td>
+ <td class="powercycle">${powerCycle(ele, machine)}</td>
+ <td>${clearDevice(ele, machine)}</td>
<td>${machine.Dimensions.quarantined}</td>
<td>${isRunning(machine)}</td>
<td>${machine.Battery}</td>
- <td>
- ${temps(machine.Temperature)}
- </td>
+ <td>${temps(machine.Temperature)}</td>
<td>${diffDate(machine.LastUpdated)}</td>
<td>${dimensions(machine)}</td>
<td>${annotation(machine)}</td>
@@ -167,9 +191,13 @@
const refreshButtonDisplayValue = (ele: MachineServerSk) => {
if (ele.refreshing) {
- return html`<pause-icon-sk></pause-icon-sk>`;
+ return html`
+ <pause-icon-sk></pause-icon-sk>
+ `;
}
- return html`<play-arrow-icon-sk></play-arrow-icon-sk>`;
+ return html`
+ <play-arrow-icon-sk></play-arrow-icon-sk>
+ `;
};
const template = (ele: MachineServerSk) => html`
@@ -178,8 +206,9 @@
id="refresh"
@click=${() => ele._toggleRefresh()}
title="Start/Stop the automatic refreshing of data on the page."
- >${refreshButtonDisplayValue(ele)}</span
>
+ ${refreshButtonDisplayValue(ele)}
+ </span>
<theme-chooser-sk
title="Toggle between light and dark mode."
></theme-chooser-sk>
@@ -193,6 +222,7 @@
<th>Mode</th>
<th>Update</th>
<th>Host</th>
+ <th>Device</th>
<th>Quarantined</th>
<th>Task</th>
<th>Battery</th>
@@ -292,6 +322,17 @@
}
}
+ async _clearDevice(id: string[]) {
+ try {
+ this.setAttribute('waiting', '');
+ await fetch(`/_/machine/remove_device/${id}`);
+ this.removeAttribute('waiting');
+ this._update(true);
+ } catch (error) {
+ this._onError(error);
+ }
+ }
+
async _refreshStep() {
// Wait for _update to finish so we don't pile up requests if server latency
// rises.
diff --git a/machine/modules/machine-server-sk/machine-server-sk_test.ts b/machine/modules/machine-server-sk/machine-server-sk_test.ts
index 6885906..d7c0253 100644
--- a/machine/modules/machine-server-sk/machine-server-sk_test.ts
+++ b/machine/modules/machine-server-sk/machine-server-sk_test.ts
@@ -104,7 +104,7 @@
await fetchMock.flush(true);
// Confirm the button text has been updated.
- assert.equal('maintenance', button.textContent);
+ assert.equal('maintenance', button.textContent?.trim());
}));
});
@@ -173,7 +173,7 @@
await fetchMock.flush(true);
// Confirm the button text has been updated.
- assert.equal('Waiting for update.', button.textContent);
+ assert.equal('Waiting for update.', button.textContent?.trim());
}));
});
@@ -304,14 +304,89 @@
]);
// Click the button.
- const button = s!.querySelector<HTMLButtonElement>('.powercycle')!;
+ const button = s!.querySelector<HTMLElement>(
+ 'power-settings-new-icon-sk'
+ )!;
button.click();
// Wait for all requests to finish.
await fetchMock.flush(true);
// Confirm the button text has been updated.
- assert.equal('Waiting for Power Cycle', button.textContent);
+ assert.equal(
+ 'Waiting for Power Cycle',
+ s!.querySelector('.powercycle')?.textContent?.trim()
+ );
+ }));
+ });
+
+ describe('clears Dimensions on click', () => {
+ fetchMock.get('/_/machines', [
+ {
+ Mode: 'available',
+ Battery: 100,
+ PodName: 'rpi-swarming-123456-987',
+ ScheduledForDeletion: '',
+ Dimensions: {
+ id: ['skia-rpi2-rack4-shelf1-002'],
+ android_devices: ['1'],
+ device_os: ['H', 'HUAWEIELE-L29'],
+ },
+ Annotation: {
+ User: '',
+ Message: '',
+ LastUpdated: '2020-04-21T17:33:09.638275Z',
+ },
+ PowerCycle: false,
+ LastUpdated: '2020-04-21T17:33:09.638275Z',
+ Temperature: { dumpsys_battery: 26 },
+ },
+ ]);
+
+ it('updates PowerCycle when you click on the button', () =>
+ window.customElements.whenDefined('machine-server-sk').then(async () => {
+ container.innerHTML = '<machine-server-sk></machine-server-sk>';
+ const s = container.firstElementChild;
+
+ // Wait for the initial fetch to finish.
+ await fetchMock.flush(true);
+
+ // Confirm there are row in the dimensions.
+ assert.isNotNull(s!.querySelector('details.dimensions table tr'));
+
+ // Now set up fetchMock for the requests that happen when the button is clicked.
+ fetchMock.reset();
+ fetchMock.get(
+ '/_/machine/remove_device/skia-rpi2-rack4-shelf1-002',
+ 200
+ );
+ fetchMock.get('/_/machines', [
+ {
+ Mode: 'maintenance',
+ Battery: 100,
+ PodName: 'rpi-swarming-123456-987',
+ ScheduledForDeletion: 'rpi-swarming-123456-987',
+ Dimensions: {},
+ Annotation: {
+ User: '',
+ Message: '',
+ LastUpdated: '2020-04-21T17:33:09.638275Z',
+ },
+ PowerCycle: true,
+ LastUpdated: '2020-04-21T17:33:09.638275Z',
+ Temperature: { dumpsys_battery: 26 },
+ },
+ ]);
+
+ // Click the button.
+ const button = s!.querySelector<HTMLElement>('clear-icon-sk')!;
+ button.click();
+
+ // Wait for all requests to finish.
+ await fetchMock.flush(true);
+
+ // Confirm the dimensions are now empty.
+ assert.isNull(s!.querySelector('details.dimensions table tr'));
}));
});
});