From b8c3df7bcf181661d91c9981f341610f96a45fca Mon Sep 17 00:00:00 2001 From: Ivan Krutov Date: Tue, 14 Feb 2017 18:35:12 +0300 Subject: [PATCH] More GoogleCloud operations (related to #215) --- perspective-google-cloud/pom.xml | 2 +- .../meridor/perspective/googlecloud/Api.java | 41 +++++ .../perspective/googlecloud/ApiProvider.java | 2 +- .../googlecloud/ApiProviderImpl.java | 146 ++++++++++++++++-- .../googlecloud/BaseInstanceOperation.java | 20 +++ .../googlecloud/DeleteInstanceOperation.java | 31 ++++ .../HardRebootInstanceOperation.java | 34 ++++ .../perspective/googlecloud/IdUtils.java | 60 +++++++ .../googlecloud/RebootInstanceOperation.java | 33 ++++ .../ShutdownInstanceOperation.java | 34 ++++ .../googlecloud/StartInstanceOperation.java | 34 ++++ 11 files changed, 424 insertions(+), 13 deletions(-) create mode 100644 perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/BaseInstanceOperation.java create mode 100644 perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/DeleteInstanceOperation.java create mode 100644 perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/HardRebootInstanceOperation.java create mode 100644 perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/IdUtils.java create mode 100644 perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/RebootInstanceOperation.java create mode 100644 perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ShutdownInstanceOperation.java create mode 100644 perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/StartInstanceOperation.java diff --git a/perspective-google-cloud/pom.xml b/perspective-google-cloud/pom.xml index d0570768..b015af5b 100644 --- a/perspective-google-cloud/pom.xml +++ b/perspective-google-cloud/pom.xml @@ -13,7 +13,7 @@ Perspective Google Cloud - 0.8.3-alpha + 0.9.0-alpha diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/Api.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/Api.java index 4b36f27b..60e8bd78 100644 --- a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/Api.java +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/Api.java @@ -1,4 +1,45 @@ package org.meridor.perspective.googlecloud; +import com.google.cloud.compute.MachineType; +import com.google.cloud.compute.Network; +import com.google.cloud.compute.Region; +import org.meridor.perspective.beans.Keypair; + +import java.util.List; + public interface Api { + + // Project operations + + List listFlavors(); + + List listNetworks(); + + List listRegions(); + + List listKeypairs(); + + // Instance operations + + boolean deleteInstance(String instanceId); + + boolean startInstance(String instanceId); + + boolean shutdownInstance(String instanceId); + + boolean rebootInstance(String instanceId); + + boolean hardRebootInstance(String instanceId); + + boolean resizeInstance(String instanceId, String flavorId); + + List listInstances(); + + // Image operations + + boolean deleteImage(String imageId); + + List listImages(); + + List listSnapshots(); } diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ApiProvider.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ApiProvider.java index 28be2649..75a93842 100644 --- a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ApiProvider.java +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ApiProvider.java @@ -1,6 +1,6 @@ package org.meridor.perspective.googlecloud; -import com.google.api.services.compute.model.Region; +import com.google.cloud.compute.Region; import org.meridor.perspective.config.Cloud; import java.util.function.BiConsumer; diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ApiProviderImpl.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ApiProviderImpl.java index f71952d3..76cd78c9 100644 --- a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ApiProviderImpl.java +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ApiProviderImpl.java @@ -1,21 +1,29 @@ package org.meridor.perspective.googlecloud; -import com.google.api.services.compute.model.Region; +import com.google.api.client.util.Lists; import com.google.auth.Credentials; import com.google.auth.oauth2.GoogleCredentials; -import com.google.cloud.compute.Compute; -import com.google.cloud.compute.ComputeOptions; +import com.google.cloud.compute.*; +import org.meridor.perspective.beans.Keypair; import org.meridor.perspective.config.Cloud; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import java.util.function.BiConsumer; +import java.util.function.Function; import static java.nio.file.StandardOpenOption.READ; +import static org.meridor.perspective.googlecloud.IdUtils.stringToImageId; +import static org.meridor.perspective.googlecloud.IdUtils.stringToInstanceId; +@Component public class ApiProviderImpl implements ApiProvider { @Override @@ -25,25 +33,27 @@ public Api getApi(Cloud cloud) { @Override public void forEachRegion(Cloud cloud, BiConsumer action) throws Exception { - + Api api = getApi(cloud); + api.listRegions().forEach(r -> action.accept(r, api)); } - private class ApiImpl implements Api { + private static class ApiImpl implements Api { + + private static final Logger LOG = LoggerFactory.getLogger(Api.class); + private final Compute computeApi; ApiImpl(Cloud cloud) { - this.computeApi = createComputeApi(cloud); + Credentials credentials = getCredentials(cloud); + this.computeApi = createComputeApi(credentials); } - private Compute createComputeApi(Cloud cloud) { + private Credentials getCredentials(Cloud cloud) { Path jsonPath = Paths.get(cloud.getCredential()); try (InputStream inputStream = Files.newInputStream(jsonPath, READ)) { - Credentials credentials = GoogleCredentials.fromStream(inputStream); - return ComputeOptions.newBuilder() - .setCredentials(credentials) - .build().getService(); + return GoogleCredentials.fromStream(inputStream); } catch (IOException e) { throw new RuntimeException(String.format( "Failed to read JSON credentials file [%s]", @@ -51,5 +61,119 @@ private Compute createComputeApi(Cloud cloud) { ), e); } } + + private Compute createComputeApi(Credentials credentials) { + return ComputeOptions.newBuilder() + .setCredentials(credentials) + .build().getService(); + } + + private static boolean executeInstanceOperation(String instanceId, Function action) { + return isOperationSuccessful(action.apply(stringToInstanceId(instanceId))); + } + + private static boolean executeImageOperation(String imageId, Function action) { + return isOperationSuccessful(action.apply(stringToImageId(imageId))); + } + + private static boolean isOperationSuccessful(Operation operation) { + try { + Operation completedOperation = operation.waitFor(); + if (completedOperation != null) { + if (completedOperation.getErrors() != null) { + completedOperation.getErrors() + .forEach( + e -> LOG.error( + "Error {} in {}: {}", + e.getCode(), + e.getLocation(), + e.getMessage() + ) + ); + } else { + return true; + } + } + return false; + } catch (Exception e) { + return false; + } + } + + @Override + public List listFlavors() { + return Lists.newArrayList(computeApi.listMachineTypes().getValues()); + } + + @Override + public List listNetworks() { + return Lists.newArrayList(computeApi.listNetworks().getValues()); + } + + @Override + public List listRegions() { + return Lists.newArrayList(computeApi.listRegions().getValues()); + } + + @Override + public List listKeypairs() { + //TODO: see https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys#project-wide + throw new UnsupportedOperationException(); + } + + @Override + public boolean deleteInstance(String instanceId) { + return executeInstanceOperation(instanceId, id -> computeApi.deleteInstance(id)); + } + + @Override + public boolean startInstance(String instanceId) { + return executeInstanceOperation(instanceId, id -> computeApi.start(id)); + } + + @Override + public boolean shutdownInstance(String instanceId) { + return executeInstanceOperation(instanceId, id -> computeApi.stop(id)); + } + + @Override + public boolean rebootInstance(String instanceId) { + //There's no soft reboot operation in Google Cloud + return shutdownInstance(instanceId) && startInstance(instanceId); + } + + @Override + public boolean hardRebootInstance(String instanceId) { + return executeInstanceOperation(instanceId, id -> computeApi.reset(id)); + } + + @Override + public boolean resizeInstance(String instanceId, String flavorId) { + return executeInstanceOperation(instanceId, id -> { + MachineTypeId machineTypeId = IdUtils.stringToMachineTypeId(flavorId); + return computeApi.setMachineType(id, machineTypeId); + }); + } + + @Override + public List listInstances() { + return Lists.newArrayList(computeApi.listInstances().getValues()); + } + + @Override + public boolean deleteImage(String imageId) { + return executeImageOperation(imageId, id -> computeApi.deleteImage(id)); + } + + @Override + public List listImages() { + return Lists.newArrayList(computeApi.listImages().getValues()); + } + + @Override + public List listSnapshots() { + return Lists.newArrayList(computeApi.listSnapshots().getValues()); + } + } } diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/BaseInstanceOperation.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/BaseInstanceOperation.java new file mode 100644 index 00000000..2ca9a552 --- /dev/null +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/BaseInstanceOperation.java @@ -0,0 +1,20 @@ +package org.meridor.perspective.googlecloud; + +import org.meridor.perspective.beans.Instance; +import org.meridor.perspective.config.Cloud; +import org.meridor.perspective.worker.operation.AbstractInstanceOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public abstract class BaseInstanceOperation extends AbstractInstanceOperation { + + @Autowired + private ApiProvider apiProvider; + + @Override + protected Api getApi(Cloud cloud, Instance instance) { + return apiProvider.getApi(cloud); + } + +} diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/DeleteInstanceOperation.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/DeleteInstanceOperation.java new file mode 100644 index 00000000..fc8a6f4c --- /dev/null +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/DeleteInstanceOperation.java @@ -0,0 +1,31 @@ +package org.meridor.perspective.googlecloud; + +import org.meridor.perspective.beans.Instance; +import org.meridor.perspective.config.OperationType; +import org.springframework.stereotype.Component; + +import java.util.function.BiFunction; + +@Component +public class DeleteInstanceOperation extends BaseInstanceOperation { + + @Override + protected BiFunction getAction() { + return (api, instance) -> api.deleteInstance(instance.getRealId()); + } + + @Override + protected String getSuccessMessage(Instance instance) { + return String.format("Deleted instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + protected String getErrorMessage(Instance instance) { + return String.format("Failed to delete instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + public OperationType[] getTypes() { + return new OperationType[]{OperationType.DELETE_INSTANCE}; + } +} diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/HardRebootInstanceOperation.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/HardRebootInstanceOperation.java new file mode 100644 index 00000000..88e69ede --- /dev/null +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/HardRebootInstanceOperation.java @@ -0,0 +1,34 @@ +package org.meridor.perspective.googlecloud; + +import org.meridor.perspective.beans.Instance; +import org.meridor.perspective.config.OperationType; +import org.springframework.stereotype.Component; + +import java.util.function.BiFunction; + +import static org.meridor.perspective.config.OperationType.HARD_REBOOT_INSTANCE; + +@Component +public class HardRebootInstanceOperation extends BaseInstanceOperation { + + @Override + protected BiFunction getAction() { + return (api, instance) -> api.hardRebootInstance(instance.getRealId()); + } + + @Override + protected String getSuccessMessage(Instance instance) { + return String.format("Hard rebooted instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + protected String getErrorMessage(Instance instance) { + return String.format("Failed to hard reboot instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + public OperationType[] getTypes() { + return new OperationType[]{HARD_REBOOT_INSTANCE}; + } + +} diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/IdUtils.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/IdUtils.java new file mode 100644 index 00000000..82c8f487 --- /dev/null +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/IdUtils.java @@ -0,0 +1,60 @@ +package org.meridor.perspective.googlecloud; + +import com.google.cloud.compute.ImageId; +import com.google.cloud.compute.InstanceId; +import com.google.cloud.compute.MachineTypeId; +import com.google.cloud.compute.ZoneId; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public final class IdUtils { + + private static final String ID_DELIMITER = ":"; + + public static String instanceIdToString(InstanceId instanceId) { + ZoneId zoneId = instanceId.getZoneId(); + return join(zoneId.getProject(), zoneId.getZone(), instanceId.getInstance()); + } + + public static InstanceId stringToInstanceId(String instanceId) { + String[] pieces = split(instanceId, 3); + return InstanceId.of(pieces[0], pieces[1], pieces[2]); + } + + public static String flavorIdToString(MachineTypeId machineTypeId) { + ZoneId zoneId = machineTypeId.getZoneId(); + return join(zoneId.getProject(), zoneId.getZone(), machineTypeId.getType()); + } + + public static MachineTypeId stringToMachineTypeId(String flavorId) { + String[] pieces = split(flavorId, 3); + return MachineTypeId.of(pieces[0], pieces[1], pieces[2]); + } + + public static String imageIdToString(ImageId imageId) { + return join(imageId.getProject(), imageId.getImage()); + } + + public static ImageId stringToImageId(String imageId) { + String[] pieces = split(imageId, 2); + return ImageId.of(pieces[0], pieces[1]); + } + + private static String[] split(String input, int piecesRequired) { + String[] pieces = String.valueOf(input).split(ID_DELIMITER); + if (pieces.length != piecesRequired) { + throw new IllegalArgumentException(String.format( + "Invalid ID [%s] - should be %d colon delimited values", + input, + piecesRequired + )); + } + return pieces; + } + + private static String join(String... pieces) { + return Arrays.stream(pieces).collect(Collectors.joining(ID_DELIMITER)); + } + +} diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/RebootInstanceOperation.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/RebootInstanceOperation.java new file mode 100644 index 00000000..6ce65ede --- /dev/null +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/RebootInstanceOperation.java @@ -0,0 +1,33 @@ +package org.meridor.perspective.googlecloud; + +import org.meridor.perspective.beans.Instance; +import org.meridor.perspective.config.OperationType; +import org.springframework.stereotype.Component; + +import java.util.function.BiFunction; + +import static org.meridor.perspective.config.OperationType.REBOOT_INSTANCE; + +@Component +public class RebootInstanceOperation extends BaseInstanceOperation { + + @Override + protected BiFunction getAction() { + return (api, instance) -> api.rebootInstance(instance.getRealId()); + } + + @Override + protected String getSuccessMessage(Instance instance) { + return String.format("Rebooted instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + protected String getErrorMessage(Instance instance) { + return String.format("Failed to reboot instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + public OperationType[] getTypes() { + return new OperationType[]{REBOOT_INSTANCE}; + } +} diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ShutdownInstanceOperation.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ShutdownInstanceOperation.java new file mode 100644 index 00000000..4831bc84 --- /dev/null +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/ShutdownInstanceOperation.java @@ -0,0 +1,34 @@ +package org.meridor.perspective.googlecloud; + +import org.meridor.perspective.beans.Instance; +import org.meridor.perspective.config.OperationType; +import org.springframework.stereotype.Component; + +import java.util.function.BiFunction; + +import static org.meridor.perspective.config.OperationType.SHUTDOWN_INSTANCE; + +@Component +public class ShutdownInstanceOperation extends BaseInstanceOperation { + + @Override + protected BiFunction getAction() { + return (api, instance) -> api.shutdownInstance(instance.getRealId()); + } + + @Override + protected String getSuccessMessage(Instance instance) { + return String.format("Shut down instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + protected String getErrorMessage(Instance instance) { + return String.format("Failed to shut down instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + public OperationType[] getTypes() { + return new OperationType[]{SHUTDOWN_INSTANCE}; + } + +} diff --git a/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/StartInstanceOperation.java b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/StartInstanceOperation.java new file mode 100644 index 00000000..d7c17c26 --- /dev/null +++ b/perspective-google-cloud/src/main/java/org/meridor/perspective/googlecloud/StartInstanceOperation.java @@ -0,0 +1,34 @@ +package org.meridor.perspective.googlecloud; + +import org.meridor.perspective.beans.Instance; +import org.meridor.perspective.config.OperationType; +import org.springframework.stereotype.Component; + +import java.util.function.BiFunction; + +import static org.meridor.perspective.config.OperationType.START_INSTANCE; + +@Component +public class StartInstanceOperation extends BaseInstanceOperation { + + @Override + protected BiFunction getAction() { + return (api, instance) -> api.startInstance(instance.getRealId()); + } + + @Override + protected String getSuccessMessage(Instance instance) { + return String.format("Started instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + protected String getErrorMessage(Instance instance) { + return String.format("Failed to start instance %s (%s)", instance.getName(), instance.getId()); + } + + @Override + public OperationType[] getTypes() { + return new OperationType[]{START_INSTANCE}; + } + +}