#!/usr/bin/python # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # # # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # # with the License. A copy of the License is located at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES # # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # # and limitations under the License. # ###################################################################################################################### # This file reads the AWS workspaces properties and will change the billing preference if necessary # It calls the metrics_helper to determine if changes are required import boto3 import botocore from botocore.config import Config from botocore.exceptions import ClientError import logging import time from lib.metrics_helper import MetricsHelper log = logging.getLogger() botoConfig = Config( max_pool_connections=100, retries={'max_attempts': 20} ) class WorkspacesHelper(object): def __init__(self, settings): self.settings = settings self.maxRetries = 20 self.region = settings['region'] self.hourlyLimits = settings['hourlyLimits'] self.testEndOfMonth = settings['testEndOfMonth'] self.isDryRun = settings['isDryRun'] self.client = boto3.client( 'workspaces', region_name=self.region, config=botoConfig ) self.metricsHelper = MetricsHelper(self.region) return ''' returns str ''' def append_entry(self, oldCsv, result): s = ',' csv = oldCsv + s.join(( result['workspaceID'], str(result['billableTime']), str(result['hourlyThreshold']), result['optimizationResult'], result['bundleType'], result['initialMode'], result['newMode'] + '\n' )) return csv ''' returns str ''' def expand_csv(self, rawCSV): csv = rawCSV.replace(',-M-', ',ToMonthly').replace(',-H-', ',ToHourly').replace(',-E-', ',Exceeded MaxRetries').replace(',-N-', ',No Change').replace(',-S-', ',Skipped') return csv ''' returns { workspaceID: str, billableTime: int, hourlyThreshold: int, optimizationResult: str, initialMode: str, newMode: str, bundleType: str } ''' def process_workspace(self, workspace): workspaceID = workspace['WorkspaceId'] log.debug('workspaceID: %s', workspaceID) workspaceRunningMode = workspace['WorkspaceProperties']['RunningMode'] log.debug('workspaceRunningMode: %s', workspaceRunningMode) workspaceBundleType = workspace['WorkspaceProperties']['ComputeTypeName'] log.debug('workspaceBundleType: %s', workspaceBundleType) billableTime = self.metricsHelper.get_billable_time( workspaceID, self.settings['startTime'], self.settings['endTime'] ); if self.check_for_skip_tag(workspaceID) == True: log.info('Skipping WorkSpace %s due to Skip_Convert tag', workspaceID) optimizationResult = { 'resultCode': '-S-', 'newMode': workspaceRunningMode } hourlyThreshold = "n/a" else: hourlyThreshold = self.get_hourly_threshold(workspaceBundleType) optimizationResult = self.compare_usage_metrics( workspaceID, billableTime, hourlyThreshold, workspaceRunningMode ) return { 'workspaceID': workspaceID, 'billableTime': billableTime, 'hourlyThreshold': hourlyThreshold, 'optimizationResult': optimizationResult['resultCode'], 'newMode': optimizationResult['newMode'], 'bundleType': workspaceBundleType, 'initialMode': workspaceRunningMode } ''' returns str ''' ''' returns int ''' def get_hourly_threshold(self, bundleType): if bundleType in self.hourlyLimits: return int(self.hourlyLimits[bundleType]) else: return None ''' returns { Workspaces: [obj...], NextToken: str } ''' def get_workspaces_page(self, directoryID, nextToken): try: if nextToken == 'None': result = self.client.describe_workspaces( DirectoryId = directoryID ) else: result = self.client.describe_workspaces( DirectoryId = directoryID, NextToken = nextToken ) return result except botocore.exceptions.ClientError as e: log.error(e) ''' returns bool ''' def check_for_skip_tag(self, workspaceID): tags = self.get_tags(workspaceID) # Added for case insensitive matching. Works with standard alphanumeric tags for tagPair in tags: if tagPair['Key'].lower() == 'Skip_Convert'.lower(): return True return False ''' returns [ { 'Key': 'str', 'Value': 'str' }, ... ] ''' def get_tags(self, workspaceID): try: workspaceTags = self.client.describe_tags( ResourceId = workspaceID ) log.debug(workspaceTags) return workspaceTags['TagList'] except botocore.exceptions.ClientError as e: log.error(e) ''' returns str ''' def modify_workspace_properties(self, workspaceID, newRunningMode, isDryRun): log.debug('modifyWorkspaceProperties') try: if isDryRun == False: wsModWS = self.client.modify_workspace_properties( WorkspaceId = workspaceID, WorkspaceProperties = { 'RunningMode': newRunningMode } ) else: log.info('Skipping modifyWorkspaceProperties for Workspace %s due to dry run', workspaceID) if newRunningMode == 'ALWAYS_ON': result = '-M-' elif newRunningMode == 'AUTO_STOP': result = '-H-' return result except botocore.exceptions.ClientError as e: log.error('Exceeded retries for %s due to error: %s', workspaceID, e) return result ''' returns { 'resultCode': str, 'newMode': str } ''' def compare_usage_metrics(self, workspaceID, billableTime, hourlyThreshold, workspaceRunningMode): if hourlyThreshold == None: return { 'resultCode': '-S-', 'newMode': workspaceRunningMode } # If the Workspace is in Auto Stop (hourly) if workspaceRunningMode == 'AUTO_STOP': log.debug('workspaceRunningMode {} == AUTO_STOP'.format(workspaceRunningMode)) # If billable time is over the threshold for this bundle type if billableTime > hourlyThreshold: log.debug('billableTime {} > hourlyThreshold {}'.format(billableTime, hourlyThreshold)) # Change the workspace to ALWAYS_ON resultCode = self.modify_workspace_properties(workspaceID, 'ALWAYS_ON', self.isDryRun) newMode = 'ALWAYS_ON' # Otherwise, report no change for the Workspace elif billableTime <= hourlyThreshold: log.debug('billableTime {} <= hourlyThreshold {}'.format(billableTime, hourlyThreshold)) resultCode = '-N-' newMode = 'AUTO_STOP' # Or if the Workspace is Always On (monthly) elif workspaceRunningMode == 'ALWAYS_ON': log.debug('workspaceRunningMode {} == ALWAYS_ON'.format(workspaceRunningMode)) # Only perform metrics gathering for ALWAYS_ON Workspaces at the end of the month. if self.testEndOfMonth == True: log.debug('testEndOfMonth {} == True'.format(self.testEndOfMonth)) # If billable time is under the threshold for this bundle type if billableTime <= hourlyThreshold: log.debug('billableTime {} < hourlyThreshold {}'.format(billableTime, hourlyThreshold)) # Change the workspace to AUTO_STOP resultCode = self.modify_workspace_properties(workspaceID, 'AUTO_STOP', self.isDryRun) newMode = 'AUTO_STOP' # Otherwise, report no change for the Workspace elif billableTime > hourlyThreshold: log.debug('billableTime {} >= hourlyThreshold {}'.format(billableTime, hourlyThreshold)) resultCode = '-N-' newMode = 'ALWAYS_ON' elif self.testEndOfMonth == False: log.debug('testEndOfMonth {} == False'.format(self.testEndOfMonth)) resultCode = '-N-' newMode = 'ALWAYS_ON' # Otherwise, we don't know what it is so skip. else: log.warning('workspaceRunningMode {} is unrecognized for workspace {}'.format(workspaceRunningMode, workspaceID)) resultCode = '-S-' newMode = workspaceRunningMode return { 'resultCode': resultCode, 'newMode': newMode }