/************************************************************************* * 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.vm; import static com.eucalyptus.util.Strings.isPrefixOf; import static com.eucalyptus.util.Strings.upper; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ExecutionException; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.log4j.Logger; import org.xbill.DNS.Name; import com.eucalyptus.auth.Accounts; import com.eucalyptus.auth.AuthException; import com.eucalyptus.auth.euare.identity.region.RegionConfigurations; import com.eucalyptus.auth.policy.ern.Ern; import com.eucalyptus.auth.policy.ern.EuareResourceName; import com.eucalyptus.auth.principal.BaseInstanceProfile; import com.eucalyptus.auth.principal.BaseRole; import com.eucalyptus.component.ComponentIds; import com.eucalyptus.component.ServiceConfiguration; import com.eucalyptus.component.Topology; import com.eucalyptus.component.auth.SystemCredentials; import com.eucalyptus.component.id.Dns; import com.eucalyptus.component.id.Eucalyptus; import com.eucalyptus.component.id.Tokens; import com.eucalyptus.compute.common.CloudMetadatas; import com.eucalyptus.compute.common.internal.images.BlockStorageImageInfo; import com.eucalyptus.compute.common.internal.images.MachineImageInfo; import com.eucalyptus.compute.common.internal.network.NetworkGroup; import com.eucalyptus.compute.common.internal.vm.VmEphemeralAttachment; import com.eucalyptus.compute.common.internal.vm.VmInstance; import com.eucalyptus.compute.common.internal.vm.VmVolumeAttachment; import com.eucalyptus.compute.common.internal.vpc.NetworkInterface; import com.eucalyptus.crypto.Pkcs7; import com.eucalyptus.crypto.Signatures; import com.eucalyptus.crypto.util.Timestamps; import com.eucalyptus.images.ImageManager; import com.eucalyptus.tokens.AssumeRoleResponseType; import com.eucalyptus.tokens.AssumeRoleType; import com.eucalyptus.tokens.CredentialsType; import com.eucalyptus.util.CompatFunction; import com.eucalyptus.util.Exceptions; import com.eucalyptus.util.FUtils; import com.eucalyptus.util.RestrictedTypes; import com.eucalyptus.util.async.AsyncRequests; import com.eucalyptus.util.dns.DomainNames; import com.eucalyptus.ws.StackConfiguration; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilderSpec; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.collect.TreeMultimap; import com.google.common.io.BaseEncoding; import net.sf.json.JSONNull; import net.sf.json.JSONObject; /** * */ public class VmInstanceMetadata { private static final Logger LOG = Logger.getLogger( VmInstanceMetadata.class ); private static final CompatFunction<String,Cache<MetadataKey,ImmutableMap<String,String>>> MEMOIZED_CACHE_BUILDER = FUtils.memoizeLast( spec -> CacheBuilder.from( CacheBuilderSpec.parse( spec ) ).build( ) ); private static final BaseEncoding B64_76 = BaseEncoding.base64( ).withSeparator( "\n", 76 ); private enum Type { Instance, Dynamic } public static String getByKey( final VmInstance vm, final String pathArg ) { return getByKeyInternal( vm, pathArg, Type.Instance ); } public static String getDynamicByKey( final VmInstance vm, final String pathArg ) { return getByKeyInternal( vm, pathArg, Type.Dynamic ); } private static String getByKeyInternal( final VmInstance vm, final String pathArg, final Type type ) { final String path = MoreObjects.firstNonNull( pathArg, "" ); final String pathNoSlash; LOG.debug( "Servicing metadata request:" + path ); if ( path.endsWith( "/" ) ) { pathNoSlash = path.substring( 0, path.length() -1 ); } else { pathNoSlash = path; } Optional<MetadataGroup> groupOption = Optional.absent(); for ( final MetadataGroup metadataGroup : MetadataGroup.values() ) { if ( metadataGroup.isType( type ) && ( metadataGroup.providesPath( pathNoSlash ) || metadataGroup.providesPath( path ) ) ) { groupOption = Optional.of( metadataGroup ); } } final MetadataGroup group = groupOption.or( MetadataGroup.core( type ) ); final Map<String,String> metadataMap = Optional.fromNullable( group.apply( vm ) ).or( Collections.<String, String>emptyMap() ); final String value = metadataMap.get( path ); return value == null ? metadataMap.get( pathNoSlash ) : value; } private static Map<String, String> getCoreMetadataMap( final VmInstance vm ) { @SuppressWarnings( "deprecation" ) final boolean dns = StackConfiguration.USE_INSTANCE_DNS && !ComponentIds.lookup( Dns.class ).runLimitedServices( ); final Map<String, String> m = Maps.newHashMap(); m.put( "ami-id", vm.getImageId() ); if ( vm.getBootRecord( ).getMachine() != null && !vm.getBootRecord( ).getMachine().getProductCodes( ).isEmpty( ) ) { m.put( "product-codes", Joiner.on( '\n' ).join( vm.getBootRecord( ).getMachine().getProductCodes( ) ) ); } m.put( "ami-launch-index", "" + vm.getLaunchIndex( ) ); //ASAP: FIXME: GRZE: // m.put( "ancestor-ami-ids", this.getImageInfo( ).getAncestorIds( ).toString( ).replaceAll( "[\\Q[]\\E]", "" ).replaceAll( ", ", "\n" ) ); if ( vm.getBootRecord( ).getMachine() instanceof MachineImageInfo ) { m.put( "ami-manifest-path", ( ( MachineImageInfo ) vm.getBootRecord( ).getMachine() ).getManifestLocation( ) ); } if ( dns ) { m.put( "hostname", vm.getPrivateDnsName() ); } else { m.put( "hostname", vm.getPrivateAddress() ); } m.put( "instance-id", vm.getInstanceId() ); m.put( "instance-type", vm.getVmType().getName( ) ); if ( dns ) { m.put( "local-hostname", vm.getPrivateDnsName() ); } else { m.put( "local-hostname", vm.getPrivateAddress() ); } m.put( "local-ipv4", vm.getPrivateAddress() ); m.put( "mac", upper().apply( vm.getMacAddress() ) ); if ( dns ) { m.put( "public-hostname", vm.getPublicDnsName() ); } else { m.put( "public-hostname", vm.getPublicAddress() ); } m.put( "public-ipv4", vm.getPublicAddress() ); m.put( "reservation-id", vm.getReservationId( ) ); if ( vm.getKernelId() != null ) { m.put( "kernel-id", vm.getKernelId() ); } if ( vm.getRamdiskId() != null ) { m.put( "ramdisk-id", vm.getRamdiskId() ); } m.put( "security-groups", Joiner.on('\n').join( Sets.newTreeSet( Iterables.transform( vm.getNetworkGroups(), CloudMetadatas.toDisplayName() ) ) ) ); m.put( "services/domain", DomainNames.externalSubdomain( ).relativize( Name.root ).toString( ) ); m.put( "placement/availability-zone", vm.getPartition() ); return m; } private static Map<String,String> getCoreDynamicMetadataMap( final VmInstance vm ) { final Map<String, String> m = Maps.newHashMap( ); m.put( "fws/instance-monitoring", MoreObjects.firstNonNull( vm.getMonitoring( ), Boolean.FALSE ) ? "enabled" : "disabled" ); return m; } private static Map<String,String> getInstanceIdentityMetadataMap( final VmInstance vm ) { final Map<String, String> m = Maps.newHashMap( ); final String identityDocument = new JSONObject( ) .element( "privateIp", vm.getPrivateAddress( ) ) .element( "devpayProductCodes", JSONNull.getInstance( ) ) .element( "availabilityZone", vm.getPartition( ) ) .element( "version", "2010-08-31" ) .element( "region", RegionConfigurations.getRegionNameOrDefault( ) ) .element( "instanceId", vm.getDisplayName( ) ) .element( "billingProducts", JSONNull.getInstance( ) ) .element( "instanceType", vm.getVmType( ).getName( ) ) .element( "accountId", vm.getOwnerAccountNumber( ) ) .element( "pendingTime", Timestamps.formatIso8601Timestamp( vm.getCreationTimestamp( ) ) ) .element( "imageId", vm.getImageId( ) ) .element( "architecture", vm.getBootRecord( ) == null || vm.getBootRecord( ).getArchitecture( ) == null ? "x86_64" : vm.getBootRecord( ).getArchitecture( ).toString( ) ) .element( "kernelId", vm.getKernelId( ) == null ? JSONNull.getInstance( ) : vm.getKernelId( ) ) .element( "ramdiskId", vm.getRamdiskId( ) == null ? JSONNull.getInstance( ) : vm.getRamdiskId( ) ) .toString( 2 ); final SystemCredentials.Credentials credentials = SystemCredentials.lookup( Eucalyptus.class ); m.put( "instance-identity/document", identityDocument ); try { m.put( "instance-identity/pkcs7", B64_76.encode( Pkcs7.sign( identityDocument, credentials.getPrivateKey( ), credentials.getCertificate( ) ) ) ); } catch ( Exception e ) { LOG.error( "Error generating pkcs7 identity document signed data", e ); } try { m.put( "instance-identity/signature", B64_76.encode( Signatures.SHA1WithRSA.signBinary( credentials.getPrivateKey( ), identityDocument.getBytes( StandardCharsets.UTF_8 ) ) ) ); } catch ( GeneralSecurityException e ) { LOG.error( "Error generating identity document signature", e ); } return m; } private static Map<String, String> getNetworkMetadataMap( final VmInstance vm ) { final Map<String, String> m = Maps.newHashMap( ); if ( !vm.getNetworkInterfaces( ).isEmpty( ) ) for ( final NetworkInterface networkInterface : vm.getNetworkInterfaces( ) ) { final String prefix = "network/interfaces/macs/" + networkInterface.getMacAddress( ) + "/"; m.put( prefix + "device-number", String.valueOf( networkInterface.getAttachment( ).getDeviceIndex( ) ) ); m.put( prefix + "interface-id", networkInterface.getDisplayName( ) ); if ( networkInterface.isAssociated( ) ) { m.put( prefix + "ipv4-associations/" + networkInterface.getAssociation( ).getPublicIp( ), networkInterface.getPrivateIpAddress( ) ); } final String privateIp = networkInterface.getPrivateIpAddress( ); m.put( prefix + "local-hostname", VmInstances.dnsName( privateIp, DomainNames.internalSubdomain( ) ) ); m.put( prefix + "local-ipv4s", privateIp ); m.put( prefix + "mac", networkInterface.getMacAddress( ) ); m.put( prefix + "owner-id", networkInterface.getOwnerAccountNumber( ) ); if ( networkInterface.isAssociated( ) ) { m.put( prefix + "public-hostname", Strings.nullToEmpty( networkInterface.getAssociation( ).getPublicDnsName( ) ) ); m.put( prefix + "public-ipv4s", networkInterface.getAssociation( ).getPublicIp( ) ); } else { m.put( prefix + "public-hostname", "" ); m.put( prefix + "public-ipv4s", "" ); } m.put( prefix + "security-groups", Joiner.on( '\n' ).join( Iterables.transform( networkInterface.getNetworkGroups( ), RestrictedTypes.toDisplayName( ) ) ) ); m.put( prefix + "security-group-ids", Joiner.on( '\n' ).join( Iterables.transform( networkInterface.getNetworkGroups( ), NetworkGroup.groupId( ) ) ) ); m.put( prefix + "subnet-id", networkInterface.getSubnet( ).getDisplayName( ) ); m.put( prefix + "subnet-ipv4-cidr-block", networkInterface.getSubnet( ).getCidr( ) ); m.put( prefix + "vpc-id", networkInterface.getVpc( ).getDisplayName( ) ); m.put( prefix + "vpc-ipv4-cidr-block", networkInterface.getVpc( ).getCidr( ) ); } else { // EC2-Classic instance @SuppressWarnings( "deprecation" ) final boolean dns = StackConfiguration.USE_INSTANCE_DNS && !ComponentIds.lookup( Dns.class ).runLimitedServices( ); final String prefix = "network/interfaces/macs/" + vm.getMacAddress( ) + "/"; m.put( prefix + "device-number", "0" ); if ( dns ) { m.put( prefix + "local-hostname", vm.getPrivateDnsName() ); } else { m.put( prefix + "local-hostname", vm.getPrivateAddress() ); } m.put( prefix + "local-ipv4s", vm.getPrivateAddress( ) ); m.put( prefix + "mac", vm.getMacAddress( ) ); m.put( prefix + "owner-id", vm.getOwnerAccountNumber( ) ); if ( dns ) { m.put( prefix + "public-hostname", vm.getPublicDnsName() ); } else { m.put( prefix + "public-hostname", vm.getPublicAddress() ); } m.put( prefix + "public-ipv4s", vm.getPublicAddress( ) ); } return m; } private static Map<String, String> getBlockDeviceMappingMetadataMap( final VmInstance vm ) { final Map<String, String> m = Maps.newHashMap( ); // Metadata should accurately reflect all the ebs mappings and ephemeral mappings if any. // Fixes EUCA-4081, EUCA-3954 and implements EUCA-4786 if( vm.getBootRecord().getMachine() instanceof BlockStorageImageInfo ) { // Get all the volume attachments and order them in some way (by device name for now) Set<VmVolumeAttachment> volAttachments = new TreeSet<>(VolumeAttachmentComparator.INSTANCE); volAttachments.addAll(vm.getBootRecord().getPersistentVolumes()); // Keep track of all ebs keys for populating block-device-mapping list int ebsCount = 0; // Iterate through the list of volume attachments and populate ebs mappings for (VmVolumeAttachment attachment : volAttachments ) { if (attachment.getIsRootDevice()) { m.put( "block-device-mapping/ami", attachment.getShortDeviceName() ); m.put( "block-device-mapping/emi", attachment.getShortDeviceName() ); m.put( "block-device-mapping/root", attachment.getDevice() ); } // add only volumes added at start up time and don't list root see EUCA-8636 if (attachment.getAttachedAtStartup() && !attachment.getIsRootDevice()) m.put( "block-device-mapping/ebs" + String.valueOf(++ebsCount), attachment.getShortDeviceName() ); } // Using ephemeral attachments for bfebs instances only, can be extended to be used by all other instances // Get all the ephemeral attachments and order them in some way (by device name for now) Set<VmEphemeralAttachment> ephemeralAttachments = new TreeSet<>(vm.getBootRecord().getEphemeralStorage()); // Iterate through the list of ephemeral attachments and populate ephemeral mappings if (!ephemeralAttachments.isEmpty()) { for(VmEphemeralAttachment attachment : ephemeralAttachments){ m.put( "block-device-mapping/" + attachment.getEphemeralId(), attachment.getShortDeviceName() ); } } } else if (vm.getBootRecord().getMachine() instanceof MachineImageInfo) { MachineImageInfo mii = (MachineImageInfo) vm.getBootRecord().getMachine(); String s = mii.getRootDeviceName(); m.put( "block-device-mapping/emi", mii.getShortRootDeviceName() ); m.put( "block-device-mapping/ami", mii.getShortRootDeviceName() ); m.put( "block-device-mapping/root", s ); if ( ImageManager.isPathAPartition( s )) { m.put( "block-device-mapping/ephemeral0", "sda2" ); m.put( "block-device-mapping/swap", "sda3" ); } else { m.put( "block-device-mapping/ephemeral0", "sdb" ); } } return m; } private static Map<String, String> getIamMetadataMap( final VmInstance vm ) { final Map<String, String> m = new HashMap<>( ); final String instanceProfileNameOrArn = vm.getIamInstanceProfileArn(); if ( !Strings.isNullOrEmpty( instanceProfileNameOrArn ) ) { BaseInstanceProfile profile = null; String profileArn = null; String roleArn = vm.getIamRoleArn(); String roleName = null; if ( !Strings.isNullOrEmpty( roleArn ) ) { roleName = roleArn.substring( roleArn.lastIndexOf('/') + 1 ); } else try { String profileName; if ( instanceProfileNameOrArn.startsWith("arn:") ) { profileName = instanceProfileNameOrArn.substring( instanceProfileNameOrArn.lastIndexOf('/') + 1 ); } else { profileName = instanceProfileNameOrArn; } profile = Accounts.lookupInstanceProfileByName( vm.getOwnerAccountNumber( ), profileName ); profileArn = Accounts.getInstanceProfileArn( profile ); if ( roleArn == null ) { final BaseRole role = profile.getRole(); if ( role != null ) { roleArn = Accounts.getRoleArn( role ); roleName = role.getName(); } } else { // Authorized role from instance creation time must be used if present final EuareResourceName ern = (EuareResourceName) Ern.parse( roleArn ); roleName = ern.getName(); } } catch (AuthException e) { LOG.debug(e); } CredentialsType credentials = null; if ( roleArn != null ) { final AssumeRoleType assumeRoleType = new AssumeRoleType( ); assumeRoleType.setRoleArn(roleArn); assumeRoleType.setRoleSessionName( vm.getInstanceId( ) ); ServiceConfiguration serviceConfiguration = Topology.lookup( Tokens.class ); try { credentials = ((AssumeRoleResponseType) AsyncRequests.sendSync( serviceConfiguration, assumeRoleType )) .getAssumeRoleResult().getCredentials(); } catch (Exception e) { LOG.debug("Unable to send assume role request to token service",e); } } if ( profile != null ) { m.put("iam/info/last-updated-date", Timestamps.formatIso8601Timestamp( new Date() ) ); m.put("iam/info/instance-profile-arn", profileArn ); m.put("iam/info/instance-profile-id", profile.getInstanceProfileId() ); } if ( roleName != null && credentials != null ) { final String jsonCredentials = new JSONObject( ) .element( "Code", "Success" ) .element( "LastUpdated", Timestamps.formatIso8601Timestamp( new Date( ) ) ) .element( "Type", "AWS-HMAC" ) .element( "AccessKeyId", credentials.getAccessKeyId( ) ) .element( "SecretAccessKey", credentials.getSecretAccessKey( ) ) .element( "Token", credentials.getSessionToken( ) ) .element( "Expiration", Timestamps.formatIso8601Timestamp( credentials.getExpiration( ) ) ) .toString( 2 ); m.put("iam/security-credentials/" + roleName + "/AccessKeyId", credentials.getAccessKeyId()); m.put("iam/security-credentials/" + roleName + "/Expiration",Timestamps.formatIso8601Timestamp(credentials.getExpiration())); m.put("iam/security-credentials/" + roleName + "/SecretAccessKey", credentials.getSecretAccessKey()); m.put("iam/security-credentials/" + roleName + "/Token", credentials.getSessionToken()); m.put("iam/security-credentials/" + roleName, jsonCredentials ); m.put("iam/security-credentials", roleName ); m.put("iam/security-credentials/", roleName ); } } return m; } private static Map<String, String> getPublicKeysMetadataMap( final VmInstance vm ) { final Map<String, String> m = Maps.newHashMap( ); if ( vm.getBootRecord( ).getSshKeyPair() != null ) { m.put( "public-keys", "0=" + vm.getBootRecord( ).getSshKeyPair().getName( ) ); m.put( "public-keys/", "0=" + vm.getBootRecord( ).getSshKeyPair().getName( ) ); m.put( "public-keys/0/openssh-key", vm.getBootRecord( ).getSshKeyPair().getPublicKey( ) ); } return m; } private enum VolumeAttachmentComparator implements Comparator<VmVolumeAttachment> { INSTANCE; @Override public int compare(VmVolumeAttachment arg0, VmVolumeAttachment arg1) { return arg0.getDevice().compareToIgnoreCase(arg1.getDevice()); } } private enum MetadataGroup implements Function<VmInstance,Map<String,String>> { Core( Type.Instance ) { @Override public Map<String, String> apply( final VmInstance instance ) { return addListingEntries( instance, getCoreMetadataMap( instance ), true, Optional.of( Type.Instance ) ); } }, Network( "network" ) { @Override public Map<String, String> apply( final VmInstance instance ) { return addListingEntries( getNetworkMetadataMap( instance ) ); } }, BlockDeviceMapping( "block-device-mapping" ) { @Override public Map<String, String> apply( final VmInstance instance ) { return addListingEntries( getBlockDeviceMappingMetadataMap( instance ) ); } }, Iam( "iam" ) { @Override public Map<String, String> apply( final VmInstance instance ) { try { return cache( ).get( new MetadataKey( instance.getInstanceUuid(), instance.getVersion(), this ), () -> ImmutableMap.copyOf( addListingEntries( getIamMetadataMap( instance ) ) ) ); } catch ( ExecutionException e ) { throw Exceptions.toUndeclared( e ); // Cache load exception not expected } } @Override protected boolean isPresent( final VmInstance instance ) { return !Strings.isNullOrEmpty( instance.getIamInstanceProfileArn() ); } }, PublicKeys( "public-keys" ) { @Override public Map<String, String> apply( final VmInstance instance ) { return addListingEntries( getPublicKeysMetadataMap( instance ) ); } @Override protected boolean isPresent( final VmInstance instance ) { return instance.getBootRecord( ).getSshKeyPair() != null; } }, CoreDynamic( Type.Dynamic ) { @Override public Map<String, String> apply( final VmInstance instance ) { return addListingEntries( instance, getCoreDynamicMetadataMap( instance ), true, Optional.of( Type.Dynamic ) ); } }, InstanceIdentity( "instance-identity", Type.Dynamic ) { @Override public Map<String, String> apply( final VmInstance instance ) { try { return cache( ).get( new MetadataKey( instance.getInstanceUuid(), instance.getVersion(), this ), () -> ImmutableMap.copyOf( addListingEntries( getInstanceIdentityMetadataMap( instance ) ) ) ); } catch ( ExecutionException e ) { throw Exceptions.toUndeclared( e ); // Cache load exception not expected } } }; private final Optional<String> prefix; private final Type type; MetadataGroup( final Type type ) { prefix = Optional.absent( ); this.type = type; } MetadataGroup( final String path ) { this( path, Type.Instance ); } MetadataGroup( final String path, final Type type ) { this.prefix = Optional.of( path ); this.type = type; } public boolean providesPath( final String path ) { return prefix.transform( Functions.forPredicate( isPrefixOf( path ) ) ) .or( Boolean.FALSE ); } public boolean isType( final Type type ) { return this.type == type; } protected boolean isPresent( final VmInstance instance ) { return true; } @Nonnull public static MetadataGroup core( final Type type ) { for ( final MetadataGroup group : values( ) ) { if ( !group.prefix.isPresent( ) && group.isType( type ) ) { return group; } } throw new IllegalStateException( "Core not found for type " + type ); } private static Map<String,String> addListingEntries( final Map<String,String> metadataMap ) { return addListingEntries( null, metadataMap, false, Optional.<Type>absent( ) ); } private static Map<String,String> addListingEntries( @Nullable final VmInstance instance, final Map<String,String> metadataMap, final boolean addRoots, final Optional<Type> type ) { final TreeMultimap<String,String> listingMap = TreeMultimap.create( ); final Splitter pathSplitter = Splitter.on( '/' ); final Joiner pathJoiner = Joiner.on( '/' ); for ( final String path : metadataMap.keySet() ) { final List<String> pathSegments = Lists.newArrayList( pathSplitter.split( path ) ); for ( int i=0; i<pathSegments.size(); i++ ) { listingMap.put( pathJoiner.join( pathSegments.subList( 0, i ) ), pathSegments.get( i ) + ( i < pathSegments.size() -1 ? "/" : "" ) ); } } if ( addRoots && instance != null ) { for ( MetadataGroup group : MetadataGroup.values() ) { if ( !type.isPresent( ) || !group.isType( type.get( ) ) ) continue; if ( group.isPresent( instance ) && group.prefix.isPresent( ) ) { listingMap.put( "", group.prefix.get() + "/" ); } } } final Joiner listingJoiner = Joiner.on( "\n" ); for ( final String key : listingMap.keySet() ) { final Set<String> values = listingMap.get( key ); final Iterator<String> valueIterator = values.iterator( ); while ( valueIterator.hasNext( ) ) { final String value = valueIterator.next( ); if ( values.contains( value+"/" ) ) valueIterator.remove( ); } if ( !metadataMap.containsKey( key ) ) { metadataMap.put( key, listingJoiner.join( values ) ); } else if ( !metadataMap.containsKey( key + "/" ) ) { metadataMap.put( key + "/", listingJoiner.join( values ) ); } } return metadataMap; } private static Cache<MetadataKey,ImmutableMap<String,String>> cache( ) { return MEMOIZED_CACHE_BUILDER.apply( VmInstances.VM_METADATA_GENERATED_CACHE ); } } private static final class MetadataKey { private final String id; // internal id private final Integer version; private final MetadataGroup metadataGroup; private MetadataKey( final String id, final Integer version, final MetadataGroup metadataGroup ) { this.id = id; this.version = version; this.metadataGroup = metadataGroup; } @Override public boolean equals( final Object o ) { if ( this == o ) return true; if ( o == null || getClass( ) != o.getClass( ) ) return false; final MetadataKey that = (MetadataKey) o; return Objects.equals( id, that.id ) && Objects.equals( version, that.version ) && metadataGroup == that.metadataGroup; } @Override public int hashCode() { return Objects.hash( id, version, metadataGroup ); } } }