Setting the file. One moment.
Deprovision Cleanup · AWS Marketplace Metering · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page Query Patterns
70
Creating Amazon Aurora Db Cluster With Instances
93
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
scripts/deprovision_cleanup.py
scripts/ deprovision_cleanup.py
Python · 101 lines · 5 KB
* The flush window is absolute (~1h after the deprovision event). Once it has elapsed, no
15 region can meter the license anymore (BatchMeterUsage returns CustomerNotSubscribed), so a
16 single time-based finalize in us-east-1 is correct for all regions at once — no cross-region
17 coordination.
18
19 Idempotent: the finalize UpdateItem is conditioned on the marker still being present, so
20 concurrent/duplicate runs do not clobber a row that was already finalized or re-deprovisioned.
21 """
22
23 import logging
24 import os
25 from datetime import datetime, timezone
26
27 import boto3
28
29 logger = logging.getLogger()
30 logger.setLevel(logging. INFO )
31
32 dynamodb = boto3.resource( "dynamodb" )
33 subscribers_table = dynamodb.Table(os.environ[ "SUBSCRIBERS_TABLE" ])
34
35
36 def _mask (value):
37 """Mask a sensitive identifier for logging: keep only the last 4 chars. Buyer account IDs /
38 license ARNs are sensitive and MUST NOT be logged in full (mirrors register.py._mask)."""
39 if not value:
40 return "<none>"
41 s = str (value)
42 return "****" + s[ - 4 :] if len (s) > 4 else "****"
43
44 DEPROVISIONING_PENDING_INDEX = "deprovisioning-pending-index"
45 DEPROVISIONING_PENDING_VALUE = "1" # constant HASH of the sparse index
46
47
48 def handler (event, context):
49 """Finalize every deprovisioning license whose flush window has elapsed."""
50 now_iso = datetime.now(timezone.utc).strftime( "%Y-%m- %d T%H:%M:%SZ" )
51 finalized = 0
52 kwargs = {
53 "IndexName" : DEPROVISIONING_PENDING_INDEX ,
54 # flag = constant HASH, deprovisioningExpiry = RANGE: everything already expired.
55 "KeyConditionExpression" : (
56 "deprovisioningPendingFlag = :f AND deprovisioningExpiry <= :now"
57 ),
58 "ExpressionAttributeValues" : { ":f" : DEPROVISIONING_PENDING_VALUE , ":now" : now_iso},
59 }
60 while True :
61 response = subscribers_table.query( ** kwargs)
62 for item in response.get( "Items" , []):
63 finalized += _finalize(item)
64 if "LastEvaluatedKey" not in response:
65 break
66 kwargs[ "ExclusiveStartKey" ] = response[ "LastEvaluatedKey" ]
67
68 logger.info( f "Deprovision cleanup: finalized { finalized } expired license(s) as inactive" )
69 return { "finalized" : finalized}
70
71
72 def _finalize (item):
73 """Set subscriptionStatus=inactive and REMOVE both deprovisioning markers, idempotently."""
74 license_arn = item.get( "licenseArn" , "" )
75 account_id = item.get( "customerAWSAccountId" , "" )
76 if not license_arn or not account_id:
77 return 0
78 try :
79 from datetime import datetime, timezone
80
81 now = datetime.now(timezone.utc).strftime( "%Y-%m- %d T%H:%M:%SZ" )
82 subscribers_table.update_item(
83 Key = { "licenseArn" : license_arn, "customerAWSAccountId" : account_id},
84 UpdateExpression = (
85 # updatedAt goes in the SET clause (row already exists, so createdAt is kept);
86 # REMOVE follows the full SET clause per DynamoDB expression syntax.
87 "SET subscriptionStatus = :inactive, updatedAt = :now "
88 "REMOVE deprovisioningPendingFlag, deprovisioningExpiry"
89 ),
90 # Only finalize while the marker is still present — makes concurrent/duplicate
91 # runs (and a row re-deprovisioned in the meantime) safe.
92 ConditionExpression = "attribute_exists(deprovisioningPendingFlag)" ,
93 ExpressionAttributeValues = { ":inactive" : "inactive" , ":now" : now},
94 )
95 logger.info(
96 f "Finalized deprovisioning->inactive for licenseArn= { _mask(license_arn) } (window elapsed)"
97 )
98 return 1
99 except dynamodb.meta.client.exceptions.ConditionalCheckFailedException:
100 # Already finalized by a concurrent run (or re-deprovisioned) — nothing to do.
101 return 0