/************************************************************************* * Copyright 2009-2015 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.cloud.run; import static com.eucalyptus.images.Images.DeviceMappingValidationOption.AllowEbsMapping; import static com.eucalyptus.images.Images.DeviceMappingValidationOption.AllowSuppressMapping; import static com.eucalyptus.images.Images.DeviceMappingValidationOption.SkipExtraEphemeral; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.eucalyptus.auth.Accounts; import com.eucalyptus.auth.AuthContextSupplier; import com.eucalyptus.auth.AuthException; import com.eucalyptus.auth.Permissions; import com.eucalyptus.auth.euare.common.policy.IamPolicySpec; import com.eucalyptus.auth.policy.PolicySpec; import com.eucalyptus.auth.policy.ern.Ern; import com.eucalyptus.auth.policy.ern.EuareResourceName; import com.eucalyptus.auth.principal.AccountFullName; import com.eucalyptus.auth.principal.InstanceProfile; import com.eucalyptus.auth.principal.Role; import com.eucalyptus.auth.principal.UserFullName; import com.eucalyptus.cluster.Clusters; import com.eucalyptus.compute.common.ResourceTag; import com.eucalyptus.compute.common.internal.util.InvalidInstanceProfileMetadataException; import com.eucalyptus.compute.common.BlockDeviceMappingItemType; import com.eucalyptus.compute.common.ImageMetadata.Platform; import com.eucalyptus.compute.common.backend.RunInstancesType; import com.eucalyptus.cloud.VmInstanceLifecycleHelpers; import com.eucalyptus.cloud.run.Allocations.Allocation; import com.eucalyptus.compute.common.internal.util.IllegalMetadataAccessException; import com.eucalyptus.compute.common.internal.util.InvalidMetadataException; import com.eucalyptus.compute.common.internal.util.MetadataException; import com.eucalyptus.component.Partition; import com.eucalyptus.component.Partitions; import com.eucalyptus.compute.common.internal.images.BlockStorageImageInfo; import com.eucalyptus.compute.common.internal.images.BootableImageInfo; import com.eucalyptus.compute.common.internal.images.DeviceMapping; import com.eucalyptus.images.Emis; import com.eucalyptus.images.Emis.BootableSet; import com.eucalyptus.compute.common.internal.images.ImageInfo; import com.eucalyptus.images.Images; import com.eucalyptus.compute.common.internal.keys.KeyPairs; import com.eucalyptus.compute.common.internal.keys.SshKeyPair; import com.eucalyptus.tags.TagHelper; import com.eucalyptus.util.Exceptions; import com.eucalyptus.util.RestrictedTypes; import com.eucalyptus.vm.VmInstances; import com.eucalyptus.compute.common.internal.vmtypes.VmType; import com.eucalyptus.vmtypes.VmTypes; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import net.sf.json.JSONException; public class VerifyMetadata { private static Logger LOG = Logger.getLogger( VerifyMetadata.class ); private static final long BYTES_PER_GB = ( 1024L * 1024L * 1024L ); public static Predicate<Allocation> get( ) { return Predicates.and( Lists.transform( verifiers, AsPredicate.INSTANCE ) ); } private interface MetadataVerifier { public abstract boolean apply( Allocation allocInfo ) throws MetadataException, AuthException, VerificationException; } private static final ArrayList<? extends MetadataVerifier> verifiers = Lists.newArrayList( VmTypeVerifier.INSTANCE, PartitionVerifier.INSTANCE, ImageVerifier.INSTANCE, KeyPairVerifier.INSTANCE, NetworkResourceVerifier.INSTANCE, RoleVerifier.INSTANCE, BlockDeviceMapVerifier.INSTANCE, UserDataVerifier.INSTANCE, ResourceTagVerifier.INSTANCE ); private enum AsPredicate implements Function<MetadataVerifier, Predicate<Allocation>> { INSTANCE; @Override public Predicate<Allocation> apply( final MetadataVerifier arg0 ) { return new Predicate<Allocation>( ) { @Override public boolean apply( Allocation allocInfo ) { try { return arg0.apply( allocInfo ); } catch ( Exception ex ) { throw Exceptions.toUndeclared( ex ); } } }; } } enum VmTypeVerifier implements MetadataVerifier { INSTANCE; @Override public boolean apply( Allocation allocInfo ) throws MetadataException { String instanceType = allocInfo.getRequest( ).getInstanceType( ); VmType vmType = VmTypes.lookup( instanceType ); if ( !RestrictedTypes.filterPrivileged( ).apply( vmType ) ) { throw new IllegalMetadataAccessException( "Not authorized to allocate vm type " + instanceType + " for " + allocInfo.getOwnerFullName() ); } allocInfo.setVmType( vmType ); return true; } } enum PartitionVerifier implements MetadataVerifier { INSTANCE; @Override public boolean apply( Allocation allocInfo ) throws MetadataException { RunInstancesType request = allocInfo.getRequest( ); String zoneName = request.getAvailabilityZone( ); if ( Clusters.list( ).isEmpty( ) ) { LOG.debug( "enabled values: " + Joiner.on( "\n" ).join( Clusters.list( ) ) ); LOG.debug( "disabled values: " + Joiner.on( "\n" ).join( Clusters.listDisabled( ) ) ); throw new VerificationException( "Not enough resources: no cluster controller is currently available to run instances." ); } else if ( Partitions.exists( zoneName ) ) { Partition partition = Partitions.lookupByName( zoneName ); allocInfo.setPartition( partition ); } else if ( Partition.DEFAULT_NAME.equals( zoneName ) ) { Partition partition = Partition.DEFAULT; allocInfo.setPartition( partition ); } else { throw new VerificationException( "Not enough resources: no cluster controller is currently available to run instances." ); } return true; } } enum ImageVerifier implements MetadataVerifier { INSTANCE; @Override public boolean apply( Allocation allocInfo ) throws MetadataException, AuthException, VerificationException { RunInstancesType msg = allocInfo.getRequest( ); String imageId = msg.getImageId( ); VmType vmType = allocInfo.getVmType( ); try { BootableSet bootSet = Emis.newBootableSet( imageId ); allocInfo.setBootableSet( bootSet ); // Add (1024L * 1024L * 10) to handle NTFS min requirements. if ( ! bootSet.isBlockStorage( ) ) { if ( Platform.windows.equals( bootSet.getMachine( ).getPlatform( ) ) && bootSet.getMachine( ).getImageSizeBytes( ) > ( ( 1024L * 1024L * 1024L * vmType.getDisk( ) ) + ( 1024L * 1024L * 10 ) ) ) { throw new ImageInstanceTypeVerificationException( "Unable to run instance " + bootSet.getMachine( ).getDisplayName( ) + " in which the size " + bootSet.getMachine( ).getImageSizeBytes( ) + " bytes of the instance is greater than the vmType " + vmType.getDisplayName( ) + " size " + vmType.getDisk( ) + " GB." ); } else if ( bootSet.getMachine( ).getImageSizeBytes( ) > ( ( 1024L * 1024L * 1024L * vmType.getDisk( ) ) ) ) { throw new ImageInstanceTypeVerificationException( "Unable to run instance " + bootSet.getMachine( ).getDisplayName( ) + " in which the size " + bootSet.getMachine( ).getImageSizeBytes( ) + " bytes of the instance is greater than the vmType " + vmType.getDisplayName( ) + " size " + vmType.getDisk( ) + " GB." ); } } } catch ( VerificationException e ) { throw e; } catch ( MetadataException ex ) { LOG.error( ex ); throw ex; } catch ( RuntimeException ex ) { LOG.error( ex ); throw new VerificationException( "Failed to verify references for request: " + msg.toSimpleString( ) + " because of: " + ex.getMessage( ), ex ); } return true; } } enum KeyPairVerifier implements MetadataVerifier { INSTANCE; @Override public boolean apply( Allocation allocInfo ) throws MetadataException { if ( allocInfo.getRequest( ).getKeyName( ) == null || "".equals( allocInfo.getRequest( ).getKeyName( ) ) ) { allocInfo.setSshKeyPair( KeyPairs.noKey( ) ); return true; } UserFullName ownerFullName = allocInfo.getOwnerFullName( ); RunInstancesType request = allocInfo.getRequest( ); String keyName = request.getKeyName( ); SshKeyPair key = KeyPairs.lookup( ownerFullName.asAccountFullName(), keyName ); if ( !RestrictedTypes.filterPrivileged( ).apply( key ) ) { throw new IllegalMetadataAccessException( "Not authorized to use keypair " + keyName + " by " + ownerFullName.getUserName() ); } allocInfo.setSshKeyPair( key ); return true; } } enum NetworkResourceVerifier implements MetadataVerifier { INSTANCE; @Override public boolean apply( Allocation allocInfo ) throws MetadataException { VmInstanceLifecycleHelpers.get( ).verifyAllocation( allocInfo ); return true; } } enum RoleVerifier implements MetadataVerifier { INSTANCE; @Override public boolean apply( final Allocation allocInfo ) throws MetadataException { final UserFullName ownerFullName = allocInfo.getOwnerFullName(); final String instanceProfileArn = allocInfo.getRequest( ).getIamInstanceProfileArn( ); final String instanceProfileName = allocInfo.getRequest( ).getIamInstanceProfileName( ); if ( !Strings.isNullOrEmpty( instanceProfileArn ) || !Strings.isNullOrEmpty( instanceProfileName ) ) { final String profileAccount; final String profileName; if ( !Strings.isNullOrEmpty( instanceProfileArn ) ) try { final Ern name = Ern.parse( instanceProfileArn ); if ( !( name instanceof EuareResourceName) ) { throw new InvalidInstanceProfileMetadataException( "Invalid IAM instance profile ARN: " + instanceProfileArn ); } profileAccount = name.getAccount( ); profileName = ((EuareResourceName) name).getName( ); } catch ( JSONException e ) { throw new InvalidInstanceProfileMetadataException( "Invalid IAM instance profile ARN: " + instanceProfileArn, e ); } else { profileAccount = ownerFullName.getAccountNumber( ); profileName = instanceProfileName; } final InstanceProfile profile; try { profile = Accounts.lookupInstanceProfileByName( profileAccount, profileName ); } catch ( AuthException e ) { throw new InvalidInstanceProfileMetadataException( "Invalid IAM instance profile: " + profileAccount + "/" + profileName, e ); } if ( !Strings.isNullOrEmpty( instanceProfileName ) && !instanceProfileName.equals( profile.getName( ) ) ) { throw new InvalidInstanceProfileMetadataException( String.format( "Invalid IAM instance profile name '%s' for ARN: %s", profileName, instanceProfileArn) ); } try { final AuthContextSupplier user = allocInfo.getAuthContext( ); if ( !Permissions.isAuthorized( IamPolicySpec.VENDOR_IAM, PolicySpec.IAM_RESOURCE_INSTANCE_PROFILE, Accounts.getInstanceProfileFullName( profile ), AccountFullName.getInstance( profile.getAccountNumber( ) ), IamPolicySpec.IAM_LISTINSTANCEPROFILES, user ) ) { throw new IllegalMetadataAccessException( String.format( "Not authorized to access instance profile with ARN %s for %s", profile.getInstanceProfileArn( ), ownerFullName ) ); } final Role role = profile.getRole( ); if ( role != null && !Permissions.isAuthorized( IamPolicySpec.VENDOR_IAM, PolicySpec.IAM_RESOURCE_ROLE, Accounts.getRoleFullName( role ), AccountFullName.getInstance( role.getAccountNumber( ) ), IamPolicySpec.IAM_PASSROLE, user ) ) { throw new IllegalMetadataAccessException( String.format( "Not authorized to pass role with ARN %s for %s", role.getRoleArn( ), ownerFullName ) ); } if ( role != null ) { allocInfo.setInstanceProfileArn( profile.getInstanceProfileArn( ) ); allocInfo.setIamInstanceProfileId( profile.getInstanceProfileId( ) ); allocInfo.setIamRoleArn( role.getRoleArn( ) ); } else { throw new InvalidInstanceProfileMetadataException( "Role not found for IAM instance profile ARN: " + profile.getInstanceProfileArn( ) ); } } catch ( AuthException e ) { throw new MetadataException( "IAM instance profile error", e ); } } return true; } } /** * <p>Verification logic for block device mappings in the run instance request. * Merges device mappings from the image registration with those from the run instance request, the later getting higher priority. * Populates the final set of device mappings for boot from ebs instances only. </p> * <p>Fixes EUCA-4047 and implements EUCA-4786</p> */ enum BlockDeviceMapVerifier implements MetadataVerifier { INSTANCE; @Override public boolean apply( Allocation allocInfo ) throws MetadataException { BootableImageInfo imageInfo = allocInfo.getBootSet().getMachine(); final List<BlockDeviceMappingItemType> instanceMappings = allocInfo.getRequest().getBlockDeviceMapping() != null ? allocInfo.getRequest().getBlockDeviceMapping() : new ArrayList<BlockDeviceMappingItemType>() ; List<DeviceMapping> imageMappings = new ArrayList<DeviceMapping>(((ImageInfo) imageInfo).getDeviceMappings()); // Is this an overkill?? Should I rather go with the above logic. Damn complexity seems same... // probably m * n where m is the number of image mappings and n is the number of instance mappings instanceMappings.addAll(Lists.transform(Lists.newArrayList(Iterables.filter(imageMappings, new Predicate<DeviceMapping>(){ @Override public boolean apply(DeviceMapping arg0) { return !Iterables.any( instanceMappings, Images.findBlockDeviceMappingItempType( arg0.getDeviceName() )); } })), Images.DeviceMappingDetails.INSTANCE)); ArrayList<BlockDeviceMappingItemType> resultedInstanceMappings = new ArrayList<>(); if ( imageInfo instanceof BlockStorageImageInfo ) { //bfebs image if ( !instanceMappings.isEmpty() ) { //Verify all block device mappings. Don't fuss if both snapshot id and volume size are left blank //Ignore (remove) extra ephemerals EUCA-9148 resultedInstanceMappings = Images.validateBlockDeviceMappings( instanceMappings, EnumSet.of( AllowSuppressMapping, AllowEbsMapping, SkipExtraEphemeral ) ); BlockStorageImageInfo bfebsImage = (BlockStorageImageInfo) imageInfo; Integer imageSizeGB = (int) ( bfebsImage.getImageSizeBytes( ) / BYTES_PER_GB ); Integer userRequestedSizeGB = null; // Find the root block device mapping in the run instance request. Validate it BlockDeviceMappingItemType rootBlockDevice = Iterables.find( resultedInstanceMappings, Images.findEbsRootOptionalSnapshot( bfebsImage.getRootDeviceName() ), null ); if( rootBlockDevice != null) { // Ensure that root device is not mapped to a different snapshot, logical device or suppressed. // Verify that the root device size is not smaller than the image size if ( StringUtils.isNotBlank(rootBlockDevice.getEbs().getSnapshotId()) && !StringUtils.equals(rootBlockDevice.getEbs().getSnapshotId(), bfebsImage.getSnapshotId()) ) { throw new InvalidMetadataException( "Snapshot ID cannot be modified for the root device. " + "Source snapshot from the image registration will be used for creating the root device" ); } else if ( StringUtils.isNotBlank(rootBlockDevice.getVirtualName()) ) { throw new InvalidMetadataException( "Logical type cannot be modified for the root device. " + "Source snapshot from the image registration will be used for creating the root device" ); } else if ( rootBlockDevice.getNoDevice() != null ) { throw new InvalidMetadataException( "Root device cannot be suppressed. " + "Source snapshot from the image registration will be used for creating the root device" ); } else if ( (userRequestedSizeGB = rootBlockDevice.getEbs().getVolumeSize() ) != null && userRequestedSizeGB < imageSizeGB ) { throw new InvalidMetadataException("Root device volume cannot be smaller than the image size"); } // Gather all the information for the root device mapping and populate it in the run instance request if( rootBlockDevice.getEbs().getSnapshotId() == null ) { rootBlockDevice.getEbs().setSnapshotId(bfebsImage.getSnapshotId()); } if( rootBlockDevice.getEbs().getVolumeSize() == null ) { rootBlockDevice.getEbs().setVolumeSize(imageSizeGB); } if( rootBlockDevice.getEbs().getDeleteOnTermination() == null ) { rootBlockDevice.getEbs().setDeleteOnTermination(bfebsImage.getDeleteOnTerminate()); } } else { // This should never happen. Root device mapping will always exist in the block storage image and or run instance request throw new InvalidMetadataException("Root block device mapping not found\n"); } } } else { // Instance store image //Verify all block device mappings. EBS mappings must be considered invalid since AWS doesn't support it resultedInstanceMappings = Images.validateBlockDeviceMappings( instanceMappings, EnumSet.of( AllowSuppressMapping, SkipExtraEphemeral ) ); } // Set the final list of block device mappings in the run instance request (necessary if the instance mappings were null). Checked with grze that its okay allocInfo.getRequest().setBlockDeviceMapping(resultedInstanceMappings); return true; } } enum UserDataVerifier implements MetadataVerifier { INSTANCE; @Override public boolean apply( Allocation allocInfo ) throws MetadataException { byte[] userData = allocInfo.getUserData(); if (userData != null && userData.length > Integer.parseInt(VmInstances.USER_DATA_MAX_SIZE_KB) * 1024) { throw new InvalidMetadataException("User data may not exceed " + VmInstances.USER_DATA_MAX_SIZE_KB + " KB"); } return true; } } enum ResourceTagVerifier implements MetadataVerifier { INSTANCE; @Override public boolean apply( Allocation allocInfo ) throws MetadataException { TagHelper.validateTagSpecifications( allocInfo.getRequest( ).getTagSpecification( ) ); final AuthContextSupplier authContext; try { authContext = allocInfo.getAuthContext( ); } catch ( AuthException e ) { throw new MetadataException( "Tag validation error", e ); } final UserFullName userFullName = allocInfo.getOwnerFullName( ); final AccountFullName accountFullName = userFullName.asAccountFullName( ); for ( final String resource : PolicySpec.EC2_RESOURCES ) { final List<ResourceTag> resourceTags = TagHelper.tagsForResource( allocInfo.getRequest( ).getTagSpecification( ), resource ); if ( !resourceTags.isEmpty( ) ) { if ( !TagHelper.createTagsAuthorized( authContext, accountFullName, resource ) ) { throw new IllegalMetadataAccessException( "Not authorized to create "+resource+" tags by " + userFullName.getUserName( ) ); } } } return true; } } public static class ImageInstanceTypeVerificationException extends VerificationException { private static final long serialVersionUID = -1L; public ImageInstanceTypeVerificationException( final String message ) { super( message ); } } }