/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.api.service.impl.resource;
import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.api.mapper.ComputeMapper;
import com.emc.storageos.api.mapper.DbObjectMapper;
import com.emc.storageos.api.mapper.TaskMapper;
import com.emc.storageos.api.service.impl.response.BulkList;
import com.emc.storageos.db.client.URIUtil;
import com.emc.storageos.db.client.constraint.ContainmentConstraint;
import com.emc.storageos.db.client.constraint.URIQueryResultList;
import com.emc.storageos.db.client.impl.EncryptionProviderImpl;
import com.emc.storageos.db.client.model.ComputeImage;
import com.emc.storageos.db.client.model.ComputeImage.ComputeImageStatus;
import com.emc.storageos.db.client.model.ComputeImageJob;
import com.emc.storageos.db.client.model.ComputeImageServer;
import com.emc.storageos.db.client.model.EncryptionProvider;
import com.emc.storageos.db.client.model.Operation;
import com.emc.storageos.imageservercontroller.ImageServerController;
import com.emc.storageos.imageservercontroller.impl.ImageServerControllerImpl;
import com.emc.storageos.model.BulkIdParam;
import com.emc.storageos.model.ResourceOperationTypeEnum;
import com.emc.storageos.model.ResourceTypeEnum;
import com.emc.storageos.model.TaskResourceRep;
import com.emc.storageos.model.compute.ComputeImageBulkRep;
import com.emc.storageos.model.compute.ComputeImageCreate;
import com.emc.storageos.model.compute.ComputeImageList;
import com.emc.storageos.model.compute.ComputeImageRestRep;
import com.emc.storageos.model.compute.ComputeImageUpdate;
import com.emc.storageos.security.audit.AuditLogManager;
import com.emc.storageos.security.authorization.ACL;
import com.emc.storageos.security.authorization.CheckPermission;
import com.emc.storageos.security.authorization.DefaultPermissions;
import com.emc.storageos.security.authorization.Role;
import com.emc.storageos.services.OperationTypeEnum;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
import com.emc.storageos.volumecontroller.AsyncTask;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
/**
* Compute image service handles create, update, and remove of compute images.
*/
@Path("/compute/images")
@DefaultPermissions(readRoles = { Role.SYSTEM_ADMIN, Role.SYSTEM_MONITOR }, readAcls = { ACL.USE }, writeRoles = { Role.SYSTEM_ADMIN,
Role.RESTRICTED_SYSTEM_ADMIN })
public class ComputeImageService extends TaskResourceService {
private static final Logger log = LoggerFactory.getLogger(ComputeImageService.class);
private static final String EVENT_SERVICE_TYPE = "ComputeImage";
/**
* Show compute image attribute.
*
* @param id
* the URN of compute image
* @brief Show compute image
* @return Compute image details
*/
@GET
@Path("/{id}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public ComputeImageRestRep getComputeImage(@PathParam("id") URI id) {
ArgValidator.checkFieldUriType(id, ComputeImage.class, "id");
ComputeImage ci = queryResource(id);
List<ComputeImageServer> successfulServers = new ArrayList<ComputeImageServer>();
List<ComputeImageServer> failedServers = new ArrayList<ComputeImageServer>();
getImageImportStatus(ci, successfulServers, failedServers);
return ComputeMapper.map(ci, successfulServers, failedServers);
}
/**
* Returns a list of all compute images.
*
* @brief Show compute images
* @return List of all compute images.
*/
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public ComputeImageList getComputeImages(@QueryParam("imageType") String imageType) {
log.info("getComputeImages, imageType: {}", imageType);
// validate query param
if (imageType != null) {
ArgValidator.checkFieldValueFromEnum(imageType, "imageType",
ComputeImage.ImageType.class);
}
List<URI> ids = _dbClient.queryByType(ComputeImage.class, true);
ComputeImageList list = new ComputeImageList();
Iterator<ComputeImage> iter = _dbClient.queryIterativeObjects(ComputeImage.class, ids);
while (iter.hasNext()) {
ComputeImage img = iter.next();
if (imageType == null || imageType.equals(img.getImageType())) {
list.getComputeImages().add(DbObjectMapper.toNamedRelatedResource(img));
}
}
return list;
}
public void getImageImportStatus(ComputeImage image, List<ComputeImageServer> successfulServers,
List<ComputeImageServer> failedServers) {
List<URI> ids = _dbClient.queryByType(ComputeImageServer.class,
true);
for (URI imageServerId : ids) {
ComputeImageServer imageServer = _dbClient.queryObject(
ComputeImageServer.class, imageServerId);
if (imageServer.getComputeImages() != null
&& imageServer.getComputeImages().contains(
image.getId().toString())) {
successfulServers.add(imageServer);
} else {
failedServers.add(imageServer);
}
}
}
/**
* Create compute image from image URL or existing installable image URN.
*
* @param param
* The ComputeImageCreate object contains all the parameters for
* creation.
* @brief Create compute image
* @return Creation task REST representation.
*/
@POST
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN })
public TaskResourceRep createComputeImage(ComputeImageCreate param) {
log.info("createComputeImage");
// unique name required
ArgValidator.checkFieldNotEmpty(param.getName(), "name");
checkDuplicateLabel(ComputeImage.class, param.getName());
ArgValidator.checkFieldNotEmpty(param.getImageUrl(), "image_url");
ArgValidator.checkUrl(param.getImageUrl(), "image_url");
if (!checkForImageServers()) {
throw APIException.badRequests.cannotAddImageWithoutImageServer();
}
ComputeImage ci = new ComputeImage();
ci.setId(URIUtil.createId(ComputeImage.class));
// IN_PROGRESS until successfully loaded by image server controller
ci.setComputeImageStatus(ComputeImageStatus.IN_PROGRESS.name());
ci.setLabel(param.getName());
ci.setImageUrl(encryptImageURLPassword(param.getImageUrl(), false));
_dbClient.createObject(ci);
auditOp(OperationTypeEnum.CREATE_COMPUTE_IMAGE, true, AuditLogManager.AUDITOP_BEGIN, ci.getId().toString(),
ci.getImageUrl(), ci.getComputeImageStatus());
try {
return doImportImage(ci);
} catch (Exception e) {
ci.setComputeImageStatus(ComputeImageStatus.NOT_AVAILABLE.name());
_dbClient.updateObject(ci);
throw e;
}
}
/*
* Returns task in ready state.
*/
private TaskResourceRep getReadyOp(ComputeImage ci, ResourceOperationTypeEnum opType) {
log.info("doImportImageDone");
String taskId = UUID.randomUUID().toString();
AsyncTask task = new AsyncTask(ComputeImage.class, ci.getId(), taskId);
Operation readyOp = new Operation();
readyOp.ready();
readyOp.setResourceType(opType);
_dbClient.createTaskOpStatus(ComputeImage.class, ci.getId(), task._opId, readyOp);
return TaskMapper.toTask(ci, task._opId, readyOp);
}
/*
* Schedules the import task.
*/
private TaskResourceRep doImportImage(ComputeImage ci) {
log.info("doImportImage");
ImageServerController controller = getController(ImageServerController.class, null);
AsyncTask task = new AsyncTask(ComputeImage.class, ci.getId(), UUID.randomUUID().toString());
Operation op = new Operation();
op.setResourceType(ResourceOperationTypeEnum.IMPORT_IMAGE);
_dbClient.createTaskOpStatus(ComputeImage.class, ci.getId(), task._opId, op);
controller.importImageToServers(task);
return TaskMapper.toTask(ci, task._opId, op);
}
/**
* Updates an already present compute image.
*
* @param id
* compute image URN.
* @param param
* The ComputeImageUpdate object with attributes to be updated.
* @brief Update compute image details
* @return Update task REST representation.
*/
@PUT
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("/{id}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN })
public TaskResourceRep updateComputeImage(@PathParam("id") URI id, ComputeImageUpdate param) {
log.info("updateComputeImage: {}, new name: {}", id, param.getName());
ArgValidator.checkFieldUriType(id, ComputeImage.class, "id");
ArgValidator.checkFieldNotEmpty(param.getName(), "name");
ComputeImage ci = _dbClient.queryObject(ComputeImage.class, id);
ArgValidator.checkEntity(ci, id, isIdEmbeddedInURL(id));
if (!ci.getLabel().equals(param.getName())) {
checkDuplicateLabel(ComputeImage.class, param.getName());
ci.setLabel(param.getName());
}
boolean reImport = false;
// see if image URL needs updating
if (!StringUtils.isBlank(param.getImageUrl()) && !param.getImageUrl().equals(ci.getImageUrl())) {
ArgValidator.checkUrl(param.getImageUrl(), "image_url");
// URL can only be update if image not successfully loaded
if (ci.getComputeImageStatus().equals(
ComputeImageStatus.NOT_AVAILABLE.name())) {
String prevImageUrl = ci.getImageUrl();
boolean isEncrypted = false;
String oldPassword = ImageServerControllerImpl
.extractPasswordFromImageUrl(prevImageUrl);
String newPassword = ImageServerControllerImpl
.extractPasswordFromImageUrl(param.getImageUrl());
if (StringUtils.isNotBlank(oldPassword)
&& StringUtils.isNotBlank(newPassword)) {
//MASKED_PASSWORD is a constant string and UI/REST feeds displays it as bunch
//of asterixk's, if user does not update the password then the newPassword and masked
//password will be same there by we know that password is not updated and same as encrypted
//password present in the DB.
if (ImageServerControllerImpl.MASKED_PASSWORD.equals(newPassword)) {
isEncrypted = true;
}
}
//Any change to the password section of the URL, we will get to know that password is updated
//and we encrypt and updated the DB, if the user does not change the password part but
//changes any other parts (username, hostname or the file part) the password of masked asterisks
//and the constant will be same and we do not update the password but update other parts if changed.
if (isEncrypted) {
ci.setImageUrl( StringUtils.replace(param.getImageUrl(), ":" + newPassword + "@", ":"
+ oldPassword + "@"));
} else {
ci.setImageUrl(encryptImageURLPassword(param.getImageUrl(),
isEncrypted));
}
ci.setComputeImageStatus(ComputeImageStatus.IN_PROGRESS.name());
reImport = true;
} else {
throw APIException.badRequests.invalidParameterCannotUpdateComputeImageUrl();
}
}
_dbClient.updateObject(ci);
auditOp(OperationTypeEnum.UPDATE_COMPUTE_IMAGE, true, null,
ci.getId().toString(), ci.getImageUrl());
return createUpdateTasks(ci, reImport);
}
/**
* Delete existing compute image.
*
* @param id
* compute image URN.
* @brief Delete compute image
* @return Async task remove the image from multiple image serevers returned in response body
*/
@POST
@Path("/{id}/deactivate")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN })
public TaskResourceRep deleteComputeImage(@PathParam("id") URI id,
@QueryParam("force") String force) {
log.info("deleteComputeImage: {}", id);
ComputeImage ci = queryObject(ComputeImage.class, id, true);
ArgValidator.checkEntity(ci, id, isIdEmbeddedInURL(id));
if (ComputeImage.ComputeImageStatus.AVAILABLE.name().equals(
ci.getComputeImageStatus())) {
if (force == null || !force.equals("true")) {
// make sure there are no active jobs associated with this image
URIQueryResultList ceUriList = new URIQueryResultList();
_dbClient.queryByConstraint(
ContainmentConstraint.Factory
.getComputeImageJobsByComputeImageConstraint(ci
.getId()),
ceUriList);
Iterator<URI> iterator = ceUriList.iterator();
while (iterator.hasNext()) {
ComputeImageJob job = _dbClient.queryObject(
ComputeImageJob.class, iterator.next());
if (job.getJobStatus().equals(
ComputeImageJob.JobStatus.CREATED.name())) {
throw APIException.badRequests
.cannotDeleteComputeWhileInUse();
}
}
}
auditOp(OperationTypeEnum.DELETE_COMPUTE_IMAGE, true,
AuditLogManager.AUDITOP_BEGIN, ci.getId().toString(),
ci.getImageUrl());
return doRemoveImage(ci);
} else if (ComputeImage.ComputeImageStatus.IN_PROGRESS.name().equals(
ci.getComputeImageStatus())) {
if (force == null || !force.equals("true")) {
throw APIException.badRequests.resourceCannotBeDeleted(ci
.getLabel());
} else { // delete is forced
deleteImageFromImageServers(ci);
_dbClient.markForDeletion(ci);
auditOp(OperationTypeEnum.DELETE_COMPUTE_IMAGE, true, null, ci
.getId().toString(), ci.getImageUrl());
return getReadyOp(ci, ResourceOperationTypeEnum.REMOVE_IMAGE);
}
} else { // NOT_AVAILABLE
deleteImageFromImageServers(ci);
_dbClient.markForDeletion(ci);
auditOp(OperationTypeEnum.DELETE_COMPUTE_IMAGE, true, null, ci
.getId().toString(), ci.getImageUrl());
return getReadyOp(ci, ResourceOperationTypeEnum.REMOVE_IMAGE);
}
}
/*
* Schedules the remove task.
*/
private TaskResourceRep doRemoveImage(ComputeImage ci) {
log.info("doRemoveImage");
ImageServerController controller = getController(
ImageServerController.class, null);
AsyncTask task = new AsyncTask(ComputeImage.class, ci.getId(), UUID
.randomUUID().toString());
Operation op = new Operation();
op.setResourceType(ResourceOperationTypeEnum.REMOVE_IMAGE);
_dbClient.createTaskOpStatus(ComputeImage.class, ci.getId(),
task._opId, op);
controller.deleteImage(task);
log.info("Removing image " + ci.getImageName());
return TaskMapper.toTask(ci, task._opId, op);
}
/**
* List data of compute images based on input ids.
*
* @param param
* POST data containing the id list.
* @prereq none
* @brief List data of compute images
* @return List of representations.
*/
@POST
@Path("/bulk")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public ComputeImageBulkRep getBulkResources(BulkIdParam param) {
return (ComputeImageBulkRep) super.getBulkResources(param);
}
@Override
public ComputeImageBulkRep queryBulkResourceReps(List<URI> ids) {
Iterator<ComputeImage> _dbIterator = _dbClient.queryIterativeObjects(getResourceClass(), ids);
return new ComputeImageBulkRep(BulkList.wrapping(_dbIterator, COMPUTE_IMAGE_MAPPER));
}
private final ComputeImageMapper COMPUTE_IMAGE_MAPPER = new ComputeImageMapper();
private class ComputeImageMapper implements Function<ComputeImage, ComputeImageRestRep> {
@Override
public ComputeImageRestRep apply(final ComputeImage ci) {
List<ComputeImageServer> successfulServers = new ArrayList<ComputeImageServer>();
List<ComputeImageServer> failedServers = new ArrayList<ComputeImageServer>();
getImageImportStatus(ci, successfulServers, failedServers);
return ComputeMapper.map(ci, successfulServers, failedServers);
}
}
@Override
protected ComputeImageBulkRep queryFilteredBulkResourceReps(List<URI> ids) {
return queryBulkResourceReps(ids);
}
@Override
protected URI getTenantOwner(URI id) {
return null;
}
@Override
protected ResourceTypeEnum getResourceType() {
return ResourceTypeEnum.COMPUTE_IMAGE;
}
@Override
public String getServiceType() {
return EVENT_SERVICE_TYPE;
}
@SuppressWarnings("unchecked")
@Override
public Class<ComputeImage> getResourceClass() {
return ComputeImage.class;
}
@Override
protected ComputeImage queryResource(URI id) {
return queryObject(ComputeImage.class, id, false);
}
/**
* Method to create and initiate task to controller.
*
* @param ci
* {@link ComputeImage} instance
* @param reImport
* boolean to let identify if a reimport of images is required
* @return {@link TaskResourceRep}
*/
private TaskResourceRep createUpdateTasks(ComputeImage ci, boolean reImport) {
boolean hasImportTask = false;
try {
List<URI> ids = _dbClient.queryByType(ComputeImageServer.class,
true);
for (URI imageServerId : ids) {
ComputeImageServer imageServer = _dbClient.queryObject(
ComputeImageServer.class, imageServerId);
if (reImport
|| imageServer.getComputeImages() == null
|| !imageServer.getComputeImages().contains(
ci.getId().toString())) {
hasImportTask = true;
break;
}
}
if (hasImportTask) {
return doImportImage(ci);
} else {
return getReadyOp(ci, ResourceOperationTypeEnum.UPDATE_IMAGE);
}
} catch (Exception e) {
ci.setComputeImageStatus(ComputeImageStatus.NOT_AVAILABLE.name());
_dbClient.updateObject(ci);
throw e;
}
}
/**
* Delete any image references or associations from all existing ImageServers.
* @param ci {@link ComputeImage}
*/
private void deleteImageFromImageServers(ComputeImage ci) {
List<URI> ids = _dbClient.queryByType(ComputeImageServer.class, true);
for (URI imageServerId : ids) {
ComputeImageServer imageServer = _dbClient.queryObject(
ComputeImageServer.class, imageServerId);
if (imageServer.getFailedComputeImages() != null
&& imageServer.getFailedComputeImages().contains(
ci.getId().toString())) {
imageServer.getFailedComputeImages().remove(
ci.getId().toString());
} else if (imageServer.getComputeImages() != null
&& imageServer.getComputeImages().contains(
ci.getId().toString())) {
imageServer.getComputeImages().remove(
ci.getId().toString());
}
_dbClient.updateObject(imageServer);
}
}
/**
* Check if there are image Servers in the system
*/
private boolean checkForImageServers() {
boolean imageServerExists = true;
List<URI> imageServerURIList = _dbClient.queryByType(
ComputeImageServer.class, true);
ArrayList<URI> tempList = Lists.newArrayList(imageServerURIList
.iterator());
if (tempList.isEmpty()) {
imageServerExists = false;
}
return imageServerExists;
}
/**
* Method to mask/encrypt password of the ImageUrl
* @param imageUrl {@link String} compute image URL string
* @param isEncrypted boolean indicating if password is already encrypted.
* @return
*/
private String encryptImageURLPassword(String imageUrl, boolean isEncrypted) {
String password = ImageServerControllerImpl
.extractPasswordFromImageUrl(imageUrl);
String encryptedPassword = password;
if (!isEncrypted && StringUtils.isNotBlank(password)) {
EncryptionProviderImpl encryptionProviderImpl = new EncryptionProviderImpl();
encryptionProviderImpl.setCoordinator(_coordinator);
encryptionProviderImpl.start();
EncryptionProvider encryptionProvider = encryptionProviderImpl;
encryptedPassword = encryptionProvider.getEncryptedString(password);
imageUrl = StringUtils.replace(imageUrl, ":" + password + "@", ":"
+ encryptedPassword + "@");
}
return imageUrl;
}
}