package com.sequenceiq.cloudbreak.cloud.gcp.compute; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.google.api.services.compute.Compute; import com.google.api.services.compute.model.AccessConfig; import com.google.api.services.compute.model.AttachedDisk; import com.google.api.services.compute.model.Instance; import com.google.api.services.compute.model.Metadata; import com.google.api.services.compute.model.NetworkInterface; import com.google.api.services.compute.model.Operation; import com.google.api.services.compute.model.Scheduling; import com.google.api.services.compute.model.Tags; import com.sequenceiq.cloudbreak.api.model.InstanceGroupType; import com.sequenceiq.cloudbreak.cloud.context.AuthenticatedContext; import com.sequenceiq.cloudbreak.cloud.context.CloudContext; import com.sequenceiq.cloudbreak.cloud.gcp.GcpResourceException; import com.sequenceiq.cloudbreak.cloud.gcp.context.GcpContext; import com.sequenceiq.cloudbreak.cloud.gcp.util.GcpStackUtil; import com.sequenceiq.cloudbreak.cloud.model.AvailabilityZone; import com.sequenceiq.cloudbreak.cloud.model.CloudInstance; import com.sequenceiq.cloudbreak.cloud.model.CloudResource; import com.sequenceiq.cloudbreak.cloud.model.CloudVmInstanceStatus; import com.sequenceiq.cloudbreak.cloud.model.Group; import com.sequenceiq.cloudbreak.cloud.model.Image; import com.sequenceiq.cloudbreak.cloud.model.InstanceStatus; import com.sequenceiq.cloudbreak.cloud.model.InstanceTemplate; import com.sequenceiq.cloudbreak.cloud.model.Location; import com.sequenceiq.cloudbreak.cloud.model.Region; import com.sequenceiq.cloudbreak.common.type.ResourceType; @Component public class GcpInstanceResourceBuilder extends AbstractGcpComputeBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(GcpInstanceResourceBuilder.class); private static final String GCP_DISK_TYPE = "PERSISTENT"; private static final String GCP_DISK_MODE = "READ_WRITE"; private static final String PREEMPTIBLE = "preemptible"; private static final int ORDER = 3; @Override public List<CloudResource> create(GcpContext context, long privateId, AuthenticatedContext auth, Group group, Image image) { CloudContext cloudContext = auth.getCloudContext(); String resourceName = getResourceNameService().resourceName(resourceType(), cloudContext.getName(), group.getName(), privateId); return Collections.singletonList(createNamedResource(resourceType(), resourceName)); } @Override public List<CloudResource> build(GcpContext context, long privateId, AuthenticatedContext auth, Group group, Image image, List<CloudResource> buildableResource, Map<String, String> customTags) throws Exception { InstanceTemplate template = group.getReferenceInstanceConfiguration().getTemplate(); String projectId = context.getProjectId(); Location location = context.getLocation(); boolean noPublicIp = context.getNoPublicIp(); Compute compute = context.getCompute(); List<CloudResource> computeResources = context.getComputeResources(privateId); List<AttachedDisk> listOfDisks = new ArrayList<>(); listOfDisks.addAll(getBootDiskList(computeResources, projectId, location.getAvailabilityZone())); listOfDisks.addAll(getAttachedDisks(computeResources, projectId, location.getAvailabilityZone())); Instance instance = new Instance(); instance.setMachineType(String.format("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/machineTypes/%s", projectId, location.getAvailabilityZone().value(), template.getFlavor())); instance.setName(buildableResource.get(0).getName()); instance.setCanIpForward(Boolean.TRUE); instance.setNetworkInterfaces(getNetworkInterface(context.getNetworkResources(), computeResources, location.getRegion(), group, compute, projectId, noPublicIp)); instance.setDisks(listOfDisks); Scheduling scheduling = new Scheduling(); boolean preemptible = false; if (template.getParameter(PREEMPTIBLE, Boolean.class) != null) { preemptible = template.getParameter(PREEMPTIBLE, Boolean.class); } scheduling.setPreemptible(preemptible); instance.setScheduling(scheduling); Tags tags = new Tags(); List<String> tagList = new ArrayList<>(); String groupname = group.getName().toLowerCase().replaceAll("[^A-Za-z0-9 ]", ""); tagList.add(groupname); tagList.add(GcpStackUtil.getClusterTag(auth.getCloudContext())); tagList.add(GcpStackUtil.getGroupClusterTag(auth.getCloudContext(), group)); customTags.entrySet().stream().forEach(e -> tagList.add(e.getKey() + "-" + e.getValue())); tags.setItems(tagList); instance.setTags(tags); Metadata metadata = new Metadata(); metadata.setItems(new ArrayList<>()); Metadata.Items sshMetaData = new Metadata.Items(); sshMetaData.setKey("sshKeys"); sshMetaData.setValue(auth.getCloudCredential().getLoginUserName() + ":" + auth.getCloudCredential().getPublicKey()); Metadata.Items startupScript = new Metadata.Items(); startupScript.setKey("startup-script"); startupScript.setValue(image.getUserData(group.getType())); metadata.getItems().add(sshMetaData); metadata.getItems().add(startupScript); instance.setMetadata(metadata); Compute.Instances.Insert insert = compute.instances().insert(projectId, location.getAvailabilityZone().value(), instance); insert.setPrettyPrint(Boolean.TRUE); try { Operation operation = insert.execute(); if (operation.getHttpErrorStatusCode() != null) { throw new GcpResourceException(operation.getHttpErrorMessage(), resourceType(), buildableResource.get(0).getName()); } return Collections.singletonList(createOperationAwareCloudResource(buildableResource.get(0), operation)); } catch (GoogleJsonResponseException e) { throw new GcpResourceException(checkException(e), resourceType(), buildableResource.get(0).getName()); } } @Override public CloudResource delete(GcpContext context, AuthenticatedContext auth, CloudResource resource) throws Exception { String resourceName = resource.getName(); try { Operation operation = context.getCompute().instances() .delete(context.getProjectId(), context.getLocation().getAvailabilityZone().value(), resourceName).execute(); return createOperationAwareCloudResource(resource, operation); } catch (GoogleJsonResponseException e) { exceptionHandler(e, resourceName, resourceType()); } return null; } @Override public List<CloudVmInstanceStatus> checkInstances(GcpContext context, AuthenticatedContext auth, List<CloudInstance> instances) { CloudInstance cloudInstance = instances.get(0); try { LOGGER.info("Checking instance: {}", cloudInstance); Operation operation = check(context, cloudInstance); boolean finished = operation == null || GcpStackUtil.analyzeOperation(operation); InstanceStatus status = finished ? context.isBuild() ? InstanceStatus.STARTED : InstanceStatus.STOPPED : InstanceStatus.IN_PROGRESS; LOGGER.info("Instance: {} status: {}", instances, status); return Collections.singletonList(new CloudVmInstanceStatus(cloudInstance, status)); } catch (Exception e) { LOGGER.info("Failed to check instance state of {}", cloudInstance); return Collections.singletonList(new CloudVmInstanceStatus(cloudInstance, InstanceStatus.IN_PROGRESS)); } } @Override public CloudVmInstanceStatus stop(GcpContext context, AuthenticatedContext auth, CloudInstance instance) { return stopStart(context, auth, instance, true); } @Override public CloudVmInstanceStatus start(GcpContext context, AuthenticatedContext auth, CloudInstance instance) { return stopStart(context, auth, instance, false); } @Override public ResourceType resourceType() { return ResourceType.GCP_INSTANCE; } @Override public int order() { return ORDER; } private List<AttachedDisk> getBootDiskList(List<CloudResource> resources, String projectId, AvailabilityZone zone) { List<AttachedDisk> listOfDisks = new ArrayList<>(); for (CloudResource resource : filterResourcesByType(resources, ResourceType.GCP_DISK)) { listOfDisks.add(createDisk(resource, projectId, zone, true)); } return listOfDisks; } private List<AttachedDisk> getAttachedDisks(List<CloudResource> resources, String projectId, AvailabilityZone zone) { List<AttachedDisk> listOfDisks = new ArrayList<>(); for (CloudResource resource : filterResourcesByType(resources, ResourceType.GCP_ATTACHED_DISK)) { listOfDisks.add(createDisk(resource, projectId, zone, false)); } return listOfDisks; } private AttachedDisk createDisk(CloudResource resource, String projectId, AvailabilityZone zone, boolean boot) { AttachedDisk attachedDisk = new AttachedDisk(); attachedDisk.setBoot(boot); attachedDisk.setAutoDelete(true); attachedDisk.setType(GCP_DISK_TYPE); attachedDisk.setMode(GCP_DISK_MODE); attachedDisk.setDeviceName(resource.getName()); attachedDisk.setSource(String.format("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/disks/%s", projectId, zone.value(), resource.getName())); return attachedDisk; } private List<NetworkInterface> getNetworkInterface(List<CloudResource> networkResources, List<CloudResource> computeResources, Region region, Group group, Compute compute, String projectId, boolean noPublicIp) throws IOException { NetworkInterface networkInterface = new NetworkInterface(); List<CloudResource> subnet = filterResourcesByType(networkResources, ResourceType.GCP_SUBNET); String networkName = subnet.isEmpty() ? filterResourcesByType(networkResources, ResourceType.GCP_NETWORK).get(0).getName() : subnet.get(0).getName(); networkInterface.setName(networkName); if (!noPublicIp) { AccessConfig accessConfig = new AccessConfig(); accessConfig.setName(networkName); accessConfig.setType("ONE_TO_ONE_NAT"); List<CloudResource> reservedIp = filterResourcesByType(computeResources, ResourceType.GCP_RESERVED_IP); if (InstanceGroupType.GATEWAY == group.getType() && !reservedIp.isEmpty()) { Compute.Addresses.Get getReservedIp = compute.addresses().get(projectId, region.value(), reservedIp.get(0).getName()); accessConfig.setNatIP(getReservedIp.execute().getAddress()); } networkInterface.setAccessConfigs(Collections.singletonList(accessConfig)); } if (subnet.isEmpty()) { networkInterface.setNetwork( String.format("https://www.googleapis.com/compute/v1/projects/%s/global/networks/%s", projectId, networkName)); } else { networkInterface.setSubnetwork( String.format("https://www.googleapis.com/compute/v1/projects/%s/regions/%s/subnetworks/%s", projectId, region.value(), networkName)); } return Collections.singletonList(networkInterface); } private List<CloudResource> filterResourcesByType(Collection<CloudResource> resources, ResourceType resourceType) { List<CloudResource> resourcesTemp = new ArrayList<>(); for (CloudResource resource : resources) { if (resourceType.equals(resource.getType())) { resourcesTemp.add(resource); } } return resourcesTemp; } private CloudVmInstanceStatus stopStart(GcpContext context, AuthenticatedContext auth, CloudInstance instance, boolean stopRequest) { String projectId = GcpStackUtil.getProjectId(auth.getCloudCredential()); String availabilityZone = context.getLocation().getAvailabilityZone().value(); Compute compute = context.getCompute(); String instanceId = instance.getInstanceId(); try { Compute.Instances.Get get = compute.instances().get(projectId, availabilityZone, instanceId); String state = stopRequest ? "RUNNING" : "TERMINATED"; if (state.equals(get.execute().getStatus())) { Operation operation = stopRequest ? compute.instances().stop(projectId, availabilityZone, instanceId).setPrettyPrint(true).execute() : compute.instances().start(projectId, availabilityZone, instanceId).setPrettyPrint(true).execute(); CloudInstance operationAwareCloudInstance = createOperationAwareCloudInstance(instance, operation); return checkInstances(context, auth, Collections.singletonList(operationAwareCloudInstance)).get(0); } else { LOGGER.info("Instance {} is not in {} state - won't start it.", state, instanceId); return null; } } catch (IOException e) { throw new GcpResourceException(String.format("An error occurred while stopping the vm '%s'", instanceId), e); } } }