Skip to content

Commit b1cd9fe

Browse files
chodono-awsiakov-aws
authored andcommitted
Updated to include WorkSpaces inventory scanning.
1 parent 025298c commit b1cd9fe

File tree

5 files changed

+209
-3
lines changed

5 files changed

+209
-3
lines changed

data-collection/deploy/deploy-in-linked-account.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ Resources:
241241
- "eks:ListNodegroups"
242242
- "eks:DescribeNodegroup"
243243
- "lambda:ListFunctions"
244+
- "workspaces:DescribeWorkspaces"
245+
- "workspaces:DescribeWorkspaceDirectories"
246+
- "workspaces:DescribeWorkspacesConnectionStatus"
244247
Resource: "*" ## Policy is used for scanning of a wide range of resources
245248
Roles:
246249
- Ref: LambdaRole

data-collection/deploy/module-inventory.yaml

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Parameters:
5656
Description: ARN of a Lambda for Managing GlueTable
5757
AwsObjects:
5858
Type: CommaDelimitedList
59-
Default: OpensearchDomains, ElasticacheClusters, RdsDbInstances, EBS, AMI, Snapshot, Ec2Instances, VpcInstances, RdsDbSnapshots, EKSClusters, LambdaFunctions, RdsDbClusters
59+
Default: OpensearchDomains, ElasticacheClusters, RdsDbInstances, EBS, AMI, Snapshot, Ec2Instances, VpcInstances, RdsDbSnapshots, EKSClusters, LambdaFunctions, RdsDbClusters, WorkSpaces
6060
Description: Services for pulling price data
6161
DataBucketsKmsKeysArns:
6262
Type: String
@@ -1011,6 +1011,73 @@ Mappings:
10111011
SerializationLibrary: org.openx.data.jsonserde.JsonSerDe
10121012
TableType: EXTERNAL_TABLE
10131013

1014+
WorkSpaces:
1015+
path: workspaces
1016+
table:
1017+
- Name: inventory_workspaces_data
1018+
Parameters:
1019+
classification: json
1020+
compressionType: none
1021+
PartitionKeys:
1022+
- Name: payer_id
1023+
Type: string
1024+
- Name: year
1025+
Type: string
1026+
- Name: month
1027+
Type: string
1028+
- Name: day
1029+
Type: string
1030+
StorageDescriptor:
1031+
Columns:
1032+
- Name: workspaceid
1033+
Type: string
1034+
- Name: username
1035+
Type: string
1036+
- Name: computetype
1037+
Type: string
1038+
- Name: directoryid
1039+
Type: string
1040+
- Name: directoryname
1041+
Type: string
1042+
- Name: directorytype
1043+
Type: string
1044+
- Name: directoryalias
1045+
Type: string
1046+
- Name: directorymaintenance
1047+
Type: string
1048+
- Name: state
1049+
Type: string
1050+
- Name: connectionstatus
1051+
Type: string
1052+
- Name: lastconnected
1053+
Type: string
1054+
- Name: lastinventoryrun
1055+
Type: string
1056+
- Name: runningmode
1057+
Type: string
1058+
- Name: operatingsystemname
1059+
Type: string
1060+
- Name: protocol
1061+
Type: string
1062+
- Name: computername
1063+
Type: string
1064+
- Name: ipaddress
1065+
Type: string
1066+
- Name: accountid
1067+
Type: string
1068+
- Name: collection_date
1069+
Type: string
1070+
- Name: region
1071+
Type: string
1072+
InputFormat: org.apache.hadoop.mapred.TextInputFormat
1073+
Location: !Sub s3://${DestinationBucket}/inventory/inventory-workspaces-data/
1074+
OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
1075+
SerdeInfo:
1076+
Parameters:
1077+
paths: WorkspaceId,UserName,ComputeType,DirectoryId,DirectoryName,DirectoryType,DirectoryAlias,DirectoryMaintenance,State,ConnectionStatus,LastConnected,LastInventoryRun,RunningMode,OperatingSystemName,Protocol,ComputerName,IPAddress,accountid,collection_date,region
1078+
SerializationLibrary: org.openx.data.jsonserde.JsonSerDe
1079+
TableType: EXTERNAL_TABLE
1080+
10141081
Resources:
10151082
LambdaRole:
10161083
Type: AWS::IAM::Role
@@ -1196,6 +1263,68 @@ Resources:
11961263
logger.error(f"Cannot get info from {account_id}/{region}: {type(exc)}-{exc}")
11971264
return []
11981265
1266+
def workspaces_scan(account_id, region):
1267+
"""Special function to scan AWS WorkSpaces resources"""
1268+
# Define WorkSpaces supported regions
1269+
WORKSPACES_REGIONS = [
1270+
'us-east-1', 'us-west-2', 'ap-south-1', 'ap-northeast-2',
1271+
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ca-central-1',
1272+
'eu-central-1', 'eu-west-1', 'eu-west-2', 'sa-east-1']
1273+
# Skip if region is not supported by WorkSpaces
1274+
if region not in WORKSPACES_REGIONS:
1275+
logger.info(f"WorkSpaces not supported in region {region}. Skipping.")
1276+
return
1277+
try:
1278+
session = assume_session(account_id, region)
1279+
client = session.client('workspaces', region_name=region)
1280+
# Get WorkSpaces data
1281+
workspaces_data = list(client.get_paginator('describe_workspaces').paginate().search('Workspaces[*]'))
1282+
if not workspaces_data:
1283+
logger.info(f"No WorkSpaces found in {account_id}/{region}")
1284+
return
1285+
# Get connection status and directories for lookup
1286+
connection_status = client.describe_workspaces_connection_status()['WorkspacesConnectionStatus']
1287+
directories = client.describe_workspace_directories()['Directories']
1288+
# Create lookup dictionaries
1289+
connection_lookup = {conn['WorkspaceId']: conn for conn in connection_status}
1290+
directory_lookup = {d['DirectoryId']: d for d in directories}
1291+
# Process workspaces
1292+
for workspace in workspaces_data:
1293+
workspace_id = workspace['WorkspaceId']
1294+
dir_id = workspace['DirectoryId']
1295+
1296+
connection_info = connection_lookup.get(workspace_id, {})
1297+
dir_info = directory_lookup.get(dir_id, {})
1298+
1299+
workspace_info = {
1300+
'accountid': account_id,
1301+
'region': region,
1302+
'ResourceType': 'WorkSpace',
1303+
'WorkspaceId': workspace_id,
1304+
'UserName': workspace['UserName'],
1305+
'DirectoryId': dir_id,
1306+
'State': workspace['State'],
1307+
'ConnectionStatus': connection_info.get('ConnectionState', 'N/A'),
1308+
'LastConnected': connection_info.get('LastKnownUserConnectionTimestamp', 'Never').isoformat() if isinstance(connection_info.get('LastKnownUserConnectionTimestamp'), datetime) else "01/01/01 00:00:00",
1309+
'LastInventoryRun': time.strftime('%x %X'),
1310+
'RunningMode': workspace.get("WorkspaceProperties", {}).get("RunningMode", "N/A"),
1311+
'ComputeType': workspace.get("WorkspaceProperties", {}).get("ComputeTypeName", "N/A"),
1312+
'OperatingSystem': workspace.get("WorkspaceProperties", {}).get("OperatingSystemName", "N/A"),
1313+
'Protocol': ''.join(
1314+
c for c in str(workspace.get("WorkspaceProperties", {}).get("Protocols", "N/A"))
1315+
if c.isalnum() or c.isspace()
1316+
),
1317+
'DirectoryName': dir_info.get('DirectoryName', 'N/A'),
1318+
'DirectoryAlias': dir_info.get('Alias', 'N/A'),
1319+
'DirectoryType': dir_info.get('DirectoryType', 'N/A'),
1320+
'DirectoryMaintenance': dir_info.get("WorkspaceCreationProperties", {}).get("EnableMaintenanceMode", "N/A"),
1321+
'ComputerName': workspace.get("ComputerName", ""),
1322+
'IPAddress': workspace.get("IpAddress", "")
1323+
}
1324+
yield workspace_info
1325+
except Exception as exc:
1326+
logger.error(f"Cannot get WorkSpaces info from {account_id}/{region}: {type(exc)}-{exc}")
1327+
11991328
def lambda_handler(event, context): #pylint: disable=unused-argument
12001329
""" this lambda collects ami, snapshots and volumes from linked accounts
12011330
and must be called from the corresponding Step Function to orchestrate
@@ -1270,7 +1399,8 @@ Resources:
12701399
function_name='list_functions',
12711400
obj_name='Functions[*]'
12721401
),
1273-
'eks': eks_clusters_scan
1402+
'eks': eks_clusters_scan,
1403+
'workspaces': workspaces_scan
12741404
}
12751405
12761406
account = json.loads(event["account"])
@@ -1799,6 +1929,37 @@ Resources:
17991929
gp3_gb_cost + gp3_iops_cost + gp3_throughput_cost AS gp3_total_cost,
18001930
(current_gb_cost + current_iops_cost) - (gp3_gb_cost + gp3_iops_cost + gp3_throughput_cost) as gp3_saving
18011931
FROM inventory_ebs_data
1932+
1933+
EUCInventoryView:
1934+
Type: AWS::Athena::NamedQuery
1935+
Properties:
1936+
Database: !Ref DatabaseName
1937+
Description: EUC Inventory View for WorkSpaces inventory data
1938+
Name: create_euc_inventory_view
1939+
QueryString: !Sub |
1940+
CREATE OR REPLACE VIEW "euc_inventory_view" AS
1941+
SELECT
1942+
"workspaceid"
1943+
, "username"
1944+
, "accountid"
1945+
, "computetype"
1946+
, "directoryid"
1947+
, "directoryname"
1948+
, "directoryalias"
1949+
, "directorytype"
1950+
, "directorymaintenance"
1951+
, "region"
1952+
, "state"
1953+
, "connectionstatus"
1954+
, CAST(parse_datetime(lastconnected, 'MM/dd/yy HH:mm:ss') AS timestamp) lastconnected
1955+
, "lastinventoryrun"
1956+
, "runningmode"
1957+
, "operatingsystemname"
1958+
, "protocol"
1959+
, "computername"
1960+
, "ipaddress"
1961+
FROM
1962+
"inventory_workspaces_data"
18021963
18031964
AthenaBackwardCompatPricingRegionNames:
18041965
Type: AWS::Athena::NamedQuery

data-collection/deploy/module-pricing.yaml

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Parameters:
3535
Description: Arn of lambda for Analytics
3636
AwsServices:
3737
Type: CommaDelimitedList
38-
Default: AmazonRDS, AmazonEC2, AmazonElastiCache, AmazonES, AWSComputeSavingsPlan, AWSLambda, RegionalServices, RegionNames
38+
Default: AmazonRDS, AmazonEC2, AmazonElastiCache, AmazonES, AWSComputeSavingsPlan, AWSLambda, RegionalServices, RegionNames, AmazonWorkSpaces
3939
Description: Services for pulling price data
4040
CodeBucket:
4141
Type: String
@@ -378,6 +378,43 @@ Mappings:
378378
- { Type: string, Name: 'regionname' }
379379
jsonPaths: ['domain','geolocationCountry','geolocationRegion','longName','partition','region','regionname',"service"]
380380

381+
AmazonWorkSpaces:
382+
path: workspaces
383+
partition:
384+
- { Name: region, Type: string }
385+
fields:
386+
- { Type: string, Name: "SKU" }
387+
- { Type: string, Name: "OfferTermCode" }
388+
- { Type: string, Name: "RateCode" }
389+
- { Type: string, Name: "TermType" }
390+
- { Type: string, Name: "PriceDescription" }
391+
- { Type: string, Name: "EffectiveDate" }
392+
- { Type: string, Name: "StartingRange" }
393+
- { Type: string, Name: "EndingRange" }
394+
- { Type: string, Name: "Unit" }
395+
- { Type: string, Name: "PricePerUnit" }
396+
- { Type: string, Name: "Currency" }
397+
- { Type: string, Name: "Product Family" }
398+
- { Type: string, Name: "serviceCode" }
399+
- { Type: string, Name: "Location" }
400+
- { Type: string, Name: "Location Type" }
401+
- { Type: string, Name: "vCPU" }
402+
- { Type: string, Name: "Memory" }
403+
- { Type: string, Name: "Storage" }
404+
- { Type: string, Name: "Operating System" }
405+
- { Type: string, Name: "Group" }
406+
- { Type: string, Name: "Group Description" }
407+
- { Type: string, Name: "usageType" }
408+
- { Type: string, Name: "operation" }
409+
- { Type: string, Name: "Bundle" }
410+
- { Type: string, Name: "License" }
411+
- { Type: string, Name: "Region Code" }
412+
- { Type: string, Name: "Resource Type" }
413+
- { Type: string, Name: "Running Mode" }
414+
- { Type: string, Name: "serviceName" }
415+
- { Type: string, Name: "Software Included" }
416+
jsonPaths: ["SKU","OfferTermCode","RateCode","TermType","PriceDescription","EffectiveDate","StartingRange","EndingRange","Unit","PricePerUnit","Currency","Product Family","serviceCode","Location","Location Type","vCPU","Memory","Storage","Operating System","Group","Group Description","usageType","operation","Bundle","License","Region Code","Resource Type","Running Mode","serviceName","Software Included"]
417+
381418
Resources:
382419
LambdaRole:
383420
Type: AWS::IAM::Role

test/test_from_scratch.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ def test_ecs_chargeback_data(athena):
8181
data = athena_query(athena=athena, sql_query='SELECT * FROM "optimization_data"."ecs_chargeback_data" LIMIT 10;')
8282
assert len(data) > 0, 'ecs_chargeback_data is empty'
8383

84+
def test_inventory_workspaces_data(athena):
85+
data = athena_query(athena=athena, sql_query='SELECT * FROM "optimization_data"."inventory_workspaces_data" LIMIT 10;')
86+
assert len(data) > 0, 'inventory_workspaces_data is empty'
8487

8588
def test_inventory_ami_data(athena):
8689
data = athena_query(athena=athena, sql_query='SELECT * FROM "optimization_data"."inventory_ami_data" LIMIT 10;')

test/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ def trigger_update(account_id):
368368
f'arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}inventory-VpcInstances-StateMachine',
369369
f'arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}inventory-RdsDbSnapshots-StateMachine',
370370
f'arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}inventory-LambdaFunctions-StateMachine',
371+
f'arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}inventory-WorkSpaces-StateMachine',
371372
f'arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}rds-usage-StateMachine',
372373
f'arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}transit-gateway-StateMachine',
373374
f'arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}trusted-advisor-StateMachine',
@@ -384,6 +385,7 @@ def trigger_update(account_id):
384385
f"arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}pricing-AWSLambda-StateMachine",
385386
f"arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}pricing-RegionalServices-StateMachine",
386387
f"arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}pricing-RegionNames-StateMachine",
388+
f"arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}pricing-AmazonWorkSpaces-StateMachine",
387389
f"arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}backup-CopyJobs-StateMachine",
388390
f"arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}backup-RestoreJobs-StateMachine",
389391
f"arn:{partition}:states:{region}:{account_id}:stateMachine:{PREFIX}backup-BackupJobs-StateMachine",

0 commit comments

Comments
 (0)