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
41 changes: 34 additions & 7 deletions src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,40 @@ public class ServerChangeListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
Server server = (Server) evt.getSource();
if (evt.getPropertyName().equals("delete")) {
logger.warn("Server delete has been called. Instructing Hetzner to delete");
HetznerCloud.getInstance().getServiceManager().getServerService().deleteServerFromHetzner(server);
} else {
logger.info("Server changed: " + evt.getPropertyName());
logger.info("Server: " + evt.getOldValue() + " -> " + evt.getNewValue());
HetznerCloud.getInstance().getServiceManager().getServerService().serverNameOrLabelUpdate(evt.getPropertyName(), evt.getNewValue(), server);
String propertyName = evt.getPropertyName();

switch (propertyName) {
case "delete" -> {
logger.warn("Server delete has been called. Instructing Hetzner to delete");
HetznerCloud.getInstance().getServiceManager().getServerService().deleteServerFromHetzner(server);
}
case "shutdown" -> {
logger.info("Server shutdown has been called. Instructing Hetzner to shut the server down");
HetznerCloud.getInstance().getServiceManager().getServerService().shutdownServer(server);
}
case "poweroff" -> {
logger.info("Server poweroff has been called. Instructing Hetzner to power down the server");
logger.warn("This is a potentially destructive action!");
HetznerCloud.getInstance().getServiceManager().getServerService().powerOffServer(server);
}
case "poweron" -> {
logger.info("Server power on has been called. Instructing Hetzner to power up the server");
HetznerCloud.getInstance().getServiceManager().getServerService().powerOnServer(server);
}
case "reboot" -> {
logger.info("Server reboot has been called. Instructing Hetzner to reboot the server");
HetznerCloud.getInstance().getServiceManager().getServerService().rebootServer(server);
}
case "reset" -> {
logger.info("Server reset has been called. Instructing Hetzner to reset the server");
logger.warn("This is a potentially destructive action!");
HetznerCloud.getInstance().getServiceManager().getServerService().resetServer(server);
}
default -> {
logger.info("Server changed: {}", evt.getPropertyName());
logger.info("Server: {} -> {}", evt.getOldValue(), evt.getNewValue());
HetznerCloud.getInstance().getServiceManager().getServerService().serverNameOrLabelUpdate(evt.getPropertyName(), evt.getNewValue(), server);
}
}
}
}
37 changes: 37 additions & 0 deletions src/main/java/dev/tomr/hcloud/resources/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,50 @@ private void setupPropertyChangeListener() {
propertyChangeSupport.addPropertyChangeListener(HetznerCloud.getInstance().getListenerManager().getServerChangeListener());
}

// The following methods are for calling Actions on the server

/**
* Deletes a Server from Hetzner. Note, this is immediate and destructive. Ensure you want to delete the server before calling.
*/
public void delete() {
propertyChangeSupport.firePropertyChange("delete", null, null);
}

/**
* 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'.
* The server OS must support ACPI
*/
public void shutdown() {
propertyChangeSupport.firePropertyChange("shutdown", null, null);
}

/**
* 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>
*/
public void powerOff() {
propertyChangeSupport.firePropertyChange("poweroff", null, null);
}

/**
* Starts the Server by turning its power on
*/
public void powerOn() {
propertyChangeSupport.firePropertyChange("poweron", null, null);
}

/**
* Sends a reboot request to the server by sending an ACPI request. The server OS must support ACPI
*/
public void reboot() {
propertyChangeSupport.firePropertyChange("reboot", null, null);
}

/**
* 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.
*/
public void reset() {
propertyChangeSupport.firePropertyChange("reset", null, null);
}

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

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/dev/tomr/hcloud/service/action/Action.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.tomr.hcloud.service.action;

public enum Action {
SHUTDOWN("shutdown"),
POWEROFF("poweroff"),
POWERON("poweron"),
REBOOT("reboot"),
RESET("reset"),;

public final String path;

Action(String path) {
this.path = path;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public CompletableFuture<Action> waitForActionToComplete(Action action) {
futures.forEach((f) -> {
f.cancel(true);
});
scheduler.shutdownNow();
return completedAction.get();
});
}
Expand Down
49 changes: 48 additions & 1 deletion src/main/java/dev/tomr/hcloud/service/server/ServerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

import static dev.tomr.hcloud.http.RequestVerb.*;
import static dev.tomr.hcloud.service.action.Action.*;

public class ServerService {
protected static final Logger logger = LogManager.getLogger();
Expand Down Expand Up @@ -127,6 +127,53 @@ public void deleteServerFromHetzner(Server server) {
}
}

public void shutdownServer(Server server) {
sendServerAction(server, SHUTDOWN);
}

public void powerOffServer(Server server) {
sendServerAction(server, POWEROFF);
}

public void powerOnServer(Server server) {
sendServerAction(server, POWERON);
}

public void rebootServer(Server server) {
sendServerAction(server, REBOOT);
}

public void resetServer(Server server) {
sendServerAction(server, RESET);
}

private void sendServerAction(Server server, dev.tomr.hcloud.service.action.Action givenAction) {
List<String> hostAndKey = HetznerCloud.getInstance().getHttpDetails();
String httpUrl = String.format("%sservers/%d/actions/%s", hostAndKey.get(0), server.getId(), givenAction.path);
AtomicReference<String> exceptionMsg = new AtomicReference<>();
try {
Action action = client.sendHttpRequest(ActionWrapper.class, httpUrl, POST, hostAndKey.get(1), "").getAction();
CompletableFuture<Action> completedActionFuture = serviceManager.getActionService().waitForActionToComplete(action).thenApplyAsync((completedAction) -> {
if (completedAction == null) {
throw new NullPointerException();
}
logger.info("Server {} at {}", givenAction.toString(), completedAction.getFinished());
return completedAction;
}).exceptionally((e) -> {
logger.error("Server {} failed", givenAction.toString());
logger.error(e.getMessage());
exceptionMsg.set(e.getMessage());
return null;
});
if (completedActionFuture.get() == null) {
throw new RuntimeException(exceptionMsg.get());
}
} catch (Exception e) {
logger.error("Failed to {} the Server", givenAction.toString());
throw new RuntimeException(e);
}
}

private void updateAllRemoteServers() {
Map<Date, Server> newServerMap = new HashMap<>();
List<String> hostAndKey = HetznerCloud.getInstance().getHttpDetails();
Expand Down
123 changes: 122 additions & 1 deletion src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@ void callingSetNameSendsAnEventToTheServerChangeListener() {
assertNull(captor.getValue().getOldValue());
assertEquals("test", captor.getValue().getNewValue());
}

}

@Test
Expand All @@ -219,8 +218,130 @@ void callingDeleteSendsAnEventToTheServerChangeListener() {
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
assertEquals("delete", captor.getValue().getPropertyName());
}
}

@Test
@DisplayName("calling shutdown sends an event to the ServerChangeListener")
void callingShutdownSendsAnEventToTheServerChangeListener() {
try (MockedStatic<HetznerCloud> hetznerCloud = mockStatic(HetznerCloud.class)) {
HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
ServerChangeListener scl = new ServerChangeListener();
ServerChangeListener serverChangeListener = spy(scl);
ListenerManager listenerManager = mock(ListenerManager.class);
ServiceManager serviceManager = mock(ServiceManager.class);
ServerService serverService = mock(ServerService.class);
ArgumentCaptor<PropertyChangeEvent> captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);

hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
when(serviceManager.getServerService()).thenReturn(serverService);

Server server = new Server();
server.shutdown();
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
assertEquals("shutdown", captor.getValue().getPropertyName());
}
}

@Test
@DisplayName("calling poweroff sends an event to the ServerChangeListener")
void callingPoweroffSendsAnEventToTheServerChangeListener() {
try (MockedStatic<HetznerCloud> hetznerCloud = mockStatic(HetznerCloud.class)) {
HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
ServerChangeListener scl = new ServerChangeListener();
ServerChangeListener serverChangeListener = spy(scl);
ListenerManager listenerManager = mock(ListenerManager.class);
ServiceManager serviceManager = mock(ServiceManager.class);
ServerService serverService = mock(ServerService.class);
ArgumentCaptor<PropertyChangeEvent> captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);

hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
when(serviceManager.getServerService()).thenReturn(serverService);

Server server = new Server();
server.powerOff();
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
assertEquals("poweroff", captor.getValue().getPropertyName());
}
}

@Test
@DisplayName("calling poweron sends an event to the ServerChangeListener")
void callingPowerOnSendsAnEventToTheServerChangeListener() {
try (MockedStatic<HetznerCloud> hetznerCloud = mockStatic(HetznerCloud.class)) {
HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
ServerChangeListener scl = new ServerChangeListener();
ServerChangeListener serverChangeListener = spy(scl);
ListenerManager listenerManager = mock(ListenerManager.class);
ServiceManager serviceManager = mock(ServiceManager.class);
ServerService serverService = mock(ServerService.class);
ArgumentCaptor<PropertyChangeEvent> captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);

hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
when(serviceManager.getServerService()).thenReturn(serverService);

Server server = new Server();
server.powerOn();
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
assertEquals("poweron", captor.getValue().getPropertyName());
}
}
@Test
@DisplayName("calling reboot sends an event to the ServerChangeListener")
void callingRebootSendsAnEventToTheServerChangeListener() {
try (MockedStatic<HetznerCloud> hetznerCloud = mockStatic(HetznerCloud.class)) {
HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
ServerChangeListener scl = new ServerChangeListener();
ServerChangeListener serverChangeListener = spy(scl);
ListenerManager listenerManager = mock(ListenerManager.class);
ServiceManager serviceManager = mock(ServiceManager.class);
ServerService serverService = mock(ServerService.class);
ArgumentCaptor<PropertyChangeEvent> captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);

hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
when(serviceManager.getServerService()).thenReturn(serverService);

Server server = new Server();
server.reboot();
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
assertEquals("reboot", captor.getValue().getPropertyName());
}
}

@Test
@DisplayName("calling reset sends an event to the ServerChangeListener")
void callingResetSendsAnEventToTheServerChangeListener() {
try (MockedStatic<HetznerCloud> hetznerCloud = mockStatic(HetznerCloud.class)) {
HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
ServerChangeListener scl = new ServerChangeListener();
ServerChangeListener serverChangeListener = spy(scl);
ListenerManager listenerManager = mock(ListenerManager.class);
ServiceManager serviceManager = mock(ServiceManager.class);
ServerService serverService = mock(ServerService.class);
ArgumentCaptor<PropertyChangeEvent> captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);

hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
when(serviceManager.getServerService()).thenReturn(serverService);

Server server = new Server();
server.reset();
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
assertEquals("reset", captor.getValue().getPropertyName());
}
}

}
33 changes: 33 additions & 0 deletions src/test/java/dev/tomr/hcloud/service/action/ActionEnumTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.tomr.hcloud.service.action;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class ActionEnumTest {

@Test
@DisplayName("SHUTDOWN enum returns 'shutdown' for the path")
void shutdown() {
assertEquals("shutdown", Action.SHUTDOWN.path);
}

@Test
@DisplayName("POWEROFF enum returns 'poweroff' for the path")
void poweroff() {
assertEquals("poweroff", Action.POWEROFF.path);
}

@Test
@DisplayName("POWERON enum returns 'poweron' for the path")
void poweron() {
assertEquals("poweron", Action.POWERON.path);
}

@Test
@DisplayName("REBOOT enum returns 'reboot' for the path")
void reboot() {
assertEquals("reboot", Action.REBOOT.path);
}
}
Loading
Loading