/*************************************************************************
* Copyright 2009-2014 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.tags;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import javax.annotation.Nullable;
import org.apache.log4j.Logger;
import com.eucalyptus.auth.principal.AccountFullName;
import com.eucalyptus.auth.principal.UserFullName;
import com.eucalyptus.component.annotation.ComponentNamed;
import com.eucalyptus.compute.common.CloudMetadata;
import com.eucalyptus.compute.common.ImageMetadata;
import com.eucalyptus.compute.common.internal.tags.Tag;
import com.eucalyptus.compute.common.internal.tags.TagSupport;
import com.eucalyptus.compute.common.internal.tags.Tags;
import com.eucalyptus.compute.common.internal.util.MetadataException;
import com.eucalyptus.compute.common.internal.util.NoSuchMetadataException;
import com.eucalyptus.compute.ClientComputeException;
import com.eucalyptus.compute.ComputeException;
import com.eucalyptus.compute.common.internal.identifier.ResourceIdentifiers;
import com.eucalyptus.configurable.ConfigurableClass;
import com.eucalyptus.configurable.ConfigurableField;
import com.eucalyptus.context.Context;
import com.eucalyptus.context.Contexts;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.entities.TransactionException;
import com.eucalyptus.compute.common.internal.images.ImageInfo;
import com.eucalyptus.images.Images;
import com.eucalyptus.util.EucalyptusCloudException;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.auth.principal.OwnerFullName;
import com.eucalyptus.auth.type.RestrictedType;
import com.eucalyptus.util.RestrictedTypes;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import edu.ucsb.eucalyptus.cloud.InvalidParameterValueException;
import com.eucalyptus.compute.common.backend.CreateTagsResponseType;
import com.eucalyptus.compute.common.backend.CreateTagsType;
import com.eucalyptus.compute.common.backend.DeleteTagsResponseType;
import com.eucalyptus.compute.common.backend.DeleteTagsType;
import com.eucalyptus.compute.common.DeleteResourceTag;
import com.eucalyptus.compute.common.ResourceTag;
/**
* Service implementation for Tag operations
*/
@ConfigurableClass( root = "tagging", description = "Parameters controlling tagging")
@ComponentNamed("computeTagManager")
public class TagManager {
private static final Logger log = Logger.getLogger( TagManager.class );
@ConfigurableField(initial = "10", description = "The maximum number of tags per resource for each account")
public static long MAX_TAGS_PER_RESOURCE = 50;
public CreateTagsResponseType createTags( final CreateTagsType request ) throws EucalyptusCloudException {
final CreateTagsResponseType reply = request.getReply( );
reply.set_return( false );
final Context context = Contexts.lookup();
final UserFullName userFullName = context.getUserFullName();
final AccountFullName accountFullName = userFullName.asAccountFullName();
final List<String> resourceIds = MoreObjects.firstNonNull( request.getResourcesSet(), Collections.<String>emptyList() );
final List<ResourceTag> resourceTags = MoreObjects.firstNonNull( request.getTagSet(), Collections.<ResourceTag>emptyList() );
try {
TagHelper.validateTags( resourceTags );
} catch ( MetadataException e ) {
throw new ClientComputeException( "InvalidParameterValue", e.getMessage( ) );
}
if ( resourceTags.size() > 0 && resourceIds.size() > 0 ) {
final Predicate<Void> creator = new Predicate<Void>(){
@Override
public boolean apply( final Void v ) {
final List<CloudMetadata> resources = Lists.transform( resourceIds, resourceLookup(true) );
if ( !Iterables.all( resources, Predicates.and( Predicates.notNull(), typeSpecificFilters(), permissionsFilter() ) ) ) {
return false;
}
TagHelper.createOrUpdateTags( userFullName, resources, resourceTags );
return true;
}
};
try {
reply.set_return( Entities.asTransaction( Tag.class, creator ).apply( null ) );
} catch ( TagLimitException e ) {
throw new ClientComputeException( "TagLimitExceeded", "The maximum number of Tags for a resource has been reached." );
} catch ( RuntimeException e ) {
handleException( e );
}
}
return reply;
}
public DeleteTagsResponseType deleteTags( final DeleteTagsType request ) throws EucalyptusCloudException {
final DeleteTagsResponseType reply = request.getReply( );
reply.set_return( false );
final Context context = Contexts.lookup();
final OwnerFullName ownerFullName = context.getUserFullName().asAccountFullName();
final List<String> resourceIds = MoreObjects.firstNonNull( request.getResourcesSet(), Collections.<String>emptyList() );
final List<DeleteResourceTag> resourceTags = MoreObjects.firstNonNull( request.getTagSet(), Collections.<DeleteResourceTag>emptyList() );
for ( final DeleteResourceTag resourceTag : resourceTags ) {
final String key = resourceTag.getKey();
if ( Strings.isNullOrEmpty( key ) || key.trim().length() > 128 || TagHelper.isReserved( key ) ) {
throw new InvalidParameterValueException(
"Invalid key (max length 128, must not be empty, reserved prefixes "+TagHelper.describeReserved()+"): "+key );
}
}
if ( resourceIds.size() > 0 && resourceIds.size() > 0 ) {
final Predicate<Void> delete = new Predicate<Void>(){
@Override
public boolean apply( final Void v ) {
final Iterable<CloudMetadata> resources = Iterables.filter( Iterables.transform( resourceIds, resourceLookup(false) ), Predicates.notNull() );
if ( !Iterables.all( resources, Predicates.and( Predicates.notNull(), typeSpecificFilters(), permissionsFilter() ) ) ) {
return false;
}
for ( final CloudMetadata resource : resources ) {
for ( final DeleteResourceTag resourceTag : resourceTags ) {
try {
final Tag example = TagSupport.fromResource( resource ).example( resource, ownerFullName, resourceTag.getKey(), resourceTag.getValue() );
Tags.delete( example );
} catch ( NoSuchMetadataException e ) {
log.trace( e );
}
}
}
return true;
}
};
try {
reply.set_return( Entities.asTransaction( Tag.class, delete ).apply( null ) );
} catch ( RuntimeException e ) {
handleException( e );
}
}
return reply;
}
private static Predicate<Object> typeSpecificFilters() {
return TypeSpecificFilters.INSTANCE;
}
private static Predicate<RestrictedType> permissionsFilter() {
return PermissionsFilter.INSTANCE;
}
/**
* A function to lookup cloud metadata by resource identifier.
*
* The returned function may return null values.
*/
private static Function<String, CloudMetadata> resourceLookup( final boolean required ) {
return new Function<String,CloudMetadata>() {
@Override
public CloudMetadata apply( final String resourceId ) {
final TagSupport tagSupport = TagSupport.fromIdentifier( resourceId );
try {
if ( tagSupport != null && resourceId.matches( "[a-z]{1,32}-[0-9a-fA-F]{8}(?:[0-9a-fA-F]{9})?" )) {
return tagSupport.lookup( ResourceIdentifiers.tryNormalize( ).apply( resourceId ) );
} else {
throw Exceptions.toUndeclared( new ClientComputeException(
"InvalidID",
String.format( "The ID '%s' is not valid", resourceId ) ) );
}
} catch ( TransactionException e ) {
throw Exceptions.toUndeclared( e );
} catch ( NoSuchElementException e ) {
if ( required ) {
throw Exceptions.toUndeclared( new ClientComputeException(
tagSupport.getNotFoundErrorCode( ),
String.format( tagSupport.getNotFoundFormatString( ), resourceId ) ) );
}
}
return null;
}
};
}
private static void handleException( final RuntimeException e ) throws EucalyptusCloudException {
final ComputeException computeException = Exceptions.findCause( e, ComputeException.class );
if ( computeException != null ) {
throw computeException;
}
throw e;
}
private static enum PermissionsFilter implements Predicate<RestrictedType> {
INSTANCE;
@Override
public boolean apply( final RestrictedType metadata ) {
if ( metadata instanceof ImageMetadata ) {
return RestrictedTypes.filterPrivilegedWithoutOwner( ).apply( metadata );
} else {
return RestrictedTypes.filterPrivileged( ).apply( metadata );
}
}
}
/**
* Access filtering using type specific approach (e.g. launch permissions for images)
*/
private static enum TypeSpecificFilters implements Predicate<Object> {
INSTANCE;
private List<Predicate<Object>> predicates = ImmutableList.of(
typedPredicate( Images.FilterPermissions.INSTANCE, ImageInfo.class )
);
private <PT> Predicate<Object> typedPredicate( final Predicate<? super PT> predicate,
final Class<PT> predicateTarget ) {
return new Predicate<Object>() {
@Override
public boolean apply( @Nullable final Object object ) {
return
!predicateTarget.isInstance( object ) ||
predicate.apply( predicateTarget.cast( object ) );
}
};
}
@Override
public boolean apply( final Object object ) {
return Predicates.and( predicates ).apply( object );
}
}
}