def test_auto_stop_nonzero(monkeypatch): metricsHelper = MetricsHelper('us-west-2') def mock_get_metric_statistics(Dimensions=[{ 'Name': 'WorkspaceId', 'Value': 'ws-xxxxxxxxx' }], Namespace='AWS/WorkSpaces', MetricName='Stopped', StartTime=startTime, EndTime=endTime, Period=3600, Statistics=['Minimum', 'Maximum']): return { 'Datapoints': [{ 'Timestamp': timeStamp, 'Minimum': 0.0, 'Unit': 'Count' }, { 'Timestamp': timeStamp, 'Minimum': 1.0, 'Unit': 'Count' }], 'ResponseMetadata': { 'HTTPStatusCode': 200 } } monkeypatch.setattr(metricsHelper.client, 'get_metric_statistics', mock_get_metric_statistics) billableTime = metricsHelper.get_billable_time('ws-xxxxxxxxx', 'AUTO_STOP', startTime, endTime) assert type(billableTime) is int assert billableTime != 2
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
def test_get_billable_time(): settings = 'us-east-1' workspaceID = 'ws-abc1234XYZ' startTime = '2020-04-01T00:00:00Z' endTime = '2020-04-02T20:35:58Z' metrics_helper = MetricsHelper(settings) client_stubber = Stubber(metrics_helper.client) response = { 'Label': 'UserConnected', 'Datapoints': [ {'Timestamp': datetime.datetime(2020, 4, 2, 11, 0, tzinfo=tzutc()), 'Maximum': 1.0, 'Unit': 'Count'}, {'Timestamp': datetime.datetime(2020, 4, 1, 7, 0, tzinfo=tzutc()), 'Maximum': 1.0, 'Unit': 'Count'}, {'Timestamp': datetime.datetime(2020, 4, 2, 6, 0, tzinfo=tzutc()), 'Maximum': 1.0, 'Unit': 'Count'}, {'Timestamp': datetime.datetime(2020, 4, 1, 2, 0, tzinfo=tzutc()), 'Maximum': 0.0, 'Unit': 'Count'}, {'Timestamp': datetime.datetime(2020, 4, 2, 1, 0, tzinfo=tzutc()), 'Maximum': 0.0, 'Unit': 'Count'} ] } expected_params = { 'Dimensions': [ { 'Name': 'WorkspaceId', 'Value': workspaceID } ], 'Namespace': 'AWS/WorkSpaces', 'MetricName': 'UserConnected', 'StartTime': startTime, 'EndTime': endTime, 'Period': 3600, 'Statistics': ['Maximum'] } client_stubber.add_response('get_metric_statistics', response, expected_params) client_stubber.activate() billable_time = metrics_helper.get_billable_time(workspaceID, startTime, endTime) assert billable_time == 3
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 { 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) workspaceBundleType = self.get_bundle_type(workspace) log.debug('workspaceBundleType: %s', workspaceBundleType) workspaceRunningMode = workspace['WorkspaceProperties']['RunningMode'] log.debug('workspaceRunningMode: %s', workspaceRunningMode) hourlyThreshold = self.get_hourly_threshold(workspaceBundleType) billableTime = 0 if self.check_for_skip_tag(workspaceID) == True: log.info('Skipping WorkSpace %s due to Skip_Convert tag', workspaceID) optimizationResult = 'S' else: billableTime = self.metricsHelper.get_billable_time( workspaceID, workspaceRunningMode, self.settings['startTime'], self.settings['endTime']) print billableTime 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 ''' def get_bundle_type(self, workspace): describeBundle = self.client.describe_workspace_bundles( BundleIds=[workspace['BundleId']]) return describeBundle['Bundles'][0]['ComputeType']['Name'] ''' 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): for i in range(0, self.maxRetries): 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) if i >= self.maxRetries - 1: log.error('ExceededMaxRetries') else: time.sleep(i / 10) ''' returns bool ''' def check_for_skip_tag(self, workspaceID): tags = self.get_tags(workspaceID) for tagPair in tags: if tagPair['Key'] == 'Skip_Convert': return True return False ''' returns [ { 'Key': 'str', 'Value': 'str' }, ... ] ''' def get_tags(self, workspaceID): for i in range(0, self.maxRetries): try: workspaceTags = self.client.describe_tags( ResourceId=workspaceID) log.debug(workspaceTags) return workspaceTags['TagList'] except botocore.exceptions.ClientError as e: log.error(e) if i >= self.maxRetries - 1: log.error('ExceededMaxRetries') else: time.sleep(i / 10) ''' returns str ''' def modify_workspace_properties(self, workspaceID, newRunningMode, isDryRun): for i in range(0, self.maxRetries): 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.exeptions.ClientError as e: if i >= self.maxRetries - 1: result = '-E-' else: time.sleep(i / 10) 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}
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'], result['directoryID'], result['userName'], result['computerName'], str(result['billableTime']), result['lastConnectionTime'], 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, directoryID: str, userName: str computerName: str billableTime: int, lastConnectionTime: str, 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']) lastKnownUserConnectionTimestamp = self.client.describe_workspaces_connection_status( WorkspaceIds=[workspaceID])['WorkspacesConnectionStatus'][0].get( 'LastKnownUserConnectionTimestamp') if lastKnownUserConnectionTimestamp is None: lastConnectionTime = 'Unavailable' else: lastConnectionTime = str(lastKnownUserConnectionTimestamp) 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, 'directoryID': workspace['DirectoryId'], 'userName': workspace['UserName'], 'computerName': workspace['ComputerName'], 'billableTime': billableTime, 'lastConnectionTime': lastConnectionTime, '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}