/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.resource;
import static org.quartz.JobBuilder.newJob;
import org.candlepin.auth.Principal;
import org.candlepin.auth.Verify;
import org.candlepin.common.auth.SecurityHole;
import org.candlepin.common.exceptions.BadRequestException;
import org.candlepin.common.exceptions.ConflictException;
import org.candlepin.common.exceptions.NotFoundException;
import org.candlepin.controller.PoolManager;
import org.candlepin.model.CandlepinQuery;
import org.candlepin.model.Consumer;
import org.candlepin.model.ConsumerCurator;
import org.candlepin.model.Content;
import org.candlepin.model.Environment;
import org.candlepin.model.EnvironmentContent;
import org.candlepin.model.EnvironmentContentCurator;
import org.candlepin.model.EnvironmentCurator;
import org.candlepin.model.OwnerContentCurator;
import org.candlepin.pinsetter.tasks.RegenEnvEntitlementCertsJob;
import org.candlepin.util.RdbmsExceptionTranslator;
import org.candlepin.util.Util;
import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
import org.jboss.resteasy.annotations.providers.jaxb.Wrapped;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnap.commons.i18n.I18n;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.PersistenceException;
import javax.persistence.RollbackException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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.Context;
import javax.ws.rs.core.MediaType;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.Authorization;
/**
* REST API for managing Environments.
*/
@Path("/environments")
@Api(value = "environments", authorizations = { @Authorization("basic") })
public class EnvironmentResource {
private static Logger log = LoggerFactory.getLogger(AdminResource.class);
private EnvironmentCurator envCurator;
private I18n i18n;
private EnvironmentContentCurator envContentCurator;
private ConsumerResource consumerResource;
private PoolManager poolManager;
private ConsumerCurator consumerCurator;
private OwnerContentCurator ownerContentCurator;
private RdbmsExceptionTranslator rdbmsExceptionTranslator;
@Inject
public EnvironmentResource(EnvironmentCurator envCurator, I18n i18n,
EnvironmentContentCurator envContentCurator, ConsumerResource consumerResource,
PoolManager poolManager, ConsumerCurator consumerCurator, OwnerContentCurator ownerContentCurator,
RdbmsExceptionTranslator rdbmsExceptionTranslator) {
this.envCurator = envCurator;
this.i18n = i18n;
this.envContentCurator = envContentCurator;
this.consumerResource = consumerResource;
this.poolManager = poolManager;
this.consumerCurator = consumerCurator;
this.ownerContentCurator = ownerContentCurator;
this.rdbmsExceptionTranslator = rdbmsExceptionTranslator;
}
@ApiOperation(notes = "Retrieves a single Environment", value = "getEnv")
@ApiResponses({ @ApiResponse(code = 404, message = "") })
@GET
@Path("/{env_id}")
@Produces(MediaType.APPLICATION_JSON)
public Environment getEnv(
@PathParam("env_id") @Verify(Environment.class) String envId) {
Environment e = envCurator.find(envId);
if (e == null) {
throw new NotFoundException(i18n.tr("No such environment: {0}", envId));
}
return e;
}
@ApiOperation(
notes = "Deletes an environment. WARNING: this will delete all consumers in the environment and " +
"revoke their entitlement certificates.",
value = "deleteEnv")
@ApiResponses({ @ApiResponse(code = 404, message = "") })
@DELETE
@Produces(MediaType.WILDCARD)
@Path("/{env_id}")
public void deleteEnv(@PathParam("env_id") @Verify(Environment.class) String envId) {
Environment e = envCurator.find(envId);
if (e == null) {
throw new NotFoundException(i18n.tr("No such environment: {0}", envId));
}
// Cleanup all consumers and their entitlements:
log.info("Deleting consumers in environment {}", e);
for (Consumer c : e.getConsumers()) {
log.info("Deleting consumer: {}", c);
poolManager.revokeAllEntitlements(c);
consumerCurator.delete(c);
}
log.info("Deleting environment: {}", e);
envCurator.delete(e);
}
@ApiOperation(notes = "Lists the Environments. Only available to super admins.",
value = "getEnvironments", response = Environment.class, responseContainer = "list")
@GET
@Produces(MediaType.APPLICATION_JSON)
@Wrapped(element = "environments")
public CandlepinQuery<Environment> getEnvironments() {
return this.envCurator.listAll();
}
/**
* Verifies that the content specified by the given content object's ID exists.
*
* @param environment
* The environment with which the content will be associated
*
* @param contentId
* The ID of the content to resolve
*
* @return
* the resolved content instance.
*/
private Content resolveContent(Environment environment, String contentId) {
if (environment == null || environment.getOwner() == null) {
throw new BadRequestException(
i18n.tr("No environment specified, or environment lacks owner information")
);
}
if (contentId == null) {
throw new BadRequestException(
i18n.tr("No content ID specified")
);
}
Content resolved = this.ownerContentCurator.getContentById(environment.getOwner(), contentId);
if (resolved == null) {
throw new NotFoundException(i18n.tr(
"Unable to find content with the ID \"{0}\".", contentId
));
}
return resolved;
}
@ApiOperation(notes = "Promotes a Content into an Environment. This call accepts multiple " +
"content sets to promote at once, after which all affected certificates for consumers" +
" in the environment will be regenerated. Consumers registered to this environment " +
"will now receive this content in their entitlement certificates. Because the" +
" certificate regeneraiton can be quite time consuming, this is done as an " +
"asynchronous job. The content will be promoted and immediately available for new " +
"entitlements, but existing entitlements could take some time to be regenerated and " +
"sent down to clients as they check in.", value = "promoteContent")
@ApiResponses({ @ApiResponse(code = 404, message = "") })
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Path("/{env_id}/content")
public JobDetail promoteContent(
@PathParam("env_id") @Verify(Environment.class) String envId,
@ApiParam(name = "contentToPromote", required = true)
List<org.candlepin.model.dto.EnvironmentContent> contentToPromote,
@QueryParam("lazy_regen") @DefaultValue("true") Boolean lazyRegen) {
Environment env = lookupEnvironment(envId);
// Make sure this content has not already been promoted within this environment
// Impl note:
// We have to do this in a separate loop or we'll end up with an undefined state, should
// there be a problem with the request.
for (org.candlepin.model.dto.EnvironmentContent promoteMe : contentToPromote) {
log.debug(
"EnvironmentContent to promote: {}:{}",
promoteMe.getEnvironmentId(), promoteMe.getContentId()
);
EnvironmentContent existing = this.envContentCurator.lookupByEnvironmentAndContent(
env, promoteMe.getContentId()
);
if (existing != null) {
throw new ConflictException(i18n.tr(
"The content with id {0} has already been promoted in this environment.",
promoteMe.getContentId()
));
}
}
Set<String> contentIds = new HashSet<String>();
try {
contentIds = batchCreate(contentToPromote, env);
}
catch (PersistenceException pe) {
if (rdbmsExceptionTranslator.isConstraintViolationDuplicateEntry(pe)) {
log.info("Concurrent content promotion will cause this request to fail.",
pe);
throw new ConflictException(
i18n.tr("Some of the content is already associated with Environment: {0}",
contentToPromote));
}
else {
throw pe;
}
}
JobDataMap map = new JobDataMap();
map.put(RegenEnvEntitlementCertsJob.ENV, env);
map.put(RegenEnvEntitlementCertsJob.CONTENT, contentIds);
map.put(RegenEnvEntitlementCertsJob.LAZY_REGEN, lazyRegen);
JobDetail detail = newJob(RegenEnvEntitlementCertsJob.class)
.withIdentity("regen_entitlement_cert_of_env" + Util.generateUUID())
.usingJobData(map)
.build();
return detail;
}
@ApiOperation(notes = "Demotes a Content from an Environment. Consumer's registered to " +
"this environment will no see this content in their entitlement certificates. (after" +
" they are regenerated and synced to clients) This call accepts multiple content IDs" +
" to demote at once, allowing us to mass demote, then trigger a cert regeneration." +
" NOTE: This call expects the actual content IDs, *not* the ID created for each " +
"EnvironmentContent object created after a promotion. This is to help integrate " +
"with other management apps which should not have to track/lookup a specific ID " +
"for the content to demote.", value = "demoteContent")
@ApiResponses({ @ApiResponse(code = 404, message = "When the content has already been demoted.") })
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@Path("/{env_id}/content")
public JobDetail demoteContent(
@PathParam("env_id") @Verify(Environment.class) String envId,
@QueryParam("content") String[] contentIds,
@QueryParam("lazy_regen") @DefaultValue("true") Boolean lazyRegen) {
Environment e = lookupEnvironment(envId);
Map<String, EnvironmentContent> demotedContent = new HashMap<String, EnvironmentContent>();
// Step through and validate all given content IDs before deleting
for (String contentId : contentIds) {
EnvironmentContent envContent = envContentCurator.lookupByEnvironmentAndContent(e, contentId);
if (envContent == null) {
throw new NotFoundException(i18n.tr("Content does not exist in environment: {0}", contentId));
}
demotedContent.put(contentId, envContent);
}
try {
envContentCurator.bulkDeleteTransactional(
new ArrayList<EnvironmentContent>(demotedContent.values()));
}
catch (RollbackException hibernateException) {
if (rdbmsExceptionTranslator.isUpdateHadNoEffectException(hibernateException)) {
log.info("Concurrent content demotion will cause this request to fail.", hibernateException);
throw new NotFoundException(
i18n.tr("One of the content does not exist in the environment anymore: {0}",
demotedContent.values()));
}
else {
throw hibernateException;
}
}
// Impl note: Unfortunately, we have to make an additional set here, as the keySet isn't
// serializable. Attempting to use it causes exceptions.
Set<String> demotedContentIds = new HashSet<String>(demotedContent.keySet());
JobDataMap map = new JobDataMap();
map.put(RegenEnvEntitlementCertsJob.ENV, e);
map.put(RegenEnvEntitlementCertsJob.CONTENT, demotedContentIds);
map.put(RegenEnvEntitlementCertsJob.LAZY_REGEN, lazyRegen);
JobDetail detail = newJob(RegenEnvEntitlementCertsJob.class)
.withIdentity("regen_entitlement_cert_of_env" + Util.generateUUID())
.usingJobData(map)
.build();
return detail;
}
/**
* To make promotion transactional
* @param contentToPromote
* @param env
* @return contentIds Ids of the promoted content
*/
@Transactional
public Set<String> batchCreate(List<org.candlepin.model.dto.EnvironmentContent> contentToPromote,
Environment env) {
Set<String> contentIds = new HashSet<String>();
for (org.candlepin.model.dto.EnvironmentContent promoteMe : contentToPromote) {
// Make sure the content exists:
EnvironmentContent envcontent = new EnvironmentContent();
envcontent.setEnvironment(env);
envcontent.setContent(this.resolveContent(env, promoteMe.getContentId()));
envcontent.setEnabled(promoteMe.getEnabled());
envContentCurator.create(envcontent);
env.getEnvironmentContent().add(envcontent);
contentIds.add(promoteMe.getContentId());
}
return contentIds;
}
private Environment lookupEnvironment(String envId) {
Environment e = envCurator.find(envId);
if (e == null) {
throw new NotFoundException(i18n.tr(
"No such environment: {0}", envId));
}
return e;
}
@ApiOperation(notes = "Creates an Environment", value = "create")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@SecurityHole(noAuth = true)
@Path("/{env_id}/consumers")
public Consumer create(@PathParam("env_id") String envId,
@ApiParam(name = "consumer", required = true) Consumer consumer,
@Context Principal principal, @QueryParam("username") String userName,
@QueryParam("owner") String ownerKey,
@QueryParam("activation_keys") String activationKeys)
throws BadRequestException {
Environment e = lookupEnvironment(envId);
consumer.setEnvironment(e);
return this.consumerResource.create(consumer, principal, userName, e.getOwner().getKey(),
activationKeys, true);
}
}