/************************************************************************* * 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.address; import static com.eucalyptus.compute.common.internal.vm.VmInstance.VmStateSet; import java.util.Collections; import java.util.NoSuchElementException; import javax.inject.Inject; import org.apache.log4j.Logger; import com.eucalyptus.auth.principal.AccountFullName; import com.eucalyptus.auth.principal.Principals; import com.eucalyptus.component.annotation.ComponentNamed; import com.eucalyptus.compute.ClientComputeException; import com.eucalyptus.compute.common.AddressInfoType; import com.eucalyptus.compute.common.CloudMetadatas; import com.eucalyptus.compute.common.internal.address.AddressDomain; import com.eucalyptus.compute.common.internal.identifier.InvalidResourceIdentifier; import com.eucalyptus.compute.common.internal.identifier.ResourceIdentifiers; import com.eucalyptus.compute.common.internal.util.NotEnoughResourcesException; import com.eucalyptus.compute.common.internal.vpc.InternetGateways; import com.eucalyptus.compute.common.internal.vpc.NetworkInterface; import com.eucalyptus.compute.vpc.NetworkInterfaceHelper; import com.eucalyptus.compute.common.internal.vpc.Vpc; import com.eucalyptus.compute.common.internal.vpc.VpcMetadataNotFoundException; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.entities.Entities; import com.eucalyptus.entities.TransactionResource; import com.eucalyptus.network.PublicAddresses; import com.eucalyptus.records.Logs; import com.eucalyptus.compute.common.internal.tags.Filters; import com.eucalyptus.util.EucalyptusCloudException; import com.eucalyptus.util.FUtils; import com.eucalyptus.util.RestrictedTypes; import com.eucalyptus.util.TypeMappers; import com.eucalyptus.compute.common.internal.vm.VmInstance; import com.eucalyptus.vm.VmInstances; import com.eucalyptus.compute.common.internal.vm.VmNetworkConfig; import com.google.common.base.Objects; import com.google.common.base.Optional; 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.eucalyptus.compute.common.backend.AllocateAddressResponseType; import com.eucalyptus.compute.common.backend.AllocateAddressType; import com.eucalyptus.compute.common.backend.AssociateAddressResponseType; import com.eucalyptus.compute.common.backend.AssociateAddressType; import com.eucalyptus.compute.common.backend.DescribeAddressesResponseType; import com.eucalyptus.compute.common.backend.DescribeAddressesType; import com.eucalyptus.compute.common.backend.DisassociateAddressResponseType; import com.eucalyptus.compute.common.backend.DisassociateAddressType; import com.eucalyptus.compute.common.backend.ReleaseAddressResponseType; import com.eucalyptus.compute.common.backend.ReleaseAddressType; @ComponentNamed("computeAddressManager") public class AddressManager { private static final Logger LOG = Logger.getLogger( AddressManager.class ); private final InternetGateways internetGateways; private final Addresses addresses; @Inject public AddressManager( final AllocatedAddressPersistence allocatedAddressPersistence, final InternetGateways internetGateways ) { this.addresses = new Addresses( allocatedAddressPersistence ); this.internetGateways = internetGateways; } public AllocateAddressResponseType allocateAddress( final AllocateAddressType request ) throws Exception { final AllocateAddressResponseType reply = request.getReply( ); try { final String defaultVpcId = getDefaultVpcId( ); final AddressDomain domain = Optional.fromNullable( request.getDomain( ) ) .transform( FUtils.valueOfFunction( AddressDomain.class ) ) .or( defaultVpcId != null ? AddressDomain.vpc : AddressDomain.standard ); final Addresses.Allocator allocator = addresses.allocator( domain ); final Address address = RestrictedTypes.allocateNamedUnitlessResources( 1, allocator, allocator ).get( 0 ); reply.setPublicIp( address.getName( ) ); reply.setAllocationId( address.getAllocationId( ) ); reply.setDomain( domain.name( ) ); } catch( final RuntimeException e ) { if( e.getCause( ) != null ) { throw new EucalyptusCloudException( e.getCause() ); } else { throw new EucalyptusCloudException( "couldn't allocate addresses" ); } } return reply; } public ReleaseAddressResponseType releaseAddress( final ReleaseAddressType request ) throws Exception { final ReleaseAddressResponseType reply = request.getReply( ).markFailed( ); if ( request.getPublicIp( ) == null && request.getAllocationId( ) == null ) { throw new ClientComputeException( "MissingParameter", "PublicIp or AllocationId required" ); } final String allocationId = ResourceIdentifiers.tryNormalize().apply( request.getAllocationId( ) ); final Address address; try { address = RestrictedTypes.doPrivileged( Objects.firstNonNull( request.getPublicIp( ), allocationId ), Address.class ); } catch ( NoSuchElementException e ) { if ( request.getAllocationId( ) != null ) { throw new ClientComputeException( "InvalidAddressID.NotFound", "Address not found for allocation ("+request.getAllocationId( )+")" ); } throw e; } try { addresses.release( address, address.getDomain( ) == AddressDomain.vpc ? allocationId != null ? allocationId : address.getAllocationId( ) : null ); } catch ( IllegalStateException e ) { throw new ClientComputeException( "InvalidIPAddress.InUse", "Address ("+address.getName( )+") in use ("+address.getNetworkInterfaceId( )+")" ); } reply.set_return( true ); return reply; } /** * NOTE: ComputeService#describeAddresses is used for non-verbose describe functionality. */ public DescribeAddressesResponseType describeAddresses( final DescribeAddressesType request ) throws EucalyptusCloudException { final DescribeAddressesResponseType reply = request.getReply( ); final Context ctx = Contexts.lookup( ); final boolean isAdmin = ctx.isAdministrator( ); final boolean verbose = isAdmin && request.getPublicIpsSet( ).remove( "verbose" ) ; final Predicate<? super Address> filter = CloudMetadatas.filteringFor( Address.class ) .byId( request.getPublicIpsSet( ) ) .byProperty( request.getAllocationIds( ), Addresses.allocation( ) ) .byPredicate( Filters.generate( request.getFilterSet( ), Address.class ).asPredicate( ) ) .byOwningAccount( verbose ? Collections.<String>emptyList( ) : Collections.singleton( ctx.getAccount( ).getAccountNumber( ) ) ) .byPrivileges( ) .buildPredicate( ); for ( final Address address : Iterables.filter( addresses.listActiveAddresses( ), filter ) ) { reply.getAddressesSet( ).add( verbose ? address.getAdminDescription( ) : TypeMappers.transform( address, AddressInfoType.class ) ); } if ( verbose ) { for ( Address address : Iterables.filter( addresses.listInactiveAddresses( ), filter ) ) { reply.getAddressesSet( ).add( new AddressInfoType( address.getName( ), AddressDomain.standard.toString(), Principals.nobodyFullName( ).getUserName( ) ) ); } } return reply; } public AssociateAddressResponseType associateAddress( final AssociateAddressType request ) throws Exception { final AssociateAddressResponseType reply = request.getReply( ).markFailed( ); final String instanceId = request.getInstanceId( )==null ? null : normalizeInstanceIdentifier( request.getInstanceId( ) ); final String networkInterfaceId = request.getNetworkInterfaceId()==null ? null : normalizeNetworkInterfaceIdentifier( request.getNetworkInterfaceId() ); final Address address = RestrictedTypes.doPrivileged( Objects.firstNonNull( request.getPublicIp( ), ResourceIdentifiers.tryNormalize().apply( request.getAllocationId( ) ) ), Address.class ); if ( !address.isAllocated( ) && !Contexts.lookup( ).isAdministrator( ) ) { throw new EucalyptusCloudException( "Cannot associate an address which is not allocated: " + request.getPublicIp( ) ); } else if ( !Contexts.lookup( ).isAdministrator( ) && !Contexts.lookup( ).getUserFullName( ).asAccountFullName( ).getAccountNumber( ).equals( address.getOwner( ).getAccountNumber( ) ) ) { throw new EucalyptusCloudException( "Cannot associate an address which is not allocated to your account: " + request.getPublicIp( ) ); } final VmInstance vm = instanceId == null ? null : RestrictedTypes.doPrivileged( instanceId, VmInstance.class ); try ( final Addresses.AddressingBatch batch = addresses.batch( ) ) { if ( address.getDomain( ) != AddressDomain.vpc ) { // EC2-Classic if ( vm == null ) { throw new ClientComputeException( "InvalidParameterCombination", "InstanceId must be specified when using PublicIp" ); } if ( VmStateSet.NOT_RUNNING.apply( vm ) ) { throw new ClientComputeException( "InvalidInstanceID", "The instance '" + vm.getDisplayName( ) + "' is not in a valid state for this operation." ); } final VmInstance oldVm = findCurrentAssignedVm( address ); final Address oldAddr = findVmExistingAddress( addresses, vm ); reply.set_return( true ); if ( oldAddr != null && address.equals( oldAddr ) ) { return reply; } if ( address.isAssigned( ) ) { // clear current assignment for address if ( oldVm != null ) { PublicAddresses.markDirty( address.getDisplayName( ), oldVm.getPartition( ) ); } addresses.unassign( address ); if ( oldVm != null ) { addresses.system( oldVm ); } } if ( oldAddr != null ) { // clear current address for vm assigning to PublicAddresses.markDirty( oldAddr.getDisplayName( ), vm.getPartition( ) ); addresses.unassign( oldAddr ); } if ( addresses.assign( address, vm ) ) { Addresses.updatePublicIpByInstanceId( vm.getInstanceId( ), address.getName( ) ); } } else { // VPC final NetworkInterface networkInterface; try ( final TransactionResource tx = Entities.transactionFor( VmInstance.class ) ) { if ( vm != null ) { if ( VmStateSet.EXPECTING_TEARDOWN.apply( vm ) || VmStateSet.DONE.apply( vm ) ) { // STOPPED is OK throw new ClientComputeException( "InvalidInstanceID", "The instance '" + vm.getDisplayName( ) + "' is not in a valid state for this operation." ); } networkInterface = Iterables.getOnlyElement( Entities.merge( vm ).getNetworkInterfaces( ) ); } else { if ( networkInterfaceId == null ) { throw new ClientComputeException( "MissingParameter", "Either instance ID or network interface id must be specified" ); } networkInterface = RestrictedTypes.doPrivileged( networkInterfaceId, NetworkInterface.class ); if ( networkInterface.isAttached( ) ) { final VmInstance attachedVm = networkInterface.getInstance( ); if ( VmStateSet.EXPECTING_TEARDOWN.apply( attachedVm ) ) { // STOPPED is OK throw new ClientComputeException( "IncorrectInstanceState", "The instance to which '" + networkInterfaceId + "' is attached is not in a valid state for this operation" ); } } } } reply.set_return( true ); if ( !address.isAssigned( ) || !networkInterface.getDisplayName( ).equals( address.getNetworkInterfaceId( ) ) ) { if ( address.isAssigned( ) && !request.getAllowReassociation( ) ) { throw new ClientComputeException( "Resource.AlreadyAssociated", "Address already associated" ); } if ( address.isAssigned( ) ) { final NetworkInterface oldNetworkInterface = RestrictedTypes.doPrivileged( address.getNetworkInterfaceId( ), NetworkInterface.class ); try ( final TransactionResource tx = Entities.transactionFor( NetworkInterface.class ) ) { final NetworkInterface eni = Entities.merge( oldNetworkInterface ); addresses.unassign( address, null ); eni.disassociate( ); handleEniAddressUnassigned( addresses, address, eni ); tx.commit( ); } } try ( final TransactionResource tx = Entities.transactionFor( NetworkInterface.class ) ) { final NetworkInterface eni = Entities.merge( networkInterface ); internetGateways.lookupByVpc( null, eni.getVpc( ).getDisplayName( ), CloudMetadatas.toDisplayName( ) ); if ( eni.isAssociated( ) ) { PublicAddresses.markDirty( eni.getAssociation( ).getPublicIp( ), eni.getPartition( ) ); NetworkInterfaceHelper.releasePublic( eni ); eni.disassociate( ); if ( eni.isAttached( ) && eni.getAttachment( ).getDeviceIndex( ) == 0 ) { final VmInstance instance = eni.getInstance( ); VmInstances.updatePublicAddress( instance, VmNetworkConfig.DEFAULT_IP ); } } NetworkInterfaceHelper.associate( address, eni ); tx.commit( ); } catch ( final VpcMetadataNotFoundException e ) { throw new ClientComputeException( "Gateway.NotAttached", "Internet gateway not found for VPC" ); } } reply.setAssociationId( address.getAssociationId( ) ); } } return reply; } public DisassociateAddressResponseType disassociateAddress( final DisassociateAddressType request ) throws Exception { final DisassociateAddressResponseType reply = request.getReply( ).markFailed( ); final Context ctx = Contexts.lookup( ); final String associationId = ResourceIdentifiers.tryNormalize( ).apply( request.getAssociationId( ) ); final Address address; try { address = RestrictedTypes.doPrivileged( Objects.firstNonNull( request.getPublicIp( ), associationId ), Address.class ); } catch ( final NoSuchElementException e ) { if ( request.getAssociationId( ) != null ) { throw new ClientComputeException( "InvalidAssociationID.NotFound", "Association identifier (" + request.getAssociationId() + ") not found." ); } else { throw e; } } reply.set_return( true ); try ( final Addresses.AddressingBatch batch = addresses.batch( ) ) { if ( address.isSystemOwned( ) && !ctx.isAdministrator( ) ) { throw new EucalyptusCloudException( "Only administrators can unassign system owned addresses: " + address.toString( ) ); } else if ( address.getDomain( ) != AddressDomain.vpc ) { // EC2-Classic (allow for null domain) final String vmId = address.getInstanceId( ); try { addresses.unassign( address ); final VmInstance instance = VmInstances.lookup( vmId ); PublicAddresses.markDirty( address.getDisplayName( ), instance.getPartition( ) ); if ( address.getAddress( ).equals( instance.getPublicAddress( ) ) ) { Addresses.updatePublicIpByInstanceId( instance.getDisplayName( ), null ); try { addresses.system( instance ); } catch ( NoSuchElementException e ) { LOG.debug( e, e ); } catch ( Exception e ) { LOG.error( "Error assigning system address for instance " + vmId, e ); } } } catch ( Exception e ) { LOG.debug( e ); Logs.extreme( ).debug( e, e ); } } else { // VPC final NetworkInterface networkInterface = address.getNetworkInterfaceId( ) == null ? null : RestrictedTypes.doPrivileged( address.getNetworkInterfaceId( ), NetworkInterface.class ); if ( networkInterface != null ) { if ( NetworkInterface.Type.NatGateway == networkInterface.getType( ) ) { throw new ClientComputeException( "InvalidIPAddress.InUse", "Address ("+address.getName( )+") in use for NAT gateway interface ("+address.getNetworkInterfaceId( )+")" ); } try ( final TransactionResource tx = Entities.transactionFor( NetworkInterface.class ) ) { final NetworkInterface eni = Entities.merge( networkInterface ); if ( addresses.unassign( address, associationId ) ) { eni.disassociate( ); handleEniAddressUnassigned( addresses, address, eni ); tx.commit( ); } } } } } return reply; } private static Address findVmExistingAddress( final Addresses addresses, final VmInstance vm ) { Address oldAddr = null; if ( vm.hasPublicAddress( ) ) { try { oldAddr = addresses.lookupActiveAddress( vm.getPublicAddress( ) ); } catch ( Exception e ) { LOG.debug( e, e ); } } return oldAddr; } private static VmInstance findCurrentAssignedVm( Address address ) { VmInstance oldVm = null; if ( address.isAssigned( ) ) { try { oldVm = VmInstances.lookup( address.getInstanceId( ) ); } catch ( Exception e ) { LOG.error( e, e ); } } return oldVm; } private static String getDefaultVpcId( ) { return getDefaultVpcId( Contexts.lookup( ).getUserFullName( ).asAccountFullName( ) ); } private static String getDefaultVpcId( final AccountFullName accountFullName ) { try ( final TransactionResource tx = Entities.transactionFor( Vpc.class ) ) { return Iterables.tryFind( Entities.query( Vpc.exampleDefault( accountFullName ) ), Predicates.alwaysTrue() ).transform( CloudMetadatas.toDisplayName() ).orNull( ); } } /** * Caller must have open transaction for eni */ private static void handleEniAddressUnassigned( final Addresses addresses, final Address address, final NetworkInterface eni ) { if ( eni.isAttached( ) ) { PublicAddresses.markDirty( address.getAddress( ), eni.getPartition( ) ); if ( eni.getAttachment( ).getDeviceIndex( ) == 0 ) { final VmInstance vm = eni.getInstance( ); VmInstances.updatePublicAddress( vm, VmNetworkConfig.DEFAULT_IP ); if ( !vm.isUsePrivateAddressing( ) && ( VmInstance.VmState.PENDING.equals( vm.getState( ) ) || VmInstance.VmState.RUNNING.equals( vm.getState( ) ) ) ) { try { NetworkInterfaceHelper.associate( addresses.allocateSystemAddress( ), eni ); } catch ( final NotEnoughResourcesException e ) { LOG.warn( "No addresses available, not assigning system address for: " + vm.getDisplayName( ) + " : " + e.getMessage( ) ); } } } } } 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 normalizeInstanceIdentifier( final String identifier ) throws EucalyptusCloudException { return normalizeIdentifier( identifier, VmInstance.ID_PREFIX, true, "Value (%s) for parameter instanceId is invalid. Expected: 'i-...'." ); } private static String normalizeNetworkInterfaceIdentifier( final String identifier ) throws EucalyptusCloudException { return normalizeIdentifier( identifier, "eni", true, "Value (%s) for parameter networkInterface is invalid. Expected: 'eni-...'." ); } }