/************************************************************************* * 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.network; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.log4j.Logger; import org.hibernate.exception.ConstraintViolationException; import com.eucalyptus.auth.principal.AccountFullName; import com.eucalyptus.compute.common.CloudMetadata; import com.eucalyptus.compute.common.CloudMetadatas; import com.eucalyptus.compute.common.internal.network.NetworkGroup; import com.eucalyptus.compute.common.internal.network.NetworkGroupTag; import com.eucalyptus.compute.common.internal.network.NetworkPeer; import com.eucalyptus.compute.common.internal.network.NetworkRule; import com.eucalyptus.compute.common.internal.network.NoSuchGroupMetadataException; import com.eucalyptus.compute.common.internal.util.MetadataConstraintException; import com.eucalyptus.compute.common.internal.util.MetadataException; import com.eucalyptus.compute.common.internal.util.NoSuchMetadataException; import com.eucalyptus.compute.common.IpPermissionType; import com.eucalyptus.compute.common.SecurityGroupItemType; import com.eucalyptus.compute.common.UserIdGroupPairType; import com.eucalyptus.configurable.ConfigurableClass; import com.eucalyptus.configurable.ConfigurableField; import com.eucalyptus.entities.Entities; import com.eucalyptus.entities.TransactionResource; import com.eucalyptus.network.config.NetworkConfigurations; import com.eucalyptus.records.Logs; import com.eucalyptus.compute.common.internal.tags.FilterSupport; import com.eucalyptus.util.Cidr; import com.eucalyptus.util.Exceptions; import com.eucalyptus.auth.principal.OwnerFullName; import com.eucalyptus.util.FUtils; import com.eucalyptus.util.RestrictedTypes; import com.eucalyptus.util.TypeMapper; import com.eucalyptus.util.TypeMappers; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @ConfigurableClass( root = "cloud.network", description = "Default values used to bootstrap networking state discovery." ) public class NetworkGroups extends com.eucalyptus.compute.common.internal.network.NetworkGroups { public static final Pattern VPC_GROUP_NAME_PATTERN = Pattern.compile( "[a-zA-Z0-9 ._\\-:/()#,@\\[\\]+=&;{}!$*]{1,255}" ); public static final Pattern VPC_GROUP_DESC_PATTERN = Pattern.compile( "[a-zA-Z0-9 ._\\-:/()#,@\\[\\]+=&;{}!$*]{0,255}" ); private static Logger LOG = Logger.getLogger( NetworkGroups.class ); @ConfigurableField( description = "Minutes before a pending index allocation timesout and is released.", initial = "35" ) public static volatile Integer NETWORK_INDEX_PENDING_TIMEOUT = 35; @ConfigurableField( description = "Minutes before a pending system public address allocation timesout and is released.", initial = "35" ) public static volatile Integer ADDRESS_PENDING_TIMEOUT = 35; @ConfigurableField( description = "Network configuration document.", changeListener = NetworkConfigurations.NetworkConfigurationPropertyChangeListener.class ) public static volatile String NETWORK_CONFIGURATION = ""; @ConfigurableField( description = "Minimum interval between broadcasts of network information (seconds).", initial = "5" ) public static volatile Integer MIN_BROADCAST_INTERVAL = 5; @ConfigurableField( description = "Maximum time to apply network information (seconds).", initial = "120" ) public static volatile Integer MAX_BROADCAST_APPLY = 120; public static NetworkGroup delete( final String groupId ) throws MetadataException { try ( final TransactionResource db = Entities.transactionFor( NetworkGroup.class ) ) { final NetworkGroup ret = Entities.uniqueResult( NetworkGroup.withGroupId( null, groupId ) ); Entities.delete( ret ); db.commit( ); return ret; } catch ( final ConstraintViolationException ex ) { Logs.exhaust( ).error( ex, ex ); throw new MetadataConstraintException( "Failed to delete security group: " + groupId + " because of: " + Exceptions.causeString( ex ), ex ); } catch ( final Exception ex ) { Logs.exhaust( ).error( ex, ex ); throw new NoSuchMetadataException( "Failed to find security group: " + groupId, ex ); } } public static NetworkGroup lookupByNaturalId( final String uuid ) throws NoSuchMetadataException { try ( final TransactionResource db = Entities.transactionFor( NetworkGroup.class ) ) { NetworkGroup entity = Entities.uniqueResult( NetworkGroup.withNaturalId( uuid ) ); db.commit( ); return entity; } catch ( Exception ex ) { Logs.exhaust( ).error( ex, ex ); throw new NoSuchMetadataException( "Failed to find security group: " + uuid, ex ); } } public static NetworkGroup lookupByGroupId( final String groupId ) throws NoSuchMetadataException { return lookupByGroupId( null, groupId ); } public static NetworkGroup lookupByGroupId( @Nullable final OwnerFullName ownerFullName, final String groupId ) throws NoSuchMetadataException { try ( final TransactionResource db = Entities.transactionFor( NetworkGroup.class ) ) { NetworkGroup entity = Entities.uniqueResult( NetworkGroup.withGroupId(ownerFullName, groupId) ); db.commit( ); return entity; } catch ( Exception ex ) { Logs.exhaust( ).error( ex, ex ); throw new NoSuchMetadataException( "Failed to find security group: " + groupId, ex ); } } public static NetworkGroup lookup( final OwnerFullName ownerFullName, final String groupName ) throws MetadataException { if ( defaultNetworkName( ).equals( groupName ) ) { createDefault( ownerFullName ); } try ( final TransactionResource db = Entities.transactionFor( NetworkGroup.class ) ) { NetworkGroup ret = Entities.uniqueResult( NetworkGroup.named( ownerFullName, groupName ) ); db.commit( ); return ret; } catch ( final Exception ex ) { Logs.exhaust( ).error( ex, ex ); LOG.debug( "Failed to find security group: " + groupName + " for " + ownerFullName, ex ); throw new NoSuchGroupMetadataException( "The security group '" + groupName + "' does not exist" ); } } public static NetworkGroup lookupDefault( final OwnerFullName ownerFullName, final String vpcId ) throws MetadataException { return lookup( ownerFullName, vpcId, defaultNetworkName( ) ); } public static NetworkGroup lookup( final OwnerFullName ownerFullName, final String vpcId, final String name ) throws MetadataException { try ( final TransactionResource db = Entities.transactionFor( NetworkGroup.class ) ) { return Entities.uniqueResult( ownerFullName == null && vpcId != null ? NetworkGroup.namedForVpc( vpcId, name ) : NetworkGroup.withUniqueName( ownerFullName, vpcId, name ) ); } catch ( final Exception ex ) { Logs.exhaust( ).error( ex, ex ); throw new NoSuchMetadataException( "Failed to find security group: " + name +", for vpc: " + vpcId + " for " + ownerFullName, ex ); } } /** * Resolve Group Names / Identifiers for the given permissions. * * <p>Caller must have open transaction.</p> * * @param permissions - The permissions to update * @param defaultUserId - The account number to use when not specified * @param vpcId - The identifier for the VPC if the a VPC security group * @param revoke - True if resolving for a revoke operation * @throws MetadataException If an error occurs */ public static void resolvePermissions( final Iterable<IpPermissionType> permissions, final String defaultUserId, @Nullable final String vpcId, final boolean revoke ) throws MetadataException { for ( final IpPermissionType ipPermission : permissions ) { if ( ipPermission.getGroups() != null ) for ( final UserIdGroupPairType groupInfo : ipPermission.getGroups() ) { if ( !Strings.isNullOrEmpty( groupInfo.getSourceGroupId( ) ) ) { try{ final NetworkGroup networkGroup = NetworkGroups.lookupByGroupId( groupInfo.getSourceGroupId() ); if ( vpcId != null && !vpcId.equals( networkGroup.getVpcId( ) ) ) { throw new NoSuchMetadataException( "Group ("+groupInfo.getSourceGroupId()+") not found." ); } groupInfo.setSourceUserId( networkGroup.getOwnerAccountNumber() ); groupInfo.setSourceGroupName( networkGroup.getDisplayName() ); }catch(final NoSuchMetadataException ex){ if(!revoke) throw ex; } } else if ( Strings.isNullOrEmpty( groupInfo.getSourceGroupName( ) ) ) { throw new MetadataException( "Group ID or Group Name required." ); } else { try{ final AccountFullName accountFullName = AccountFullName.getInstance( Objects.firstNonNull( Strings.emptyToNull( groupInfo.getSourceUserId() ), defaultUserId ) ); final NetworkGroup networkGroup = vpcId == null ? NetworkGroups.lookup( accountFullName, groupInfo.getSourceGroupName( ) ) : NetworkGroups.lookup( accountFullName, vpcId, groupInfo.getSourceGroupName( ) ); groupInfo.setSourceGroupId( networkGroup.getGroupId( ) ); }catch(final NoSuchMetadataException ex){ if(!revoke) throw ex; } } } } } public static void flushRules( ) { NetworkInfoBroadcaster.requestNetworkInfoBroadcast( ); } @TypeMapper public enum NetworkPeerAsUserIdGroupPairType implements Function<NetworkPeer, UserIdGroupPairType> { INSTANCE; @Override public UserIdGroupPairType apply( final NetworkPeer peer ) { return new UserIdGroupPairType( peer.getUserQueryKey( ), peer.getGroupName( ), peer.getGroupId( ) ); } } @TypeMapper public enum NetworkRuleAsIpPerm implements Function<NetworkRule, IpPermissionType> { INSTANCE; @Override public IpPermissionType apply( final NetworkRule rule ) { final IpPermissionType ipPerm = new IpPermissionType( rule.getDisplayProtocol( ), rule.getLowPort( ), rule.getHighPort( ) ); final Iterable<UserIdGroupPairType> peers = Iterables.transform( rule.getNetworkPeers( ), TypeMappers.lookup( NetworkPeer.class, UserIdGroupPairType.class ) ); Iterables.addAll( ipPerm.getGroups( ), peers ); ipPerm.setCidrIpRanges( rule.getIpRanges( ) ); return ipPerm; } } @TypeMapper public enum NetworkGroupAsSecurityGroupItem implements Function<NetworkGroup, SecurityGroupItemType> { INSTANCE; @Override public SecurityGroupItemType apply( final NetworkGroup input ) { try ( final TransactionResource tx = Entities.transactionFor( NetworkGroup.class ) ) { final NetworkGroup netGroup = Entities.merge( input ); final SecurityGroupItemType groupInfo = new SecurityGroupItemType( netGroup.getOwnerAccountNumber( ), netGroup.getGroupId( ), netGroup.getDisplayName( ), netGroup.getDescription( ), netGroup.getVpcId( ) ); final Iterable<IpPermissionType> ipPerms = Iterables.transform( netGroup.getIngressNetworkRules( ), TypeMappers.lookup( NetworkRule.class, IpPermissionType.class ) ); Iterables.addAll( groupInfo.getIpPermissions( ), ipPerms ); final Iterable<IpPermissionType> ipPermsEgress = Iterables.transform( netGroup.getEgressNetworkRules( ), TypeMappers.lookup( NetworkRule.class, IpPermissionType.class ) ); Iterables.addAll( groupInfo.getIpPermissionsEgress( ), ipPermsEgress ); return groupInfo; } } } public enum IpPermissionTypeExtractNetworkPeers implements Function<IpPermissionType, Collection<NetworkPeer>> { INSTANCE; @Override public Collection<NetworkPeer> apply( IpPermissionType ipPerm ) { final Collection<NetworkPeer> networkPeers = Lists.newArrayList(); for ( UserIdGroupPairType peerInfo : ipPerm.getGroups( ) ) { networkPeers.add( new NetworkPeer( peerInfo.getSourceUserId(), peerInfo.getSourceGroupName(), peerInfo.getSourceGroupId() ) ); } return networkPeers; } } private static class IpPermissionTypeAsNetworkRule implements Function<IpPermissionType, List<NetworkRule>> { private final boolean anyProtocolAllowed; private final boolean forRevoke; private IpPermissionTypeAsNetworkRule( boolean anyProtocolAllowed, boolean forRevoke ) { this.anyProtocolAllowed = anyProtocolAllowed; this.forRevoke = forRevoke; } /** * @see com.google.common.base.Function#apply(java.lang.Object) */ @Nonnull @Override public List<NetworkRule> apply( IpPermissionType ipPerm ) { List<NetworkRule> ruleList = new ArrayList<NetworkRule>( ); if ( !ipPerm.getGroups( ).isEmpty( ) ) { if ( ipPerm.getFromPort()!=null && ipPerm.getFromPort( ) == 0 && ipPerm.getToPort( ) != null && ipPerm.getToPort( ) == 0 ) { ipPerm.setToPort( 65535 ); } List<String> empty = Lists.newArrayList( ); //:: fixes handling of under-specified named-network rules sent by some clients ::// if ( ipPerm.getIpProtocol( ) == null ) { NetworkRule rule = NetworkRule.create( NetworkRule.Protocol.tcp, ipPerm.getFromPort( ), ipPerm.getToPort( ), IpPermissionTypeExtractNetworkPeers.INSTANCE.apply( ipPerm ), empty ); ruleList.add( rule ); NetworkRule rule1 = NetworkRule.create( NetworkRule.Protocol.udp, ipPerm.getFromPort( ), ipPerm.getToPort( ), IpPermissionTypeExtractNetworkPeers.INSTANCE.apply( ipPerm ), empty ); ruleList.add( rule1 ); NetworkRule rule2 = NetworkRule.create( NetworkRule.Protocol.tcp, -1, -1, IpPermissionTypeExtractNetworkPeers.INSTANCE.apply( ipPerm ), empty ); ruleList.add( rule2 ); } else { NetworkRule rule = NetworkRule.create( ipPerm.getIpProtocol( ), anyProtocolAllowed, ipPerm.getFromPort( ), ipPerm.getToPort( ), IpPermissionTypeExtractNetworkPeers.INSTANCE.apply( ipPerm ), empty ); ruleList.add( rule ); } } else if ( !ipPerm.getCidrIpRanges().isEmpty( ) ) { List<String> ipRanges = Lists.newArrayList( ); List<String> literalIpRanges = Lists.newArrayList( ipPerm.getCidrIpRanges( ) ); for ( final String range : ipPerm.getCidrIpRanges( ) ) { try { if ( range.indexOf( '/' ) < 0 ) throw new IllegalArgumentException( ); try { ipRanges.add( Cidr.parse( range, true ).toString( ) ); } catch ( IllegalArgumentException e ) { if ( forRevoke ) { ipRanges.add( range ); } else { throw e; } } } catch ( IllegalArgumentException e ) { throw new IllegalArgumentException( "Invalid IP range: '"+range+"'" ); } } NetworkRule rule = NetworkRule.create( ipPerm.getIpProtocol( ), anyProtocolAllowed, ipPerm.getFromPort( ), ipPerm.getToPort( ), IpPermissionTypeExtractNetworkPeers.INSTANCE.apply( ipPerm ), ipRanges ); ruleList.add( rule ); if ( forRevoke && !ipRanges.equals( literalIpRanges ) ) { // When removing rules we must allow removal of rules that were created before we // fully validated CIDRs so create an additional network rule for matching if needed ruleList.add( NetworkRule.create( ipPerm.getIpProtocol( ), anyProtocolAllowed, ipPerm.getFromPort( ), ipPerm.getToPort( ), IpPermissionTypeExtractNetworkPeers.INSTANCE.apply( ipPerm ), literalIpRanges ) ); } } else { throw new IllegalArgumentException( "Invalid Ip Permissions: must specify either a source cidr or user" ); } return ruleList; } } static List<NetworkRule> ipPermissionAsNetworkRules( final IpPermissionType ipPermission, final boolean vpc, final boolean forRevoke ) { return ipPermissionsAsNetworkRules( Collections.singletonList( ipPermission ), vpc, forRevoke ); } static List<NetworkRule> ipPermissionsAsNetworkRules( final List<IpPermissionType> ipPermissions, final boolean vpc, final boolean forRevoke ) { return Lists.newArrayList( Iterables.concat( Iterables.transform( ipPermissions, new IpPermissionTypeAsNetworkRule( vpc, forRevoke ) ) ) ); } @RestrictedTypes.Resolver( NetworkGroup.class ) public enum Lookup implements Function<String, NetworkGroup> { INSTANCE; @Override public NetworkGroup apply( final String identifier ) { try { return NetworkGroups.lookupByGroupId( identifier ); } catch ( NoSuchMetadataException e ) { throw Exceptions.toUndeclared( e ); } } } public static class NetworkGroupFilterSupport extends FilterSupport<NetworkGroup> { public NetworkGroupFilterSupport() { super( builderFor( NetworkGroup.class ) .withTagFiltering( NetworkGroupTag.class, "networkGroup" ) .withStringProperty( "description", NetworkGroup.description( ) ) .withStringProperty( "group-id", NetworkGroup.groupId() ) .withStringProperty( "group-name", CloudMetadatas.toDisplayName( ) ) .withStringSetProperty( "ip-permission.cidr", FilterSetFunctions.PERMISSION_CIDR ) .withStringSetProperty( "ip-permission.from-port", FilterSetFunctions.PERMISSION_FROM_PORT ) .withStringSetProperty( "ip-permission.group-id", FilterSetFunctions.PERMISSION_GROUP_ID ) .withStringSetProperty( "ip-permission.group-name", FilterSetFunctions.PERMISSION_GROUP ) .withStringSetProperty( "ip-permission.protocol", FilterSetFunctions.PERMISSION_PROTOCOL ) .withStringSetProperty( "ip-permission.to-port", FilterSetFunctions.PERMISSION_TO_PORT ) .withStringSetProperty( "ip-permission.user-id", FilterSetFunctions.PERMISSION_ACCOUNT_ID ) .withStringProperty( "owner-id", NetworkGroup.accountNumber() ) .withStringProperty( "vpc-id", NetworkGroup.vpcId() ) .withPersistenceAlias( "networkRules", "networkRules" ) .withPersistenceFilter( "description" ) .withPersistenceFilter( "group-id", "groupId" ) .withPersistenceFilter( "group-name", "displayName" ) .withPersistenceFilter( "ip-permission.from-port", "networkRules.lowPort", PersistenceFilter.Type.Integer ) .withPersistenceFilter( "ip-permission.protocol", "networkRules.protocol", FUtils.valueOfFunction( NetworkRule.Protocol.class ) ) .withPersistenceFilter( "ip-permission.to-port", "networkRules.highPort", PersistenceFilter.Type.Integer ) .withPersistenceFilter( "owner-id", "ownerAccountNumber" ) .withPersistenceFilter( "vpc-id", "vpcId" ) ); } } private enum FilterSetFunctions implements Function<NetworkGroup,Set<String>> { PERMISSION_CIDR { @Override public Set<String> apply( final NetworkGroup group ) { final Set<String> result = Sets.newHashSet(); for ( final NetworkRule rule : group.getNetworkRules() ) { result.addAll( rule.getIpRanges() ); } return result; } }, PERMISSION_FROM_PORT { @Override public Set<String> apply( final NetworkGroup group ) { final Set<String> result = Sets.newHashSet(); for ( final NetworkRule rule : group.getNetworkRules() ) { result.addAll( Optional.fromNullable( rule.getLowPort() ).transform( Functions.toStringFunction() ).asSet() ); } return result; } }, PERMISSION_GROUP { @Override public Set<String> apply( final NetworkGroup group ) { final Set<String> result = Sets.newHashSet(); for ( final NetworkRule rule : group.getNetworkRules() ) { for ( final NetworkPeer peer : rule.getNetworkPeers() ) { if ( peer.getGroupName() != null ) result.add( peer.getGroupName() ); } } return result; } }, PERMISSION_GROUP_ID { @Override public Set<String> apply( final NetworkGroup group ) { final Set<String> result = Sets.newHashSet(); for ( final NetworkRule rule : group.getNetworkRules() ) { for ( final NetworkPeer peer : rule.getNetworkPeers() ) { if ( peer.getGroupId() != null ) result.add( peer.getGroupId() ); } } return result; } }, PERMISSION_PROTOCOL { @Override public Set<String> apply( final NetworkGroup group ) { final Set<String> result = Sets.newHashSet(); for ( final NetworkRule rule : group.getNetworkRules() ) { result.add( rule.getDisplayProtocol( ) ); } return result; } }, PERMISSION_TO_PORT { @Override public Set<String> apply( final NetworkGroup group ) { final Set<String> result = Sets.newHashSet(); for ( final NetworkRule rule : group.getNetworkRules() ) { result.addAll( Optional.fromNullable( rule.getHighPort() ).transform( Functions.toStringFunction() ).asSet() ); } return result; } }, PERMISSION_ACCOUNT_ID { @Override public Set<String> apply( final NetworkGroup group ) { final Set<String> result = Sets.newHashSet(); for ( final NetworkRule rule : group.getNetworkRules() ) { for ( final NetworkPeer peer : rule.getNetworkPeers() ) { if ( peer.getUserQueryKey() != null ) result.add( peer.getUserQueryKey() ); } } return result; } } } @RestrictedTypes.QuantityMetricFunction( CloudMetadata.NetworkGroupMetadata.class ) public enum CountNetworkGroups implements Function<OwnerFullName, Long> { INSTANCE; @Override public Long apply( @Nullable final OwnerFullName input ) { try ( final TransactionResource tx = Entities.transactionFor( NetworkGroup.class ) ) { return Entities.count( NetworkGroup.withOwner( input ) ); //TODO:STEVE: Quota for regular vs VPC security groups } } } }