Skip to content

Commit b07587b

Browse files
committed
feat: Add Poweroff action
- Moves ServerChangeListener to use a Switch - Moves action sending + tracking logic to a shared method - Adds powerOff method on Server
1 parent af9d71b commit b07587b

File tree

7 files changed

+144
-19
lines changed

7 files changed

+144
-19
lines changed

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

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,36 @@ public class ServerChangeListener implements PropertyChangeListener {
1919
@Override
2020
public void propertyChange(PropertyChangeEvent evt) {
2121
Server server = (Server) evt.getSource();
22-
if (evt.getPropertyName().equals("delete")) {
23-
logger.warn("Server delete has been called. Instructing Hetzner to delete");
24-
HetznerCloud.getInstance().getServiceManager().getServerService().deleteServerFromHetzner(server);
25-
} else if (evt.getPropertyName().equals("shutdown")) {
26-
logger.info("Server shutdown has been called. Instructing Hetzner to shut the server down");
27-
HetznerCloud.getInstance().getServiceManager().getServerService().shutdownServer(server);
28-
} else {
29-
logger.info("Server changed: " + evt.getPropertyName());
30-
logger.info("Server: " + evt.getOldValue() + " -> " + evt.getNewValue());
31-
HetznerCloud.getInstance().getServiceManager().getServerService().serverNameOrLabelUpdate(evt.getPropertyName(), evt.getNewValue(), server);
22+
String propertyName = evt.getPropertyName();
23+
24+
switch (propertyName) {
25+
case "delete" -> {
26+
logger.warn("Server delete has been called. Instructing Hetzner to delete");
27+
HetznerCloud.getInstance().getServiceManager().getServerService().deleteServerFromHetzner(server);
28+
}
29+
case "shutdown" -> {
30+
logger.info("Server shutdown has been called. Instructing Hetzner to shut the server down");
31+
HetznerCloud.getInstance().getServiceManager().getServerService().shutdownServer(server);
32+
}
33+
case "poweroff" -> {
34+
logger.info("Server poweroff has been called. Instructing Hetzner to power down the server");
35+
logger.warn("This is a potentially destructive action!");
36+
HetznerCloud.getInstance().getServiceManager().getServerService().powerOffServer(server);
37+
}
38+
default -> {
39+
logger.info("Server changed: {}", evt.getPropertyName());
40+
logger.info("Server: {} -> {}", evt.getOldValue(), evt.getNewValue());
41+
HetznerCloud.getInstance().getServiceManager().getServerService().serverNameOrLabelUpdate(evt.getPropertyName(), evt.getNewValue(), server);
42+
}
3243
}
44+
// if (evt.getPropertyName().equals("delete")) {
45+
//
46+
// } else if (evt.getPropertyName().equals("shutdown")) {
47+
//
48+
// } else if (evt.getPropertyName().equals("poweroff")) {
49+
//
50+
// } else {
51+
//
52+
// }
3353
}
3454
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,19 @@ 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 **must** ensure the server is completely offline, you should also call .powerOff() to ensure the 'plug is pulled'
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'
111111
*/
112112
public void shutdown() {
113113
propertyChangeSupport.firePropertyChange("shutdown", null, null);
114114
}
115115

116+
/**
117+
* Sends a command to Power off the server. This is essentially <b>'pulling the plug'</b> and could be destructive if programs are still running on the server. <b>Only use if absolutely necessary</b>
118+
*/
119+
public void powerOff() {
120+
propertyChangeSupport.firePropertyChange("poweroff", null, null);
121+
}
122+
116123

117124
// 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
118125

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package dev.tomr.hcloud.service.action;
2+
3+
public enum Action {
4+
SHUTDOWN("shutdown"),
5+
POWEROFF("poweroff");
6+
7+
public final String path;
8+
9+
Action(String path) {
10+
this.path = path;
11+
}
12+
}

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import java.util.concurrent.atomic.AtomicReference;
2626

2727
import static dev.tomr.hcloud.http.RequestVerb.*;
28+
import static dev.tomr.hcloud.service.action.Action.POWEROFF;
29+
import static dev.tomr.hcloud.service.action.Action.SHUTDOWN;
2830

2931
public class ServerService {
3032
protected static final Logger logger = LogManager.getLogger();
@@ -127,19 +129,27 @@ public void deleteServerFromHetzner(Server server) {
127129
}
128130

129131
public void shutdownServer(Server server) {
132+
sendServerAction(server, SHUTDOWN);
133+
}
134+
135+
public void powerOffServer(Server server) {
136+
sendServerAction(server, POWEROFF);
137+
}
138+
139+
private void sendServerAction(Server server, dev.tomr.hcloud.service.action.Action givenAction) {
130140
List<String> hostAndKey = HetznerCloud.getInstance().getHttpDetails();
131-
String httpUrl = String.format("%sservers/%d/actions/poweroff", hostAndKey.get(0), server.getId());
141+
String httpUrl = String.format("%sservers/%d/actions/%s", hostAndKey.get(0), server.getId(), givenAction.path);
132142
AtomicReference<String> exceptionMsg = new AtomicReference<>();
133143
try {
134144
Action action = client.sendHttpRequest(ActionWrapper.class, httpUrl, POST, hostAndKey.get(1), "").getAction();
135145
CompletableFuture<Action> completedActionFuture = serviceManager.getActionService().waitForActionToComplete(action).thenApplyAsync((completedAction) -> {
136146
if (completedAction == null) {
137147
throw new NullPointerException();
138148
}
139-
logger.info("Server shutdown at {}", completedAction.getFinished());
149+
logger.info("Server {} at {}", givenAction.toString(), completedAction.getFinished());
140150
return completedAction;
141151
}).exceptionally((e) -> {
142-
logger.error("Server shutdown failed");
152+
logger.error("Server {} failed", givenAction.toString());
143153
logger.error(e.getMessage());
144154
exceptionMsg.set(e.getMessage());
145155
return null;
@@ -148,7 +158,7 @@ public void shutdownServer(Server server) {
148158
throw new RuntimeException(exceptionMsg.get());
149159
}
150160
} catch (Exception e) {
151-
logger.error("Failed to shutdown the Server");
161+
logger.error("Failed to {} the Server", givenAction.toString());
152162
throw new RuntimeException(e);
153163
}
154164
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,5 +248,31 @@ void callingShutdownSendsAnEventToTheServerChangeListener() {
248248

249249
}
250250

251+
@Test
252+
@DisplayName("calling poweroff sends an event to the ServerChangeListener")
253+
void callingPoweroffSendsAnEventToTheServerChangeListener() {
254+
try (MockedStatic<HetznerCloud> hetznerCloud = mockStatic(HetznerCloud.class)) {
255+
HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
256+
ServerChangeListener scl = new ServerChangeListener();
257+
ServerChangeListener serverChangeListener = spy(scl);
258+
ListenerManager listenerManager = mock(ListenerManager.class);
259+
ServiceManager serviceManager = mock(ServiceManager.class);
260+
ServerService serverService = mock(ServerService.class);
261+
ArgumentCaptor<PropertyChangeEvent> captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);
262+
263+
hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
264+
when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
265+
when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
266+
when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
267+
when(serviceManager.getServerService()).thenReturn(serverService);
268+
269+
Server server = new Server();
270+
server.powerOff();
271+
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
272+
assertEquals("poweroff", captor.getValue().getPropertyName());
273+
}
274+
275+
}
276+
251277

252278
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dev.tomr.hcloud.service.action;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
8+
public class ActionEnumTest {
9+
10+
@Test
11+
@DisplayName("SHUTDOWN enum returns 'shutdown' for the path")
12+
void shutdown() {
13+
assertEquals("shutdown", Action.SHUTDOWN.path);
14+
}
15+
16+
@Test
17+
@DisplayName("POWEROFF enum returns 'poweroff' for the path")
18+
void poweroff() {
19+
assertEquals("poweroff", Action.POWEROFF.path);
20+
}
21+
}

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

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@
1212
import dev.tomr.hcloud.resources.server.Server;
1313
import dev.tomr.hcloud.service.ServiceManager;
1414
import dev.tomr.hcloud.service.action.ActionService;
15-
import org.junit.jupiter.api.AfterEach;
16-
import org.junit.jupiter.api.BeforeEach;
17-
import org.junit.jupiter.api.DisplayName;
18-
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.*;
1916
import org.mockito.MockedStatic;
2017
import org.mockito.Mockito;
2118

@@ -761,4 +758,36 @@ void whenShutdownActionReturnedFromHetznerIsNullServerServiceThrowsANullPointer(
761758
}
762759
}
763760

761+
@Test
762+
@DisplayName("Poweroff Server calls Hetzner and tracks the action")
763+
void powerOffServerCallsHetznerAndTracksTheAction() throws IOException, InterruptedException, IllegalAccessException {
764+
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
765+
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
766+
ListenerManager listenerManager = mock(ListenerManager.class);
767+
ServiceManager serviceManager = mock(ServiceManager.class);
768+
ActionService actionService = mock(ActionService.class);
769+
770+
try (MockedStatic<HetznerCloud> hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
771+
MockedStatic<HetznerCloudHttpClient> hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
772+
773+
Action action = new Action();
774+
action.setFinished(Date.from(Instant.now()).toString());
775+
776+
hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
777+
hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
778+
when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
779+
when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
780+
when(serviceManager.getActionService()).thenReturn(actionService);
781+
when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(action));
782+
783+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenReturn(new ActionWrapper(action));
784+
785+
ServerService serverService = new ServerService(serviceManager);
786+
serverService.powerOffServer(new Server());
787+
788+
verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
789+
verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
790+
}
791+
}
792+
764793
}

0 commit comments

Comments
 (0)