/*************************************************************************
* Copyright 2009-2016 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.
*
* This file may incorporate work covered under the following copyright
* and permission notice:
*
* Software License Agreement (BSD License)
*
* Copyright (c) 2008, Regents of the University of California
* All rights reserved.
*
* Redistribution and use of this software in source and binary forms,
* with or without modification, are permitted provided that the
* following conditions are met:
*
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE
* THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL,
* COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE,
* AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING
* IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA,
* SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY,
* WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION,
* REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO
* IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT
* NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS.
************************************************************************/
package com.eucalyptus.blockstorage;
import static com.eucalyptus.compute.common.ImageMetadata.State.available;
import static com.eucalyptus.compute.common.ImageMetadata.State.pending;
import static com.eucalyptus.images.Images.inState;
import java.util.EnumSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceException;
import com.eucalyptus.auth.Accounts;
import com.eucalyptus.auth.principal.UserFullName;
import com.eucalyptus.blockstorage.msgs.CreateStorageSnapshotResponseType;
import com.eucalyptus.blockstorage.msgs.CreateStorageSnapshotType;
import com.eucalyptus.component.annotation.ComponentNamed;
import com.eucalyptus.compute.ClientUnauthorizedComputeException;
import com.eucalyptus.compute.ComputeException;
import com.eucalyptus.compute.common.backend.CreateSnapshotResponseType;
import com.eucalyptus.compute.common.backend.CreateSnapshotType;
import com.eucalyptus.compute.common.backend.DeleteSnapshotResponseType;
import com.eucalyptus.compute.common.backend.DeleteSnapshotType;
import com.eucalyptus.compute.common.backend.ModifySnapshotAttributeResponseType;
import com.eucalyptus.compute.common.backend.ModifySnapshotAttributeType;
import com.eucalyptus.compute.common.backend.ResetSnapshotAttributeResponseType;
import com.eucalyptus.compute.common.backend.ResetSnapshotAttributeType;
import com.eucalyptus.compute.common.internal.account.IdentityIdFormats;
import com.eucalyptus.compute.common.internal.blockstorage.Snapshot;
import com.eucalyptus.compute.common.internal.blockstorage.Snapshots;
import com.eucalyptus.compute.common.internal.blockstorage.State;
import com.eucalyptus.compute.common.internal.blockstorage.Volume;
import com.eucalyptus.util.Callback;
import com.eucalyptus.ws.EucalyptusWebServiceException;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import org.apache.log4j.Logger;
import org.hibernate.exception.ConstraintViolationException;
import com.eucalyptus.auth.AuthException;
import com.eucalyptus.blockstorage.exceptions.SnapshotTooLargeException;
import com.eucalyptus.blockstorage.msgs.DeleteStorageSnapshotResponseType;
import com.eucalyptus.blockstorage.msgs.DeleteStorageSnapshotType;
import com.eucalyptus.compute.common.internal.util.DuplicateMetadataException;
import com.eucalyptus.component.NoSuchComponentException;
import com.eucalyptus.component.Partitions;
import com.eucalyptus.component.ServiceConfiguration;
import com.eucalyptus.component.Topology;
import com.eucalyptus.component.id.Eucalyptus;
import com.eucalyptus.compute.ClientComputeException;
import com.eucalyptus.compute.common.internal.identifier.InvalidResourceIdentifier;
import com.eucalyptus.compute.common.internal.identifier.ResourceIdentifiers;
import com.eucalyptus.context.Context;
import com.eucalyptus.context.Contexts;
import com.eucalyptus.context.IllegalContextAccessException;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.entities.TransactionException;
import com.eucalyptus.entities.Transactions;
import com.eucalyptus.event.ListenerRegistry;
import com.eucalyptus.images.Images;
import com.eucalyptus.records.Logs;
import com.eucalyptus.reporting.event.EventActionInfo;
import com.eucalyptus.reporting.event.SnapShotEvent;
import com.eucalyptus.reporting.event.SnapShotEvent.SnapShotAction;
import com.eucalyptus.system.Threads;
import com.eucalyptus.util.EucalyptusCloudException;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.util.RestrictedTypes;
import com.eucalyptus.util.async.AsyncRequests;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
@ComponentNamed("computeSnapshotManager")
public class SnapshotManager {
static Logger LOG = Logger.getLogger( SnapshotManager.class );
public CreateSnapshotResponseType create( final CreateSnapshotType request ) throws EucalyptusCloudException, NoSuchComponentException, DuplicateMetadataException, AuthException, IllegalContextAccessException, NoSuchElementException, PersistenceException, TransactionException {
final Context ctx = Contexts.lookup( );
final String volumeId = normalizeVolumeIdentifier( request.getVolumeId( ) );
final Volume vol;
try {
vol = Transactions.find( Volume.named( ctx.getUserFullName( ).asAccountFullName( ), volumeId ) );
} catch ( NoSuchElementException e ) {
throw new ClientComputeException( "InvalidVolume.NotFound", "Volume not found '" + request.getVolumeId( ) + "'" );
}
final ServiceConfiguration sc = Topology.lookup( Storage.class, Partitions.lookupByName( vol.getPartition( ) ) );
final Volume volReady = Volumes.checkVolumeReady( vol );
Supplier<Snapshot> allocator = new Supplier<Snapshot>( ) {
@Override
public Snapshot get( ) {
try {
return initializeSnapshot( Accounts.getAuthenticatedArn( ctx.getUser( ) ), ctx.getUserFullName( ), volReady, sc, request.getDescription() );
} catch ( EucalyptusCloudException ex ) {
throw new RuntimeException( ex );
}
}
};
Snapshot snap = RestrictedTypes.allocateUnitlessResource( allocator );
try {
snap = startCreateSnapshot( volReady, snap );
} catch ( Exception e ) {
final EntityTransaction db = Entities.get( Snapshot.class );
try {
Snapshot entity = Entities.uniqueResult( snap );
Entities.delete( entity );
db.commit();
} catch ( Exception ex ) {
Logs.extreme( ).error( ex , ex );
} finally {
if ( db.isActive() ) db.rollback( );
}
final SnapshotTooLargeException snapshotTooLargeException =
Exceptions.findCause( e, SnapshotTooLargeException.class );
if ( snapshotTooLargeException != null ) {
throw new ClientComputeException(
"SnapshotLimitExceeded", snapshotTooLargeException.getMessage( ) );
}
if ( !( e.getCause( ) instanceof ExecutionException ) ) {
throw handleException( e );
} else {
throw e;
}
}
try {
fireUsageEvent( snap, SnapShotEvent.forSnapShotCreate( snap.getVolumeSize(), volReady.getNaturalId(), snap.getDisplayName() ) );
} catch (Throwable reportEx) {
LOG.error("Unable to fire snap shot creation reporting event", reportEx);
}
CreateSnapshotResponseType reply = ( CreateSnapshotResponseType ) request.getReply( );
com.eucalyptus.compute.common.Snapshot snapMsg = snap.morph( new com.eucalyptus.compute.common.Snapshot( ) );
snapMsg.setProgress( "0%" );
snapMsg.setOwnerId( snap.getOwnerAccountNumber( ) );
snapMsg.setVolumeSize( volReady.getSize( ).toString( ) );
reply.setSnapshot( snapMsg );
return reply;
}
public DeleteSnapshotResponseType delete( final DeleteSnapshotType request ) throws EucalyptusCloudException {
final DeleteSnapshotResponseType reply = ( DeleteSnapshotResponseType ) request.getReply( );
final Context ctx = Contexts.lookup( );
final String snapshotId = normalizeSnapshotIdentifier( request.getSnapshotId( ) );
Predicate<Snapshot> deleteSnapshot = new Predicate<Snapshot>( ) {
@Override
public boolean apply( Snapshot snap ) {
if ( !State.EXTANT.equals( snap.getState( ) ) && !State.FAIL.equals( snap.getState( ) ) ) {
return false;
} else if ( !RestrictedTypes.filterPrivileged( ).apply( snap ) ) {
throw Exceptions.toUndeclared( new ClientUnauthorizedComputeException( "Not authorized to delete snapshot " + request.getSnapshotId( ) + " by " + ctx.getUser( ).getName( ) ) );
} else if ( isReservedSnapshot( snapshotId ) ) {
throw Exceptions.toUndeclared( new ClientComputeException( "InvalidSnapshot.InUse", "Snapshot " + request.getSnapshotId( ) + " is in use, deletion not permitted" ) );
} else {
fireUsageEvent(snap, SnapShotEvent.forSnapShotDelete());
final String partition = snap.getPartition();
final String snapshotId = snap.getDisplayName( );
Callable<Boolean> deleteBroadcast = new Callable<Boolean>( ) {
public Boolean call( ) {
final DeleteStorageSnapshotType deleteMsg = new DeleteStorageSnapshotType( snapshotId );
return Iterables.all( Topology.enabledServices( Storage.class ), new Predicate<ServiceConfiguration>( ) {
@Override
public boolean apply( ServiceConfiguration arg0 ) {
if ( !arg0.getPartition( ).equals( partition ) ) {
try {
AsyncRequests.sendSync( arg0, deleteMsg );
} catch ( Exception ex ) {
LOG.error( ex );
Logs.extreme( ).error( ex, ex );
}
}
return true;
}
} );
}
};
ServiceConfiguration sc = null;
try{
sc = Topology.lookup( Storage.class, Partitions.lookupByName( snap.getPartition( ) ) );
}catch(final Exception ex){
sc= null;
}
if(sc!=null){
try {
DeleteStorageSnapshotResponseType scReply = AsyncRequests.sendSync( sc, new DeleteStorageSnapshotType( snap.getDisplayName( ) ) );
if ( scReply.get_return( ) ) {
Threads.enqueue( Eucalyptus.class, Snapshots.class, deleteBroadcast );
} else {
throw Exceptions.toUndeclared( new EucalyptusCloudException( "Unable to delete snapshot: " + snap ) );
}
} catch ( Exception ex1 ) {
throw Exceptions.toUndeclared( ex1.getMessage( ), ex1 );
}
}else{
Threads.enqueue( Eucalyptus.class, Snapshots.class, deleteBroadcast );
}
return true;
}
}
};
boolean result = false;
try {
result = Transactions.delete( Snapshot.named( ctx.getUserFullName( ).asAccountFullName( ), snapshotId ), deleteSnapshot);
} catch ( NoSuchElementException ex2 ) {
try {
result = Transactions.delete( Snapshot.named( null, snapshotId ), deleteSnapshot );
} catch ( ExecutionException ex3 ) {
throw handleException( ex3.getCause() );
} catch ( NoSuchElementException ex4 ) {
throw new ClientComputeException( "InvalidSnapshot.NotFound", "The snapshot '"+request.getSnapshotId( )+"' does not exist." );
}
} catch ( ExecutionException ex1 ) {
throw new EucalyptusCloudException( ex1.getCause( ) );
}
reply.set_return( result );
return reply;
}
public ResetSnapshotAttributeResponseType resetSnapshotAttribute(
final ResetSnapshotAttributeType request
) throws EucalyptusCloudException {
final ResetSnapshotAttributeResponseType reply = request.getReply( );
final Context ctx = Contexts.lookup( );
final String snapshotId = normalizeSnapshotIdentifier( request.getSnapshotId( ) );
final Function<String, Boolean> resetSnapshotAttribute = new Function<String,Boolean>( ) {
@Override
public Boolean apply( final String snapshotId ) {
try {
final Snapshot snap = Entities.uniqueResult( Snapshot.named(
ctx.isAdministrator( ) ? null : ctx.getUserFullName( ).asAccountFullName( ),
snapshotId ) );
//Can only reset attributes of snapshots in 'creating' or 'available' state
if ( !State.EXTANT.equals( snap.getState() ) && !State.GENERATING.equals( snap.getState() ) ) {
return false;
} else if ( !canModifySnapshot( snap ) ) {
throw Exceptions.toUndeclared( new ClientComputeException(
"AuthFailure",
"Not authorized to reset attribute for snapshot " + request.getSnapshotId( ) ) );
} else if ( request.getCreateVolumePermission() != null &&
( "".equals( request.getCreateVolumePermission() ) ||
"createVolumePermission".equals( request.getCreateVolumePermission() ) ) ) {
snap.setSnapshotPublic( false );
snap.setPermissions( Sets.<String>newHashSet() );
}
return true;
} catch ( TransactionException e ) {
throw Exceptions.toUndeclared( e );
}
}
};
try {
reply.set_return( Entities.asDistinctTransaction( Snapshot.class, resetSnapshotAttribute ).apply( snapshotId ) );
} catch ( NoSuchElementException e ) {
throw new ClientComputeException(
"InvalidSnapshot.NotFound",
"The snapshot '"+request.getSnapshotId( )+"' does not exist." );
} catch ( Exception e ) {
Exceptions.findAndRethrow( e, EucalyptusCloudException.class, EucalyptusWebServiceException.class );
throw new EucalyptusCloudException( Objects.firstNonNull( e.getCause( ), e ) );
}
return reply;
}
private static List<String> verifyAccountIds( final List<String> accountIds ) throws EucalyptusCloudException {
for ( String userId : accountIds ) {
if ( !Accounts.isAccountNumber( userId ) ) {
throw new EucalyptusCloudException( "Not a valid userId : " + userId );
}
}
return Lists.newArrayList( accountIds );
}
private static boolean canModifySnapshot( final Snapshot snap ) {
final Context ctx = Contexts.lookup( );
final String requestAccountId = ctx.getUserFullName( ).getAccountNumber( );
return
( ctx.isAdministrator( ) || snap.getOwnerAccountNumber( ).equals( requestAccountId ) ) &&
RestrictedTypes.filterPrivileged( ).apply( snap );
}
public ModifySnapshotAttributeResponseType modifySnapshotAttribute( final ModifySnapshotAttributeType request ) throws EucalyptusCloudException {
ModifySnapshotAttributeResponseType reply = request.getReply( );
final Context ctx = Contexts.lookup();
final String snapshotId = normalizeSnapshotIdentifier( request.getSnapshotId( ) );
Function<Snapshot, Boolean> modifySnapshotAttribute = Functions.forPredicate(new Predicate<Snapshot>( ) {
@Override
public boolean apply( Snapshot snap ) {
//Can only modify attributes of snapshots in 'creating' or 'available' state
if ( !State.EXTANT.equals( snap.getState( ) ) && !State.GENERATING.equals( snap.getState( ) ) ) {
return false;
} else if ( !canModifySnapshot(snap)) {
throw Exceptions.toUndeclared( new EucalyptusCloudException( "Not authorized to modify attribute for snapshot " + request.getSnapshotId( ) + " by " + ctx.getUser( ).getName( ) ) );
} else {
switch ( request.snapshotAttribute() ) {
case CreateVolumePermission:
//do adds
try {
snap.addPermissions(verifyAccountIds(request.addUserIds()));
} catch (EucalyptusCloudException e) {
LOG.warn("Failed validating accountIds", e);
throw Exceptions.toUndeclared(e);
}
if (request.addGroupAll()) {
snap.setSnapshotPublic(true);
}
//do removes
snap.removePermissions(request.removeUserIds());
if (request.removeGroupAll()) {
snap.setSnapshotPublic(false);
}
break;
case ProductCode:
for ( String productCode : request.getProductCodes( ) ) {
snap.addProductCode( productCode );
}
break;
}
return true;
}
}
});
final boolean result;
try {
result = Transactions.one(
Snapshot.named(
ctx.isAdministrator( ) ? null : ctx.getUserFullName( ).asAccountFullName( ),
snapshotId ),
modifySnapshotAttribute );
} catch ( NoSuchElementException ex2 ) {
throw new ClientComputeException( "InvalidSnapshot.NotFound", "The snapshot '"+request.getSnapshotId( )+"' does not exist." );
} catch ( ExecutionException ex1 ) {
throw new EucalyptusCloudException( ex1.getCause( ) );
}
reply.set_return( result );
return reply;
}
private static boolean isReservedSnapshot( final String snapshotId ) {
// Fix for EUCA-4932. Any snapshot associated with an (available or pending) image as a root/non-root device is a reserved snapshot
// and can't be deleted without first unregistering the image
return Predicates.or( SnapshotInUseVerifier.INSTANCE ).apply( snapshotId );
}
private static Snapshot initializeSnapshot( final String authenticatedArn,
final UserFullName userFullName,
final Volume vol,
final ServiceConfiguration sc,
final String description ) throws EucalyptusCloudException {
final EntityTransaction db = Entities.get( Snapshot.class );
try {
while ( true ) {
final String newId = IdentityIdFormats.generate( authenticatedArn, Snapshot.ID_PREFIX );
try {
Entities.uniqueResult( Snapshot.named( null, newId ) );
} catch ( NoSuchElementException e ) {
final Snapshot snap = new Snapshot( userFullName, newId, description, vol.getDisplayName( ), vol.getSize( ), sc.getName( ), sc.getPartition( ) );
Entities.persist( snap );
db.commit( );
return snap;
}
}
} catch ( Exception ex ) {
db.rollback( );
throw new EucalyptusCloudException( "Failed to initialize snapshot state because of: " + ex.getMessage( ), ex );
}
}
private static Snapshot startCreateSnapshot( final Volume vol,
final Snapshot snap ) throws EucalyptusCloudException, DuplicateMetadataException {
final ServiceConfiguration sc = Topology.lookup( Storage.class, Partitions.lookupByName( vol.getPartition( ) ) );
try {
Snapshot snapState = Transactions.save( snap, new Callback<Snapshot>( ) {
@Override
public void fire( Snapshot s ) {
String scSnapStatus = null;
try {
CreateStorageSnapshotType scRequest = new CreateStorageSnapshotType( vol.getDisplayName( ), snap.getDisplayName( ) );
CreateStorageSnapshotResponseType scReply = AsyncRequests.sendSync( sc, scRequest );
StorageUtil.setMappedState( s, scReply.getStatus( ) );
scSnapStatus = scReply.getStatus();
} catch ( Exception ex ) {
throw Exceptions.toUndeclared( ex );
}
}
} );
} catch ( ConstraintViolationException ex ) {
throw new DuplicateMetadataException( "Duplicate snapshot creation: " + snap + ": " + ex.getMessage( ), ex );
} catch ( ExecutionException ex ) {
LOG.error( ex.getCause( ), ex.getCause( ) );
throw new EucalyptusCloudException( ex );
}
return snap;
}
private static String normalizeIdentifier( final String identifier,
final String prefix,
final boolean required,
final String message ) throws ClientComputeException {
try {
return Strings.emptyToNull( identifier ) == null && !required ?
null :
ResourceIdentifiers.parse( prefix, identifier ).getIdentifier( );
} catch ( final InvalidResourceIdentifier e ) {
throw new ClientComputeException( "InvalidParameterValue", String.format( message, e.getIdentifier( ) ) );
}
}
private static String normalizeSnapshotIdentifier( final String identifier ) throws EucalyptusCloudException {
return normalizeIdentifier(
identifier, Snapshot.ID_PREFIX, true, "Value (%s) for parameter snapshotId is invalid. Expected: 'snap-...'." );
}
private static String normalizeVolumeIdentifier( final String identifier ) throws EucalyptusCloudException {
return normalizeIdentifier(
identifier, Volume.ID_PREFIX, true, "Value (%s) for parameter volume is invalid. Expected: 'vol-...'." );
}
private enum ImageSnapshotReservation implements Predicate<String> {
INSTANCE;
@Override
public boolean apply( final String identifier ) {
return Iterables.any(
Entities.query(Images.exampleBlockStorageWithSnapshotId(identifier), true),
inState(EnumSet.of( pending, available ) ) );
}
}
/**
* <p>Predicate to check if a snapshot is associated with a pending or available boot from ebs image.
* Returns true if the snapshot is used in the image registration with root or non root devices.</p>
*/
private enum SnapshotInUseVerifier implements Predicate<String> {
INSTANCE;
@Override
public boolean apply( final String identifier ) {
return Iterables.any(
Entities.query(Images.exampleBSDMappingWithSnapshotId(identifier), true),
Images.imageInState(EnumSet.of( pending, available ) ) );
}
}
private static void fireUsageEvent( final Snapshot snap,
final EventActionInfo<SnapShotAction> actionInfo ) {
try {
ListenerRegistry.getInstance().fireEvent(
SnapShotEvent.with( actionInfo, snap.getNaturalId( ), snap.getDisplayName( ), snap.getOwnerUserId( ), snap.getOwnerUserName( ), snap.getOwnerAccountNumber( ) ) );
} catch (final Throwable e) {
LOG.error(e, e);
}
}
/**
* Method always throws, signature allows use of "throw handleException ..."
*/
private static ComputeException handleException( final Throwable e ) throws ComputeException {
final ComputeException cause = Exceptions.findCause( e, ComputeException.class );
if ( cause != null ) {
throw cause;
}
LOG.error( e, e );
final ComputeException exception = new ComputeException( "InternalError", String.valueOf( e.getMessage( ) ) );
if ( Contexts.lookup( ).hasAdministrativePrivileges() ) {
exception.initCause( e );
}
throw exception;
}
}