Skip to content

Commit e8861fa

Browse files
committed
feat: Add Reboot & Reset action to server
1 parent 920c7eb commit e8861fa

File tree

7 files changed

+161
-7
lines changed

7 files changed

+161
-7
lines changed

src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ public void propertyChange(PropertyChangeEvent evt) {
3939
logger.info("Server power on has been called. Instructing Hetzner to power up the server");
4040
HetznerCloud.getInstance().getServiceManager().getServerService().powerOnServer(server);
4141
}
42+
case "reboot" -> {
43+
logger.info("Server reboot has been called. Instructing Hetzner to reboot the server");
44+
HetznerCloud.getInstance().getServiceManager().getServerService().rebootServer(server);
45+
}
46+
case "reset" -> {
47+
logger.info("Server reset has been called. Instructing Hetzner to reset the server");
48+
logger.warn("This is a potentially destructive action!");
49+
HetznerCloud.getInstance().getServiceManager().getServerService().resetServer(server);
50+
}
4251
default -> {
4352
logger.info("Server changed: {}", evt.getPropertyName());
4453
logger.info("Server: {} -> {}", evt.getOldValue(), evt.getNewValue());

src/main/java/dev/tomr/hcloud/resources/server/Server.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ public void delete() {
107107
}
108108

109109
/**
110-
* Sends a Shutdown signal to the server, which will instruct the OS to shut it down. Note that if you <b>must</b>> ensure the server is completely offline, you should also call .powerOff() to ensure the 'plug is pulled'
110+
* Sends a Shutdown request to the server by sending an ACPI request, which will instruct the OS to shut it down. Note that if you <b>must</b>> ensure the server is completely offline, you should also call .powerOff() to ensure the 'plug is pulled'.
111+
* The server OS must support ACPI
111112
*/
112113
public void shutdown() {
113114
propertyChangeSupport.firePropertyChange("shutdown", null, null);
@@ -121,12 +122,25 @@ public void powerOff() {
121122
}
122123

123124
/**
124-
* Starts the Server by turning it's power on
125+
* Starts the Server by turning its power on
125126
*/
126127
public void powerOn() {
127128
propertyChangeSupport.firePropertyChange("poweron", null, null);
128129
}
129130

131+
/**
132+
* Sends a reboot request to the server by sending an ACPI request. The server OS must support ACPI
133+
*/
134+
public void reboot() {
135+
propertyChangeSupport.firePropertyChange("reboot", null, null);
136+
}
137+
138+
/**
139+
* Cuts power to the server and starts it again. Forcefully stops the server without giving the OS time to shut down. Should only be used if reboot does not work.
140+
*/
141+
public void reset() {
142+
propertyChangeSupport.firePropertyChange("reset", null, null);
143+
}
130144

131145
// These are the current setters that will send an API request (PUT /servers) when actions begin to be added, they will also likely be triggered by setters
132146

src/main/java/dev/tomr/hcloud/service/action/Action.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
public enum Action {
44
SHUTDOWN("shutdown"),
55
POWEROFF("poweroff"),
6-
POWERON("poweron");
6+
POWERON("poweron"),
7+
REBOOT("reboot"),
8+
RESET("reset"),;
79

810
public final String path;
911

src/main/java/dev/tomr/hcloud/service/server/ServerService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ public void powerOnServer(Server server) {
139139
sendServerAction(server, POWERON);
140140
}
141141

142+
public void rebootServer(Server server) {
143+
sendServerAction(server, REBOOT);
144+
}
145+
146+
public void resetServer(Server server) {
147+
sendServerAction(server, RESET);
148+
}
149+
142150
private void sendServerAction(Server server, dev.tomr.hcloud.service.action.Action givenAction) {
143151
List<String> hostAndKey = HetznerCloud.getInstance().getHttpDetails();
144152
String httpUrl = String.format("%sservers/%d/actions/%s", hostAndKey.get(0), server.getId(), givenAction.path);

src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,6 @@ void callingSetNameSendsAnEventToTheServerChangeListener() {
193193
assertNull(captor.getValue().getOldValue());
194194
assertEquals("test", captor.getValue().getNewValue());
195195
}
196-
197196
}
198197

199198
@Test
@@ -219,7 +218,6 @@ void callingDeleteSendsAnEventToTheServerChangeListener() {
219218
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
220219
assertEquals("delete", captor.getValue().getPropertyName());
221220
}
222-
223221
}
224222

225223
@Test
@@ -245,7 +243,6 @@ void callingShutdownSendsAnEventToTheServerChangeListener() {
245243
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
246244
assertEquals("shutdown", captor.getValue().getPropertyName());
247245
}
248-
249246
}
250247

251248
@Test
@@ -271,7 +268,6 @@ void callingPoweroffSendsAnEventToTheServerChangeListener() {
271268
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
272269
assertEquals("poweroff", captor.getValue().getPropertyName());
273270
}
274-
275271
}
276272

277273
@Test
@@ -297,6 +293,55 @@ void callingPowerOnSendsAnEventToTheServerChangeListener() {
297293
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
298294
assertEquals("poweron", captor.getValue().getPropertyName());
299295
}
296+
}
297+
@Test
298+
@DisplayName("calling reboot sends an event to the ServerChangeListener")
299+
void callingRebootSendsAnEventToTheServerChangeListener() {
300+
try (MockedStatic<HetznerCloud> hetznerCloud = mockStatic(HetznerCloud.class)) {
301+
HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
302+
ServerChangeListener scl = new ServerChangeListener();
303+
ServerChangeListener serverChangeListener = spy(scl);
304+
ListenerManager listenerManager = mock(ListenerManager.class);
305+
ServiceManager serviceManager = mock(ServiceManager.class);
306+
ServerService serverService = mock(ServerService.class);
307+
ArgumentCaptor<PropertyChangeEvent> captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);
300308

309+
hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
310+
when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
311+
when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
312+
when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
313+
when(serviceManager.getServerService()).thenReturn(serverService);
314+
315+
Server server = new Server();
316+
server.reboot();
317+
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
318+
assertEquals("reboot", captor.getValue().getPropertyName());
319+
}
301320
}
321+
322+
@Test
323+
@DisplayName("calling reset sends an event to the ServerChangeListener")
324+
void callingResetSendsAnEventToTheServerChangeListener() {
325+
try (MockedStatic<HetznerCloud> hetznerCloud = mockStatic(HetznerCloud.class)) {
326+
HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
327+
ServerChangeListener scl = new ServerChangeListener();
328+
ServerChangeListener serverChangeListener = spy(scl);
329+
ListenerManager listenerManager = mock(ListenerManager.class);
330+
ServiceManager serviceManager = mock(ServiceManager.class);
331+
ServerService serverService = mock(ServerService.class);
332+
ArgumentCaptor<PropertyChangeEvent> captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);
333+
334+
hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
335+
when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
336+
when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
337+
when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
338+
when(serviceManager.getServerService()).thenReturn(serverService);
339+
340+
Server server = new Server();
341+
server.reset();
342+
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
343+
assertEquals("reset", captor.getValue().getPropertyName());
344+
}
345+
}
346+
302347
}

src/test/java/dev/tomr/hcloud/service/action/ActionEnumTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,16 @@ void shutdown() {
1818
void poweroff() {
1919
assertEquals("poweroff", Action.POWEROFF.path);
2020
}
21+
22+
@Test
23+
@DisplayName("POWERON enum returns 'poweron' for the path")
24+
void poweron() {
25+
assertEquals("poweron", Action.POWERON.path);
26+
}
27+
28+
@Test
29+
@DisplayName("REBOOT enum returns 'reboot' for the path")
30+
void reboot() {
31+
assertEquals("reboot", Action.REBOOT.path);
32+
}
2133
}

src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,4 +821,68 @@ void powerOnServerCallsHetznerAndTracksTheAction() throws IOException, Interrupt
821821
verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
822822
}
823823
}
824+
825+
@Test
826+
@DisplayName("Reboot Server calls Hetzner and tracks the action")
827+
void RebootServerCallsHetznerAndTracksTheAction() throws IOException, InterruptedException, IllegalAccessException {
828+
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
829+
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
830+
ListenerManager listenerManager = mock(ListenerManager.class);
831+
ServiceManager serviceManager = mock(ServiceManager.class);
832+
ActionService actionService = mock(ActionService.class);
833+
834+
try (MockedStatic<HetznerCloud> hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
835+
MockedStatic<HetznerCloudHttpClient> hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
836+
837+
Action action = new Action();
838+
action.setFinished(Date.from(Instant.now()).toString());
839+
840+
hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
841+
hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
842+
when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
843+
when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
844+
when(serviceManager.getActionService()).thenReturn(actionService);
845+
when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(action));
846+
847+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenReturn(new ActionWrapper(action));
848+
849+
ServerService serverService = new ServerService(serviceManager);
850+
serverService.rebootServer(new Server());
851+
852+
verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
853+
verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
854+
}
855+
}
856+
857+
@Test
858+
@DisplayName("Reset Server calls Hetzner and tracks the action")
859+
void ResetServerCallsHetznerAndTracksTheAction() throws IOException, InterruptedException, IllegalAccessException {
860+
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
861+
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
862+
ListenerManager listenerManager = mock(ListenerManager.class);
863+
ServiceManager serviceManager = mock(ServiceManager.class);
864+
ActionService actionService = mock(ActionService.class);
865+
866+
try (MockedStatic<HetznerCloud> hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
867+
MockedStatic<HetznerCloudHttpClient> hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
868+
869+
Action action = new Action();
870+
action.setFinished(Date.from(Instant.now()).toString());
871+
872+
hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
873+
hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
874+
when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
875+
when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
876+
when(serviceManager.getActionService()).thenReturn(actionService);
877+
when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(action));
878+
879+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenReturn(new ActionWrapper(action));
880+
881+
ServerService serverService = new ServerService(serviceManager);
882+
serverService.resetServer(new Server());
883+
884+
verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
885+
verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
886+
}
887+
}
824888
}

0 commit comments

Comments
 (0)