/************************************************************************* * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP * * 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/. ************************************************************************/ package com.eucalyptus.portal; import static com.eucalyptus.util.RestrictedTypes.getIamActionByMessageType; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; import javax.inject.Inject; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.eucalyptus.auth.principal.AccountFullName; import com.eucalyptus.objectstorage.client.EucaS3Client; import com.eucalyptus.portal.monthlyreport.MonthlyReports; import com.eucalyptus.portal.workflow.AwsUsageRecord; import com.eucalyptus.portal.awsusage.AwsUsageRecords; import com.eucalyptus.portal.common.model.*; import org.apache.log4j.Logger; import com.eucalyptus.auth.AuthContextSupplier; import com.eucalyptus.auth.Permissions; import com.eucalyptus.component.annotation.ComponentNamed; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.portal.common.TagClient; import com.eucalyptus.portal.common.policy.PortalPolicySpec; import com.eucalyptus.util.Exceptions; import com.eucalyptus.util.TypeMappers; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; /** * */ @SuppressWarnings( "unused" ) @ComponentNamed public class PortalService { private static final Logger logger = Logger.getLogger( PortalService.class ); private static final Map<String,String> AWS_USAGE_SERVICE_MAP = ImmutableMap.<String,String>builder() .put( "ec2", "AmazonEC2" ) .put( "s3", "AmazonS3" ) .put( "cloudwatch", "AmazonCloudWatch" ) .build( ); private static final Map<String,Map<String,String>> AWS_USAGE_USAGE_TYPE_MAPS = ImmutableMap.<String,Map<String,String>>builder() .put( "AmazonCloudWatch", ImmutableMap.of( "request", "Request-NoCharge" ) ) .build( ); private final BillingAccounts billingAccounts; private final BillingInfos billingInfos; private final TagClient tagClient; @Inject public PortalService( final BillingAccounts billingAccounts, final BillingInfos billingInfos, final TagClient tagClient ) { this.billingAccounts = billingAccounts; this.billingInfos = billingInfos; this.tagClient = tagClient; } public ModifyAccountResponseType modifyAccount( final ModifyAccountType request ) throws PortalServiceException { final Context context = checkAuthorized( ); final ModifyAccountResponseType response = request.getReply( ); if ( request.getUserBillingAccess( ) != null ) { Function<BillingAccount,BillingAccount> updater = account -> { account.setUserAccessEnabled( request.getUserBillingAccess( ) ); return account; }; try { try { response.getResult( ).setAccountSettings( billingAccounts.updateByAccount( context.getAccountNumber( ), context.getAccount( ), account -> TypeMappers.transform( updater.apply( account ), AccountSettings.class ) ) ); } catch ( PortalMetadataNotFoundException e ) { final BillingAccount billingAccount = updater.apply( billingAccounts.defaults( ) ); billingAccount.setOwner( context.getUserFullName( ) ); billingAccount.setDisplayName( context.getAccountNumber( ) ); response.getResult( ).setAccountSettings( billingAccounts.save( billingAccount, TypeMappers.lookupF( BillingAccount.class, AccountSettings.class ) ) ); } } catch ( Exception e ) { throw handleException( e ); } } return response; } public ViewAccountResponseType viewAccount( final ViewAccountType request ) throws PortalServiceException { final Context context = checkAuthorized( ); final ViewAccountResponseType response = request.getReply( ); try { response.getResult( ).setAccountSettings( billingAccounts.lookupByAccount( context.getAccountNumber( ), context.getAccount( ), TypeMappers.lookupF( BillingAccount.class, AccountSettings.class ) ) ); } catch ( PortalMetadataNotFoundException e ) { response.getResult( ).setAccountSettings( TypeMappers.transform( billingAccounts.defaults( ), AccountSettings.class ) ); } catch ( Exception e ) { throw handleException( e ); } return response; } public ModifyBillingResponseType modifyBilling( final ModifyBillingType request ) throws PortalServiceException { final Context context = checkAuthorized( ); final ModifyBillingResponseType response = request.getReply( ); Function<BillingInfo,BillingInfo> updater = info -> { info.setBillingReportsBucket( request.getReportBucket( ) ); info.setDetailedBillingEnabled( MoreObjects.firstNonNull( request.getDetailedBillingEnabled( ), false ) ); if ( request.getActiveCostAllocationTags( ) != null ) { info.setActiveCostAllocationTags( request.getActiveCostAllocationTags( ) ); } return info; }; final Predicate<String> testBucket = (bucket) -> { try { final EucaS3Client s3c = BucketUploadableActivities.getS3Client(); PutObjectRequest req = new PutObjectRequest(bucket, "aws-programmatic-access-test-object", new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8)), new ObjectMetadata()) .withCannedAcl(CannedAccessControlList.BucketOwnerFullControl); s3c.putObject(req); return true; } catch (final Exception ex) { ; } return false; }; try { if (request.getReportBucket()!=null && !testBucket.test(request.getReportBucket()) ) { throw new PortalInvalidParameterException("Requested bucket is not accessible by billing"); } try { response.getResult( ).setBillingSettings( billingInfos.updateByAccount( context.getAccountNumber( ), context.getAccount( ), info -> TypeMappers.transform( updater.apply( info ), BillingSettings.class ) ) ); } catch ( PortalMetadataNotFoundException e ) { final BillingInfo billingInfo = updater.apply( billingInfos.defaults( ) ); billingInfo.setOwner( context.getUserFullName( ) ); billingInfo.setDisplayName( context.getAccountNumber( ) ); response.getResult( ).setBillingSettings( billingInfos.save( billingInfo, TypeMappers.lookupF( BillingInfo.class, BillingSettings.class ) ) ); } } catch ( Exception e ) { throw handleException( e ); } return response; } public ViewBillingResponseType viewBilling( final ViewBillingType request ) throws PortalServiceException { final Context context = checkAuthorized( ); final ViewBillingResponseType response = request.getReply( ); try { response.getResult( ).setBillingSettings( billingInfos.lookupByAccount( context.getAccountNumber( ), context.getAccount( ), TypeMappers.lookupF( BillingInfo.class, BillingSettings.class ) ) ); } catch ( PortalMetadataNotFoundException e ) { response.getResult( ).setBillingSettings( TypeMappers.transform( billingInfos.defaults( ), BillingSettings.class ) ); } catch ( Exception e ) { throw handleException( e ); } try { final Set<String> inactiveTagKeys = Sets.newTreeSet( ); inactiveTagKeys.addAll( tagClient.getTagKeys( new GetTagKeysType( ).markPrivileged( ) ).getResult( ).getKeys( ) ); inactiveTagKeys.removeAll( response.getResult( ).getBillingSettings( ).getActiveCostAllocationTags( ) ); response.getResult( ).getBillingMetadata( ).setInactiveCostAllocationTags( Lists.newArrayList( Ordering.from( String.CASE_INSENSITIVE_ORDER ).sortedCopy( inactiveTagKeys ) ) ); } catch ( Exception e ) { logger.error( "Error loading tag keys", e ); } return response; } public ViewUsageResponseType viewUsage(final ViewUsageType request) throws PortalServiceException { final Context context = checkAuthorized( ); final ViewUsageResponseType response = request.getReply(); final Function<ViewUsageType, Optional<PortalServiceException>> requestVerifier = (req) -> { final String granularity = req.getReportGranularity() != null ? req.getReportGranularity().toLowerCase() : null; if (granularity==null) { return Optional.of(new PortalInvalidParameterException("Granularity must be specified")); } if (!Sets.newHashSet("hourly", "hour", "daily", "day", "monthly", "month").contains(granularity)) { return Optional.of(new PortalInvalidParameterException("Can't recognize granularity. Valid values are hourly, daily, and monthly")); } final String service = req.getServices() != null ? req.getServices().toLowerCase() : null; if (service == null) { return Optional.of(new PortalInvalidParameterException("Service name must be specified")); } else if (!Sets.newHashSet("ec2", "s3", "cloudwatch").contains(service)) { return Optional.of(new PortalInvalidParameterException("Can't recognize service name. Supported services are ec2, s3, and cloudwatch")); } if (req.getTimePeriodFrom() == null) { return Optional.of(new PortalInvalidParameterException("Beginning time period must be specified")); } if (req.getTimePeriodTo() == null) { return Optional.of(new PortalInvalidParameterException("Ending time period must be specified")); } return Optional.empty(); }; final Function<ViewUsageType, ViewUsageType> requestFormatter = (req) -> { final String service = req.getServices().toLowerCase(); if ( AWS_USAGE_SERVICE_MAP.containsKey( service ) ) { req.setServices( AWS_USAGE_SERVICE_MAP.get( service ) ); } final String operation = req.getOperations(); if (operation != null && "all".equals(operation.toLowerCase())) { req.setOperations(null); } if ( "all".equalsIgnoreCase( request.getUsageTypes( ) ) ) { req.setUsageTypes( null ); } else if ( AWS_USAGE_USAGE_TYPE_MAPS.containsKey( request.getServices( ) ) ) { final Map<String,String> usageTypeMapForService = AWS_USAGE_USAGE_TYPE_MAPS.get( request.getServices( ) ); if ( usageTypeMapForService.containsKey( request.getUsageTypes( ) ) ) { request.setUsageTypes( usageTypeMapForService.get( request.getUsageTypes( ) ) ); } } return req; }; final Optional<PortalServiceException> error = requestVerifier.apply(request); if (error.isPresent()) { throw error.get(); } final ViewUsageType req = requestFormatter.apply(request); final String service = req.getServices(); final String operation = req.getOperations(); final String usageType = req.getUsageTypes(); final String granularity = req.getReportGranularity(); final Date periodBegin = req.getTimePeriodFrom(); final Date periodEnd = req.getTimePeriodTo(); final List<AwsUsageRecord> records = Lists.newArrayList(); if (granularity != null && granularity.startsWith("hour")) { records.addAll(AwsUsageRecords.getInstance().queryHourly( context.getAccountNumber(), service, operation, usageType, periodBegin, periodEnd)); } else if (granularity != null && (granularity.startsWith("day") || granularity.startsWith("dai"))) { records.addAll(AwsUsageRecords.getInstance().queryDaily( context.getAccountNumber(), service, operation, usageType, periodBegin, periodEnd)); } else if (granularity != null && granularity.startsWith("month")) { records.addAll(AwsUsageRecords.getInstance().queryMonthly( context.getAccountNumber(), service, operation, usageType, periodBegin, periodEnd)); } else { throw new PortalInvalidParameterException("Valid report granularity are hourly, daily or monthly"); } final Function<AwsUsageRecord, String> formatter = (r) -> { final StringBuilder sb = new StringBuilder(); final DateFormat df = new SimpleDateFormat("MM/dd/yy HH:mm:ss"); //Service, Operation, UsageType, Resource, StartTime, EndTime, UsageValue //AmazonEC2,Unknown,CW:AlarmMonitorUsage,,11/01/16 00:00:00,11/02/16 00:00:00,48 sb.append(r.getService()!=null ? r.getService() + "," : ","); sb.append(r.getOperation()!=null ? r.getOperation() + "," : ","); sb.append(r.getUsageType()!=null ? r.getUsageType() + "," : ","); sb.append(r.getResource()!=null ? r.getResource() + "," : ","); sb.append(r.getStartTime()!=null ? df.format(r.getStartTime()) + "," : ","); sb.append(r.getEndTime()!=null ? df.format(r.getEndTime()) + "," : ","); sb.append(r.getUsageValue()!=null ? r.getUsageValue(): ""); return sb.toString(); }; response.setResult(new ViewUsageResult()); final StringBuilder sb = new StringBuilder(); sb.append("Service, Operation, UsageType, Resource, StartTime, EndTime, UsageValue"); final Optional<String> data = records.stream() .map (formatter) .reduce((l1, l2) -> String.format("%s\n%s", l1, l2)); if (data.isPresent()) { sb.append("\n"); sb.append(data.get()); } response.getResult().setData(sb.toString()); return response; } public ViewMonthlyUsageResponseType viewMonthlyUsage(final ViewMonthlyUsageType request) throws PortalServiceException { final Context context = checkAuthorized( ); final ViewMonthlyUsageResponseType response = request.getReply(); final Predicate<ViewMonthlyUsageType> requestVerifier = (req) -> { final String year = req.getYear(); final String month = req.getMonth(); if (! Pattern.matches("2[0-9][0-9][0-9]", year)) // Do EUCA exists in year 3000? return false; if (! Pattern.matches("[0-1]?[0-9]", month)) { return false; } try { final int nMonth = Integer.parseInt(month); if (! (nMonth >= 1 && nMonth <= 12)) return false; } catch (final NumberFormatException ex) { return false; } return true; }; if (!requestVerifier.test(request)) throw new PortalInvalidParameterException("Invalid year and month requested"); final String year; final String month; try { year = String.format("%d", Integer.parseInt(request.getYear())); month = String.format("%d", Integer.parseInt(request.getMonth())); } catch (final NumberFormatException ex) { throw new PortalInvalidParameterException("Invalid year and month requested"); } response.setResult( new ViewMonthlyUsageResult()); final StringBuilder sb = new StringBuilder(); sb.append("\"InvoiceID\",\"PayerAccountId\",\"LinkedAccountId\",\"RecordType\",\"RecordID\",\"BillingPeriodStartDate\"," + "\"BillingPeriodEndDate\",\"InvoiceDate\",\"PayerAccountName\",\"LinkedAccountName\",\"TaxationAddress\"," + "\"PayerPONumber\",\"ProductCode\",\"ProductName\",\"SellerOfRecord\",\"UsageType\",\"Operation\",\"RateId\"," + "\"ItemDescription\",\"UsageStartDate\",\"UsageEndDate\",\"UsageQuantity\",\"BlendedRate\",\"CurrencyCode\"," + "\"CostBeforeTax\",\"Credits\",\"TaxAmount\",\"TaxType\",\"TotalCost\""); try { final Optional<String> data = MonthlyReports.getInstance() .lookupReport(AccountFullName.getInstance(context.getAccountNumber()), year, month).stream() .reduce((l1, l2) -> String.format("%s\n%s", l1, l2)); if (data.isPresent()) { sb.append("\n"); sb.append(data.get()); } } catch (final NoSuchElementException ex) { ; } response.getResult().setData(sb.toString()); return response; } protected static Context checkAuthorized( ) throws PortalServiceException { final Context ctx = Contexts.lookup( ); final AuthContextSupplier requestUserSupplier = ctx.getAuthContext( ); if ( !Permissions.isAuthorized( PortalPolicySpec.VENDOR_PORTAL, "", "", null, getIamActionByMessageType( ), requestUserSupplier ) ) { throw new PortalServiceUnauthorizedException( "UnauthorizedOperation", "You are not authorized to perform this operation." ); } return ctx; } /** * Method always throws, signature allows use of "throw handleException ..." */ private static PortalServiceException handleException( final Exception e ) throws PortalServiceException { Exceptions.findAndRethrow( e, PortalServiceException.class ); logger.error( e, e ); final PortalServiceException exception = new PortalServiceException( "InternalError", String.valueOf(e.getMessage()) ); if ( Contexts.lookup( ).hasAdministrativePrivileges() ) { exception.initCause( e ); } throw exception; } }