/**
* Copyright (C) 2012-2015 Dell, Inc
* See annotations for authorship information
*
* ====================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ====================================================================
*/
package org.dasein.cloud.google.compute.server;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.services.compute.Compute;
import com.google.api.services.compute.model.Disk;
import com.google.api.services.compute.model.Image;
import com.google.api.services.compute.model.ImageList;
import com.google.api.services.compute.model.Operation;
import org.apache.log4j.Logger;
import org.dasein.cloud.*;
import org.dasein.cloud.compute.*;
import org.dasein.cloud.google.GoogleOperationType;
import org.dasein.cloud.google.GoogleException;
import org.dasein.cloud.google.GoogleMethod;
import org.dasein.cloud.google.Google;
import org.dasein.cloud.google.capabilities.GCEImageCapabilities;
import org.dasein.cloud.util.APITrace;
public class ImageSupport extends AbstractImageSupport<Google> {
private Google provider;
static private final Logger logger = Google.getLogger(ImageSupport.class);
private enum ImageProject{
DEBIAN(Platform.DEBIAN, "debian-cloud"),
CENT_OS(Platform.CENT_OS, "centos-cloud"),
COREOS(Platform.COREOS, "coreos-cloud"),
RHEL(Platform.RHEL, "rhel-cloud"),
SUSE(Platform.SUSE, "suse-cloud"),
UBUNTU(Platform.UBUNTU, "ubuntu-os-cloud"),
WINDOWS(Platform.WINDOWS, "windows-cloud"),
GOOGLE(null, "google");
private Platform platform;
private String projectName;
private ImageProject(Platform platform, String projectName){
this.platform = platform;
this.projectName = projectName;
}
public static String getImageProject(Platform platform) {
for (ImageProject imgProject : ImageProject.values()) {
if (platform != null && platform.equals(imgProject.platform)) {
return imgProject.projectName;
}
}
return GOOGLE.projectName;
}
}
public ImageSupport(Google provider) {
super(provider);
this.provider = provider;
}
@Override
public void addImageShare(@Nonnull String providerImageId, @Nonnull String accountNumber) throws CloudException, InternalException {
throw new OperationNotSupportedException("No ability to share images");
}
@Override
public void addPublicShare(@Nonnull String providerImageId) throws CloudException, InternalException {
throw new OperationNotSupportedException("No ability to make images public");
}
@Override
public @Nonnull String bundleVirtualMachine(@Nonnull String virtualMachineId, @Nonnull MachineImageFormat format, @Nonnull String bucket, @Nonnull String name) throws CloudException, InternalException {
throw new OperationNotSupportedException("Bundling of virtual machines not supported");
}
@Override
public void bundleVirtualMachineAsync(@Nonnull String virtualMachineId, @Nonnull MachineImageFormat format, @Nonnull String bucket, @Nonnull String name, AsynchronousTask<String> trackingTask) throws CloudException, InternalException {
throw new OperationNotSupportedException("Bundling of virtual machines not supported");
}
private transient volatile GCEImageCapabilities capabilities;
@Override
public @Nonnull GCEImageCapabilities getCapabilities(){
if(capabilities == null){
capabilities = new GCEImageCapabilities(provider);
}
return capabilities;
}
@Override
public @Nullable MachineImage getImage(@Nonnull String providerImageId) throws CloudException, InternalException {
APITrace.begin(provider, "Image.getImage");
if (providerImageId.contains("_") == false)
throw new CloudException("Invalid image. Image does not conform to Dasein convention, " + providerImageId + " lacks a '_'" );
try{
ProviderContext ctx = provider.getContext();
if( ctx == null ) {
throw new CloudException("No context has been established for this request");
}
Compute gce = provider.getGoogleCompute();
Image image;
try{
String[] parts = providerImageId.split("_");
image = gce.images().get(parts[0], parts[1]).execute();
} catch (IOException ex) {
if (ex.getMessage().contains("was not found")) // could use 404, but in theory 404 could appear in a image name.
return null;
logger.error("An error occurred while getting image: " + providerImageId + ": " + ex.getMessage());
if (ex.getClass() == GoogleJsonResponseException.class) {
GoogleJsonResponseException gjre = (GoogleJsonResponseException)ex;
throw new GoogleException(CloudErrorType.GENERAL, gjre.getStatusCode(), gjre.getContent(), gjre.getDetails().getMessage());
} else
throw new CloudException(ex.getMessage());
}
return toMachineImage(image);
}
finally {
APITrace.end();
}
}
@Override
public boolean isSubscribed() throws CloudException, InternalException {
return true;
}
@Override
public @Nonnull Iterable<ResourceStatus> listImageStatus(@Nonnull ImageClass cls) throws CloudException, InternalException {
List<ResourceStatus> status = new ArrayList<ResourceStatus>();
Iterable<MachineImage> images = listImages(cls);
for (MachineImage image : images) {
MachineImageState state = image.getCurrentState();
ResourceStatus resStatus = new ResourceStatus(image.getProviderMachineImageId(), state);
status.add(resStatus);
}
return status;
}
@Override
public @Nonnull Iterable<MachineImage> listImages(ImageFilterOptions options) throws CloudException, InternalException {
APITrace.begin(getProvider(), "Image.listImages");
try{
ArrayList<MachineImage> images = new ArrayList<MachineImage>();
try{
Compute gce = provider.getGoogleCompute();
ImageList imgList = gce.images().list(provider.getContext().getAccountNumber()).execute();
//TODO: Add filter options
if(imgList.getItems() != null){
for(Image img : imgList.getItems()){
MachineImage image = toMachineImage(img);
if(image != null)images.add(image);
}
}
} catch (IOException ex) {
logger.error("An error occurred while listing images: " + ex.getMessage());
if (ex.getClass() == GoogleJsonResponseException.class) {
GoogleJsonResponseException gjre = (GoogleJsonResponseException)ex;
throw new GoogleException(CloudErrorType.GENERAL, gjre.getStatusCode(), gjre.getContent(), gjre.getDetails().getMessage());
} else
throw new CloudException(ex.getMessage());
}
return images;
}
finally {
APITrace.end();
}
}
@Override
public @Nonnull Iterable<MachineImage> listMachineImages() throws CloudException, InternalException {
return listImages(ImageClass.MACHINE);
}
@Override
public @Nonnull Iterable<MachineImage> listMachineImagesOwnedBy(String accountId) throws CloudException, InternalException {
return listImages(ImageClass.MACHINE, accountId);
}
@Override
public @Nonnull Iterable<String> listShares(@Nonnull String providerImageId) throws CloudException, InternalException {
return Collections.emptyList();
}
@Override
public @Nonnull MachineImage registerImageBundle(@Nonnull ImageCreateOptions options) throws CloudException, InternalException {
throw new OperationNotSupportedException ("Google does not support bundling images");
}
@Override
public void remove(@Nonnull String providerImageId) throws CloudException, InternalException {
remove(providerImageId, false);
}
@Override
public void remove(@Nonnull String providerImageId, boolean checkState) throws CloudException, InternalException {
Compute gce = provider.getGoogleCompute();
Operation job = null;
try{
MachineImage image = getImage(providerImageId);
if ((null == image) || (null == image.getCurrentState())) {
throw new CloudException("Image " + providerImageId + " does not exist.");
}
if (image.getCurrentState().equals(MachineImageState.ACTIVE)) {
job = gce.images().delete(provider.getContext().getAccountNumber(), image.getName()).execute();
GoogleMethod method = new GoogleMethod(provider);
method.getOperationComplete(provider.getContext(), job, GoogleOperationType.GLOBAL_OPERATION, "", "");
}
} catch (IOException ex) {
logger.error(ex.getMessage());
if (ex.getClass() == GoogleJsonResponseException.class) {
GoogleJsonResponseException gjre = (GoogleJsonResponseException)ex;
if ((gjre.getStatusCode() == 503) && (gjre.getStatusMessage().contains("backendError"))) {
throw new CloudException("Due to a GCE error, you will need to start your task again. If the problem persists, please contact support.");
}
throw new GoogleException(CloudErrorType.GENERAL, gjre.getStatusCode(), gjre.getContent(), gjre.getDetails().getMessage());
} else
throw new CloudException("An error occurred while deleting the image: " + ex.getMessage());
}
}
@Override
public void removeAllImageShares(@Nonnull String providerImageId) throws CloudException, InternalException {
throw new OperationNotSupportedException("Image sharing is not supported in GCE");
}
@Override
public void removeImageShare(@Nonnull String providerImageId, @Nonnull String accountNumber) throws CloudException, InternalException {
throw new OperationNotSupportedException("Image sharing is not supported in GCE");
}
@Override
public void removePublicShare(@Nonnull String providerImageId) throws CloudException, InternalException {
throw new OperationNotSupportedException("Image sharing is not supported in GCE");
}
@Override
public @Nonnull Iterable<MachineImage> searchImages(String accountNumber, String keyword, Platform platform, Architecture architecture, ImageClass... imageClasses) throws CloudException, InternalException {
APITrace.begin(getProvider(), "Image.searchImages");
ArrayList<MachineImage> results = new ArrayList<MachineImage>();
/* GCE only supports intel 64 bit */
if ((architecture != null) && (architecture != Architecture.I64)) {
return results;
}
try{
Collection<MachineImage> images = new ArrayList<MachineImage>();
if(accountNumber == null){
images.addAll((Collection<MachineImage>)searchPublicImages(ImageFilterOptions.getInstance()));
}
logger.error("******************* searchImages 268");
images.addAll((Collection<MachineImage>)listImages(ImageFilterOptions.getInstance()));
for( MachineImage image : images ) {
if(image != null){
if( keyword != null ) {
if( !image.getProviderMachineImageId().contains(keyword) && !image.getName().contains(keyword) && !image.getDescription().contains(keyword) ) {
continue;
}
}
if( platform != null ) {
Platform p = image.getPlatform();
if( !platform.equals(p) ) {
if( platform.isWindows() ) {
if( !p.isWindows() ) {
continue;
}
}
else if( platform.equals(Platform.UNIX) ){
if( !p.isUnix() ) {
continue;
}
}
else {
continue;
}
}
}
if (architecture != null) {
if (architecture != image.getArchitecture()) {
continue;
}
}
results.add(image);
}
}
return results;
}
finally {
APITrace.end();
}
}
private boolean imageMatches(MachineImage image, Pattern pattern, String regex) {
Matcher nameMatcher = pattern.matcher(image.getName());
if (nameMatcher.matches()) {
return true;
}
Matcher descriptionMatcher = pattern.matcher(image.getDescription());
if (descriptionMatcher.matches()) {
return true;
}
Map<String, String> tags = image.getTags();
for (String key : tags.keySet()) {
Matcher tagMatcher = pattern.matcher(tags.get(key));
if (tagMatcher.matches()) {
return true;
}
}
return false;
}
@Override
public @Nonnull Iterable<MachineImage> searchPublicImages(@Nonnull ImageFilterOptions options) throws InternalException, CloudException{
APITrace.begin(getProvider(), "Image.searchPublicImages");
ArrayList<MachineImage> images = new ArrayList<MachineImage>();
/* GCE only supports intel 64 bit */
if ((options.getArchitecture() != null) && (options.getArchitecture() != Architecture.I64)) {
return images;
}
Pattern pattern = null;
if (options.getRegex() != null) {
pattern = Pattern.compile(options.getRegex());
}
try {
try {
Compute gce = provider.getGoogleCompute();
Platform platform = options.getPlatform();
ImageList imgList;
if (platform != null) {
String imageProject = ImageProject.getImageProject(platform);
imgList = gce.images().list(imageProject).execute();
if (imgList != null && imgList.getItems() != null) {
for (Image img : imgList.getItems()) {
MachineImage image = toMachineImage(img);
if (image != null)
if ((options.getRegex() == null) || (imageMatches(image, pattern, options.getRegex())))
images.add(image);
}
}
} else {
for (ImageProject imageProject : ImageProject.values()) {
try{
imgList = gce.images().list(imageProject.projectName).execute();
if (imgList != null && imgList.getItems() != null) {
for (Image img : imgList.getItems()) {
MachineImage image = toMachineImage(img);
if (image != null) {
if ((options.getRegex() == null) || (imageMatches(image, pattern, options.getRegex())))
images.add(image);
}
}
}
} catch(IOException ex) {
/*Don't really care, likely means the image project doesn't exist*/
}
}
}
} catch(IOException ex) {
/* Don't really care, likely means the image project doesn't exist */
}
return images;
}
finally {
APITrace.end();
}
}
@Override
public void updateTags(@Nonnull String imageId, @Nonnull Tag... tags) throws CloudException, InternalException {
throw new OperationNotSupportedException ("Google image does not have meta data");
}
@Override
public void updateTags(@Nonnull String[] imageIds, @Nonnull Tag... tags) throws CloudException, InternalException {
throw new OperationNotSupportedException ("Google image does not have meta data");
}
@Override
public void removeTags(@Nonnull String imageId, @Nonnull Tag... tags) throws CloudException, InternalException {
throw new OperationNotSupportedException ("Google image does not have meta data");
}
@Override
public void removeTags(@Nonnull String[] imageIds, @Nonnull Tag... tags) throws CloudException, InternalException {
throw new OperationNotSupportedException ("Google image does not have meta data");
}
private MachineImage toMachineImage(Image img){
if(img.getDeprecated() != null && (img.getDeprecated().getState().equals("DELETED") || img.getDeprecated().getState().equals("DEPRECATED"))){
return null;
}
String imageStatus = img.getStatus();
MachineImageState state = null;
if(imageStatus.equalsIgnoreCase("READY"))state = MachineImageState.ACTIVE;
else if(imageStatus.equalsIgnoreCase("PENDING"))state = MachineImageState.PENDING;
else return null;//TODO: This might not be appropriate - the final state is FAILED
Architecture arch = Architecture.I64;
Platform platform = Platform.guess(img.getName());
if (platform == Platform.UNKNOWN) {
platform = Platform.guess(img.getDescription());
}
String project = "";
Pattern p = Pattern.compile("/projects/(.*?)/");
Matcher m = p.matcher(img.getSelfLink());
while(m.find()){
project = m.group(1);
break;
}
String owner = provider.getCloudName();
if(project.equals(provider.getContext().getAccountNumber()))owner = provider.getContext().getAccountNumber();
String description = null;
if (img.getDescription() != null)
description = img.getDescription();
else
description = "Image Name: " + img.getName(); //description = "Created from " + img.getSourceDisk();
MachineImage image = MachineImage.getImageInstance(owner, "", project + "_" + img.getName(), ImageClass.MACHINE, state, img.getName(), description, arch, platform, MachineImageFormat.RAW, VisibleScope.ACCOUNT_GLOBAL);
if (owner.equals("GCE")) {
image = image.sharedWithPublic();
}
image.setTag("contentLink", img.getSelfLink());
image.setTag("project", project);
String size = null;
try {
size = img.get("diskSizeGb").toString();
Long s = Long.valueOf(size).longValue();
image.setMinimumDiskSizeGb(s);
} catch (Exception e) {
// I guess leave it at the default.
}
return image;
}
/*
* gcloud compute images create example-image --source-disk example-disk --source-disk-zone ZONE
* Can do this with the command line utility, but not with the API
* Partially implemented code to do it with the API once it catches up...
*/
@Override
public MachineImage capture(@Nonnull ImageCreateOptions options, @Nullable AsynchronousTask<MachineImage> task) throws CloudException, InternalException {
Compute gce = provider.getGoogleCompute();
ServerSupport server = new ServerSupport(provider);
Image imageContent = new Image();
try {
VirtualMachine vm = server.getVirtualMachine(options.getVirtualMachineId());
String[] disks = vm.getProviderVolumeIds(provider);
server.terminateVm(options.getVirtualMachineId());
Disk disk = gce.disks().get(provider.getContext().getAccountNumber(), vm.getProviderDataCenterId(), disks[0]).execute();
imageContent.setName(getCapabilities().getImageNamingConstraints().convertToValidName(options.getName(), Locale.US));
imageContent.setKind("compute#disk");
imageContent.setSourceDisk(disk.getSelfLink());
String derivedFrom = disk.getSourceImage().replaceAll(".*/", "");
if (Platform.guess(derivedFrom) == Platform.UNKNOWN) {
Boolean done = false;
try {
while (!done) {
Image imagePrior = gce.images().get(provider.getContext().getAccountNumber(), derivedFrom).execute();
if (imagePrior.getDescription().startsWith("Derived from ")) {
derivedFrom = imagePrior.getDescription().replaceAll("Derived from ", "");
if (Platform.guess(derivedFrom) != Platform.UNKNOWN) {
done = true;
imageContent.setDescription(imagePrior.getDescription());
}
}
}
} catch (Exception e) {
imageContent.setDescription(derivedFrom); // best guess.
}
} else {
imageContent.setDescription("Derived from " + derivedFrom);
}
Operation job = gce.images().insert(provider.getContext().getAccountNumber(), imageContent).execute();
GoogleMethod method = new GoogleMethod(provider);
method.getOperationComplete(provider.getContext(), job, GoogleOperationType.GLOBAL_OPERATION, "", "");
String zone = disk.getZone();
zone = zone.substring(zone.lastIndexOf("/") + 1);
// now delete source disk...
job = gce.disks().delete(provider.getContext().getAccountNumber(), zone, disk.getName()).execute();
method.getOperationComplete(provider.getContext(), job, GoogleOperationType.ZONE_OPERATION, "", zone);
} catch (Exception ex) {
logger.error(ex.getMessage()); // CloudException: An error occurred: Invalid value for field 'image.hasRawDisk': 'false'.
if (ex.getClass() == GoogleJsonResponseException.class) {
GoogleJsonResponseException gjre = (GoogleJsonResponseException)ex;
throw new GoogleException(CloudErrorType.GENERAL, gjre.getStatusCode(), gjre.getContent(), gjre.getDetails().getMessage());
} else
throw new CloudException("An error occurred while deleting the image: " + ex.getMessage());
}
return getImage(provider.getContext().getAccountNumber() + "_" + options.getName());
}
}