Related
I am using Cloudwatch subscriptions to send over cloudtrail log of one account into another. The Account receiving the logs has a Kinesis data stream which receives the logs from the cloudwatch subscription and invokes the standard lambda function provided by AWS to parse and store the logs to an S3 bucket of the log receiver account.
The log files getting written to s3 bucket are in the form of :
{"eventVersion":"1.08","userIdentity":{"type":"AssumedRole","principalId":"AA:i-096379450e69ed082","arn":"arn:aws:sts::34502sdsdsd:assumed-role/RDSAccessRole/i-096379450e69ed082","accountId":"34502sdsdsd","accessKeyId":"ASIAVAVKXAXXXXXXXC","sessionContext":{"sessionIssuer":{"type":"Role","principalId":"AROAVAVKXAKDDDDD","arn":"arn:aws:iam::3450291sdsdsd:role/RDSAccessRole","accountId":"345029asasas","userName":"RDSAccessRole"},"webIdFederationData":{},"attributes":{"mfaAuthenticated":"false","creationDate":"2021-04-27T04:38:52Z"},"ec2RoleDelivery":"2.0"}},"eventTime":"2021-04-27T07:24:20Z","eventSource":"ssm.amazonaws.com","eventName":"ListInstanceAssociations","awsRegion":"us-east-1","sourceIPAddress":"188.208.227.188","userAgent":"aws-sdk-go/1.25.41 (go1.13.15; linux; amd64) amazon-ssm-agent/","requestParameters":{"instanceId":"i-096379450e69ed082","maxResults":20},"responseElements":null,"requestID":"a5c63b9d-aaed-4a3c-9b7d-a4f7c6b774ab","eventID":"70de51df-c6df-4a57-8c1e-0ffdeb5ac29d","readOnly":true,"resources":[{"accountId":"34502914asasas","ARN":"arn:aws:ec2:us-east-1:3450291asasas:instance/i-096379450e69ed082"}],"eventType":"AwsApiCall","managementEvent":true,"eventCategory":"Management","recipientAccountId":"345029149342"}
{"eventVersion":"1.08","userIdentity":{"type":"AssumedRole","principalId":"AROAVAVKXAKPKZ25XXXX:AmazonMWAA-airflow","arn":"arn:aws:sts::3450291asasas:assumed-role/dev-1xdcfd/AmazonMWAA-airflow","accountId":"34502asasas","accessKeyId":"ASIAVAVKXAXXXXXXX","sessionContext":{"sessionIssuer":{"type":"Role","principalId":"AROAVAVKXAKPKZXXXXX","arn":"arn:aws:iam::345029asasas:role/service-role/AmazonMWAA-dlp-dev-1xdcfd","accountId":"3450291asasas","userName":"dlp-dev-1xdcfd"},"webIdFederationData":{},"attributes":{"mfaAuthenticated":"false","creationDate":"2021-04-27T07:04:08Z"}},"invokedBy":"airflow.amazonaws.com"},"eventTime":"2021-04-27T07:23:46Z","eventSource":"logs.amazonaws.com","eventName":"CreateLogStream","awsRegion":"us-east-1","sourceIPAddress":"airflow.amazonaws.com","userAgent":"airflow.amazonaws.com","errorCode":"ResourceAlreadyExistsException","errorMessage":"The specified log stream already exists","requestParameters":{"logStreamName":"scheduler.py.log","logGroupName":"dlp-dev-DAGProcessing"},"responseElements":null,"requestID":"40b48ef9-fc4b-4d1a-8fd1-4f2584aff1e9","eventID":"ef608d43-4765-4a3a-9c92-14ef35104697","readOnly":false,"eventType":"AwsApiCall","apiVersion":"20140328","managementEvent":true,"eventCategory":"Management","recipientAccountId":"3450291asasas"}
The problem with this type of log lines is that Athena is not able to Parse these log lines and I am not able to query the logs using Athena.
I tried modifying the blueprint lambda function to save the log file as a standard JSON result which would make it easy for Athena to parse the files.
Eg:
{'Records': ['{"eventVersion":"1.08","userIdentity":{"type":"AssumedRole","principalId":"AROAVAVKXAKPBRW2S3TAF:i-096379450e69ed082","arn":"arn:aws:sts::345029149342:assumed-role/RightslineRDSAccessRole/i-096379450e69ed082","accountId":"345029149342","accessKeyId":"ASIAVAVKXAKPBL653UOC","sessionContext":{"sessionIssuer":{"type":"Role","principalId":"AROAVAVKXAKPXXXXXXX","arn":"arn:aws:iam::34502asasas:role/RDSAccessRole","accountId":"345029asasas","userName":"RDSAccessRole"},"webIdFederationData":{},"attributes":{"mfaAuthenticated":"false","creationDate":"2021-04-27T04:38:52Z"},"ec2RoleDelivery":"2.0"}},"eventTime":"2021-04-27T07:24:20Z","eventSource":"ssm.amazonaws.com","eventName":"ListInstanceAssociations","awsRegion":"us-east-1","sourceIPAddress":"188.208.227.188","userAgent":"aws-sdk-go/1.25.41 (go1.13.15; linux; amd64) amazon-ssm-agent/","requestParameters":{"instanceId":"i-096379450e69ed082","maxResults":20},"responseElements":null,"requestID":"a5c63b9d-aaed-4a3c-9b7d-a4f7c6b774ab","eventID":"70de51df-c6df-4a57-8c1e-0ffdeb5ac29d","readOnly":true,"resources":[{"accountId":"3450291asasas","ARN":"arn:aws:ec2:us-east-1:34502asasas:instance/i-096379450e69ed082"}],"eventType":"AwsApiCall","managementEvent":true,"eventCategory":"Management","recipientAccountId":"345029asasas"}]}
The modified code for Blueprint Lambda function that I looks like:
import base64
import json
import gzip
from io import BytesIO
import boto3
def transformLogEvent(log_event):
return log_event['message'] + '\n'
def processRecords(records):
for r in records:
data = base64.b64decode(r['data'])
striodata = BytesIO(data)
with gzip.GzipFile(fileobj=striodata, mode='r') as f:
data = json.loads(f.read())
recId = r['recordId']
if data['messageType'] == 'CONTROL_MESSAGE':
yield {
'result': 'Dropped',
'recordId': recId
}
elif data['messageType'] == 'DATA_MESSAGE':
result = {}
result["Records"] = {}
events = []
for e in data['logEvents']:
events.append(e["message"])
result["Records"] = events
print(result)
if len(result) <= 6000000:
yield {
'data': result,
'result': 'Ok',
'recordId': recId
}
else:
yield {
'result': 'ProcessingFailed',
'recordId': recId
}
else:
yield {
'result': 'ProcessingFailed',
'recordId': recId
}
def putRecordsToFirehoseStream(streamName, records, client, attemptsMade, maxAttempts):
failedRecords = []
codes = []
errMsg = ''
# if put_record_batch throws for whatever reason, response['xx'] will error out, adding a check for a valid
# response will prevent this
response = None
try:
response = client.put_record_batch(DeliveryStreamName=streamName, Records=records)
except Exception as e:
failedRecords = records
errMsg = str(e)
# if there are no failedRecords (put_record_batch succeeded), iterate over the response to gather results
if not failedRecords and response and response['FailedPutCount'] > 0:
for idx, res in enumerate(response['RequestResponses']):
# (if the result does not have a key 'ErrorCode' OR if it does and is empty) => we do not need to re-ingest
if 'ErrorCode' not in res or not res['ErrorCode']:
continue
codes.append(res['ErrorCode'])
failedRecords.append(records[idx])
errMsg = 'Individual error codes: ' + ','.join(codes)
if len(failedRecords) > 0:
if attemptsMade + 1 < maxAttempts:
print('Some records failed while calling PutRecordBatch to Firehose stream, retrying. %s' % (errMsg))
putRecordsToFirehoseStream(streamName, failedRecords, client, attemptsMade + 1, maxAttempts)
else:
raise RuntimeError('Could not put records after %s attempts. %s' % (str(maxAttempts), errMsg))
def putRecordsToKinesisStream(streamName, records, client, attemptsMade, maxAttempts):
failedRecords = []
codes = []
errMsg = ''
# if put_records throws for whatever reason, response['xx'] will error out, adding a check for a valid
# response will prevent this
response = None
try:
response = client.put_records(StreamName=streamName, Records=records)
except Exception as e:
failedRecords = records
errMsg = str(e)
# if there are no failedRecords (put_record_batch succeeded), iterate over the response to gather results
if not failedRecords and response and response['FailedRecordCount'] > 0:
for idx, res in enumerate(response['Records']):
# (if the result does not have a key 'ErrorCode' OR if it does and is empty) => we do not need to re-ingest
if 'ErrorCode' not in res or not res['ErrorCode']:
continue
codes.append(res['ErrorCode'])
failedRecords.append(records[idx])
errMsg = 'Individual error codes: ' + ','.join(codes)
if len(failedRecords) > 0:
if attemptsMade + 1 < maxAttempts:
print('Some records failed while calling PutRecords to Kinesis stream, retrying. %s' % (errMsg))
putRecordsToKinesisStream(streamName, failedRecords, client, attemptsMade + 1, maxAttempts)
else:
raise RuntimeError('Could not put records after %s attempts. %s' % (str(maxAttempts), errMsg))
def createReingestionRecord(isSas, originalRecord):
if isSas:
return {'data': base64.b64decode(originalRecord['data']), 'partitionKey': originalRecord['kinesisRecordMetadata']['partitionKey']}
else:
return {'data': base64.b64decode(originalRecord['data'])}
def getReingestionRecord(isSas, reIngestionRecord):
if isSas:
return {'Data': reIngestionRecord['data'], 'PartitionKey': reIngestionRecord['partitionKey']}
else:
return {'Data': reIngestionRecord['data']}
def lambda_handler(event, context):
print(event)
isSas = 'sourceKinesisStreamArn' in event
streamARN = event['sourceKinesisStreamArn'] if isSas else event['deliveryStreamArn']
region = streamARN.split(':')[3]
streamName = streamARN.split('/')[1]
records = list(processRecords(event['records']))
projectedSize = 0
dataByRecordId = {rec['recordId']: createReingestionRecord(isSas, rec) for rec in event['records']}
putRecordBatches = []
recordsToReingest = []
totalRecordsToBeReingested = 0
for idx, rec in enumerate(records):
if rec['result'] != 'Ok':
continue
projectedSize += len(rec['data']) + len(rec['recordId'])
# 6000000 instead of 6291456 to leave ample headroom for the stuff we didn't account for
if projectedSize > 6000000:
totalRecordsToBeReingested += 1
recordsToReingest.append(
getReingestionRecord(isSas, dataByRecordId[rec['recordId']])
)
records[idx]['result'] = 'Dropped'
del(records[idx]['data'])
# split out the record batches into multiple groups, 500 records at max per group
if len(recordsToReingest) == 500:
putRecordBatches.append(recordsToReingest)
recordsToReingest = []
if len(recordsToReingest) > 0:
# add the last batch
putRecordBatches.append(recordsToReingest)
# iterate and call putRecordBatch for each group
recordsReingestedSoFar = 0
if len(putRecordBatches) > 0:
client = boto3.client('kinesis', region_name=region) if isSas else boto3.client('firehose', region_name=region)
for recordBatch in putRecordBatches:
if isSas:
putRecordsToKinesisStream(streamName, recordBatch, client, attemptsMade=0, maxAttempts=20)
else:
putRecordsToFirehoseStream(streamName, recordBatch, client, attemptsMade=0, maxAttempts=20)
recordsReingestedSoFar += len(recordBatch)
print('Reingested %d/%d records out of %d' % (recordsReingestedSoFar, totalRecordsToBeReingested, len(event['records'])))
else:
print('No records to be reingested')
return {"records": records}
My end goal is to store the result on S3 as JSON so that it can be queried easily with Athena.
the line where the transformation is happening is:
elif data['messageType'] == 'DATA_MESSAGE':
Any help in this would be greatly appreciated.
I'm trying to create a request to Amazon Selling Partner API following this guide.
The first part: Creating an access has already been taken care of here.
The documentation of the API for Orders can be found here.
I'm trying to invoke the GET /orders/v0/orders operation.
Connecting to the API
The only mandatory parameter for this operation is the MarketplaceIds based on the documentation.
In order to get the orders we need to sign our request. Here is my code so far:
function GetOrders(){
var access_token = AccessToken();
//Time variables
var currentDate = new Date();
var isoDate = currentDate.toISOString();
var yearMonthDay= Utilities.formatDate(currentDate, 'GTM-5', 'yyyyMMdd');
//API variables
var end_point = 'https://sellingpartnerapi-eu.amazon.com';
//Credential variables
var aws_region = "eu-west-1";
var service = "execute-api";
var termination_string = "aws4_request";
//CanonicalRequest = httpRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload));
//CanonicalRequest components:
var httpRequestMethod = 'GET';
var canonicalURI = '/orders/v0/orders';
var canonicalQueryString = '?marketplaceId=A1PA6795UKMFR9';
var canonicalheaders = 'host:' + canonicalURI + '\n' + 'x-amz-access-token:' + access_token + '\n' + 'x-amz-date:' + isoDate;
var signedheaders = 'host;user-agent;x-amz-access-token;x-amz-date';
var requestPayloadHashed = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, "");//NEW
requestPayloadHashed = requestPayloadHashed.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");//NEW
//Building the canonical request
var canonical_string = httpRequestMethod + '\n' + canonicalURI + '\n' + canonicalQueryString + '\n' + canonicalheaders + '\n' + signedheaders + '\n' + requestPayloadHashed;//UPDATED
var canonical_signature = Utilities.computeHmacSha256Signature(canonical_string, ACCESS_KEY);
var canonical_request = canonical_string + '\n' + canonical_signature;
canonical_request = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, canonical_request);//NEW
//CredentialScope = Date + AWS region + Service + Termination string;
//StringToSign = Algorithm + \n + RequestDateTime + \n + CredentialScope + \n + HashedCanonicalRequest;
var credential_scope = yearMonthDay + '/' + aws_region + '/' + service + '/' + termination_string;
var string_to_sign = "AWS4-HMAC-SHA256" + '\n' + isoDate + '\n' + credential_scope + '\n' + canonical_request;
var kSecret = ACCESS_KEY;
var kDate = Utilities.computeHmacSha256Signature(yearMonthDay, "AWS4" + kSecret);
var kRegion = Utilities.computeHmacSha256Signature(Utilities.newBlob(aws_region).getBytes(), kDate);
var kService = Utilities.computeHmacSha256Signature(Utilities.newBlob(service).getBytes(), kRegion);
var kSigning = Utilities.computeHmacSha256Signature(Utilities.newBlob(termination_string).getBytes(), kService);
kSigning = kSigning.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");
Logger.log('kSigning: ' + kSigning);
var signature = Utilities.computeHmacSha256Signature(kSigning, string_to_sign);
signature = signature.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");
var options = {
'method': 'GET',
'payload': {
'end_point': end_point,
'path': canonicalURI,
'query_string': canonicalQueryString
//Path parameter not needed
},
'headers': {
//'host': end_point,
'x-amz-access-token': access_token,
'x-amz-date': isoDate,
'user-agent': 'GAS Script 1.0 (Javascript)',
'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
},
}
var getOrders = UrlFetchApp.fetch(end_point, options);
Logger.log(getOrders);
}
PROBLEMS
When running the script I get the following error:
Exception: Request failed for https://sellingpartnerapi-eu.amazon.com returned code 403. Truncated server response: {
{
"errors": [
{
"message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.
The Canonical String for this request should have been
'POST
/
host:sellingpartnerapi-eu.amazon.com
user-agent:Mozilla/5.0 (compatible; Google-Apps-Script; beanserver; +https://script.google.com; id: UAEmdDd-KyWEWcR137UzUzWb1fu3rUgNviHA)
x-amz-access-token:Atza|IwEBSomeAccessToken
x-amz-date:2021-03-10T02:44:01.727Z
host;user-agent;x-amz-access-token;x-amz-date
cf22942946358a7530d8b72df6333e859644aaebb08a1cd825a6af65a8561111'
The String-to-Sign should have been
'AWS4-HMAC-SHA256
20210310T024401Z
20210310/eu-west-1/execute-api/aws4_request
c4c1dcea7026765f52c5265296f9e1cb91b6618928debbc04a393bac89ce8493'
",
"code": "InvalidSignature"
}
]
}
QUESTIONS
I have a big doubt about what is "Payload"
For this part of the code:
var canonical_string = httpRequestMethod + '\n' + canonicalURI + '\n' + canonicalQueryString + '\n' + canonicalheaders + '\n' + signedheaders + '\n' + requestPayloadHashed;
We have to incorporate a hashed version of the payload request requestPayloadHashed.
It also mentions:
If the payload is empty, use an empty string as the input to the hash
function.
For now I have just create that variable with a blank value
var requestPayloadHashed = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, "");//NEW
requestPayloadHashed = requestPayloadHashed.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");//NEW
But I'm not sure if I'm ommiting something important there.
UPDATE #1
After applying Tanaike recommendations I got the following message:
{
"errors": [
{
"message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.
The Canonical String for this request should have been
'GET
/orders/v0/orders
marketplaceId=A1PA6795UKMFR9
host:sellingpartnerapi-eu.amazon.com
user-agent:Mozilla/5.0 (compatible; Google-Apps-Script; beanserver; +https://script.google.com; id: UAEmdDd-KyWEWcR137UzUzWb1fu3rUgNviHA)
x-amz-access-token:Atza|IwEBISomeAccessToken
x-amz-date:2021-03-10T03:00:14.411Z
host;user-agent;x-amz-access-token;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
The String-to-Sign should have been
'AWS4-HMAC-SHA256
20210310T030014Z
20210310/eu-west-1/execute-api/aws4_request
f1bbc99190ca5a9e9e068ad6a0b2ef6a7aed4a1232095ef8f3d77ad62d0e66ac'
",
"code": "InvalidSignature"
}
]
}
UPDATE #2
There is this website that help us do some testing with these connections:
https://mws.amazonservices.de/scratchpad/index.html
By using it I believe I have validated the Access Key ID and Secret Key, however, it is asking for a SellerId which is new to me and is also not mentioned in the API docs.
I'm wondering where it could go.
UPDATE #3
I implemented most of Tanaike recommendations and also tried to align what I was sending to the API to the error message I was getting:
This is the last version of the script:
function GetOrders(){
var access_token = AccessToken();
//Time variables
var currentDate = new Date();
var isoDate = currentDate.toISOString();
var isoString = isoDate.replace(/-/g, "").replace(/:/g, "").replace(/(\.\d{3})/, "");
var yearMonthDay= Utilities.formatDate(currentDate, 'GTM-5', 'yyyyMMdd');
Logger.log('isoDate: ' + isoDate)
//API variables
var end_point = 'https://sellingpartnerapi-eu.amazon.com';
//Credential variables
var aws_region = "eu-west-1";
var service = "execute-api";
var termination_string = "aws4_request";
//CanonicalRequest = httpRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload));
//CanonicalRequest components:
var httpRequestMethod = 'GET';
var canonicalURI = '/orders/v0/orders';
var canonicalQueryString = '?marketplaceId=A1PA6795UKMFR9';
var canonicalheaders = 'host:' + "sellingpartnerapi-eu.amazon.com" + '\n' + 'x-amz-access-token:' + access_token + '\n' + 'x-amz-date:' + isoDate;
var signedheaders = 'host;x-amz-access-token;x-amz-date';//;user-agent
var requestPayloadHashed = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, "");//NEW
requestPayloadHashed = requestPayloadHashed.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");//NEW
//Building the canonical request
var canonical_string = httpRequestMethod + '\n' + canonicalURI + '\n' + "marketplaceId=A1PA6795UKMFR9" + '\n' + canonicalheaders + '\n\n' + signedheaders + '\n' + requestPayloadHashed;//UPDATED
Logger.log('canonical_string: ' + canonical_string)
var canonical_signature = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, canonical_string);
canonical_request = canonical_signature.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");
Logger.log("canonical_request: " + canonical_request)
//CredentialScope = Date + AWS region + Service + Termination string;
//StringToSign = Algorithm + \n + RequestDateTime + \n + CredentialScope + \n + HashedCanonicalRequest;
var credential_scope = yearMonthDay + '/' + aws_region + '/' + service + '/' + termination_string;
var string_to_sign = "AWS4-HMAC-SHA256" + '\n' + isoString + '\n' + credential_scope + '\n' + canonical_request;
Logger.log("string_to_sign: " + string_to_sign);
var kSecret = ACCESS_KEY;
var kDate = Utilities.computeHmacSha256Signature(yearMonthDay, "AWS4" + kSecret);
var kRegion = Utilities.computeHmacSha256Signature(toBytes(aws_region), kDate);
var kService = Utilities.computeHmacSha256Signature(toBytes(service), kRegion);
var kSigning = Utilities.computeHmacSha256Signature(toBytes(termination_string), kService);
Logger.log('kSigning: ' + kSigning);
var signature = hex(Utilities.computeHmacSha256Signature(toBytes(string_to_sign), kSigning));
Logger.log('signature: ' + signature)
var options = {
'method': 'GET',
'headers': {
//'host': end_point,
'x-amz-access-token': access_token,
'x-amz-date': isoDate,
//'user-agent': 'Mozilla/5.0 (compatible; Google-Apps-Script; beanserver; +https://script.google.com; id: UAEmdDd-KyWEWcR137UzUzWb1fu3rUgNviHA)',
'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
},
'muteHttpExceptions': true
}
var getOrders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options);
Logger.log(getOrders);
}
I'm now getting an error related entirely to my access:
{
"errors": [
{
"message": "Access to requested resource is denied.",
"code": "Unauthorized",
"details": ""
}
]
}
However this is probably due to the fact that when I registered the application (guide here) I used the IAM user instead of the IAM role.
And it says in the guide that:
Important. When registering your application, the IAM ARN that you
provide must be for the IAM entity to which you attached the IAM
policy from Step 3. Create an IAM policy. In this workflow, that IAM
entity is the IAM role from Step 4. Create an IAM role. If you
register your application using your IAM user, be sure that the IAM
policy is attached to it. Otherwise your calls to the Selling Partner
API will fail. We recommend registering your application using an IAM
role, as shown in this workflow, to help you better control access to
your AWS resources.
So I'm going to go ahead and fix that and see if I get the authorization I need.
Modification points:
In the case of UrlFetchApp, when payload is used, even when method is GET, it is requested as the POST request. It seems that this is the current specification.
user-agent cannot be changed for UrlFetchApp.
As a precondition, when your values for authorizing are correct values for requesting to the endpoint, your script can be modified by reflected above points as follows.
I thought that your error message might be due to the difference between the method of "GET" and "POST". At first, please test the following modification. When an error occurs, please show it.
Modified script:
From:
var options = {
'method': 'GET',
'payload': {
'end_point': end_point,
'path': canonicalURI,
'query_string': canonicalQueryString
//Path parameter not needed
},
'headers': {
//'host': end_point,
'x-amz-access-token': access_token,
'x-amz-date': isoDate,
'user-agent': 'GAS Script 1.0 (Javascript)',
'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
},
}
var getOrders = UrlFetchApp.fetch(end_point, options);
To:
var options = {
'method': 'GET',
'headers': {
'x-amz-access-token': access_token,
'x-amz-date': isoDate,
'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
},
}
var getOrders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options);
Reference:
fetch(url, params) of Class UrlFetchApp
Added:
From Signing AWS requests with Signature Version 4, I modified your script. When I saw your script, I noticed that the byte array is included in the string values. I thought that this might be also one of reasons of your issue. So I modified your script. Could you please confirm it? And when I saw the official document, I confirmed that when the byte array is used for Utilities.computeHmacSha256Signature, converting the string value to byte array is the same with the samples rather than converting the byte array to string value.
function sample() {
const hex = bytes => bytes.map(byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
const digestToHex = data => hex(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, data));
const toBytes = data => Utilities.newBlob(data).getBytes();
const ACCESS_ID = "MyAccessKey";
const ACCESS_KEY = "MyAccessSecret";
var access_token = "access_token"; // AccessToken();
//Time variables
var currentDate = new Date();
var isoDate = currentDate.toISOString();
var yearMonthDay= Utilities.formatDate(currentDate, 'GTM-5', 'yyyyMMdd');
//API variables
var end_point = 'https://sellingpartnerapi-eu.amazon.com';
//Credential variables
var aws_region = "eu-west-1";
var service = "execute-api";
var termination_string = "aws4_request";
// 1. Create string to sign.
var httpRequestMethod = 'GET';
var canonicalURI = '/orders/v0/orders';
var canonicalQueryString = '?marketplaceId=A1PA6795UKMFR9';
var canonicalheaders = 'host:' + canonicalURI + '\n' + 'x-amz-access-token:' + access_token + '\n' + 'x-amz-date:' + isoDate;
var signedheaders = 'host;user-agent;x-amz-access-token;x-amz-date';
const canonicalRequest = [httpRequestMethod,canonicalURI,canonicalQueryString,canonicalheaders + "\n",signedheaders,digestToHex("")].join("\n");
const canonical_request = digestToHex(canonicalRequest);
var credential_scope = yearMonthDay + '/' + aws_region + '/' + service + '/' + termination_string;
var string_to_sign = "AWS4-HMAC-SHA256" + '\n' + isoDate + '\n' + credential_scope + '\n' + canonical_request;
// 2. Create derived signing key.
var kSecret = ACCESS_KEY;
var kDate = Utilities.computeHmacSha256Signature(yearMonthDay, "AWS4" + kSecret);
var kRegion = Utilities.computeHmacSha256Signature(toBytes(aws_region), kDate);
var kService = Utilities.computeHmacSha256Signature(toBytes(service), kRegion);
var kSigning = Utilities.computeHmacSha256Signature(toBytes(termination_string), kService);
// 3. Create signature.
const signature = hex(Utilities.computeHmacSha256Signature(toBytes(string_to_sign), kSigning));
// 4. Request.
var options = {
'method': 'GET',
'headers': {
'x-amz-access-token': access_token,
'x-amz-date': isoDate,
'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
},
}
var getOrders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options);
Logger.log(getOrders);
}
Note:
Unfortunately, I cannot test above modified script. So when you tested it and an error occurs, please confirm your values for authorizating again. And please show the error message.
In this modification, it supposes that your values for authorizating are the correct values. Please be careful this.
About const canonicalRequest = [httpRequestMethod,canonicalURI,canonicalQueryString,canonicalheaders + "\n",signedheaders,digestToHex("")].join("\n");, when above script occurs an error, please test const canonicalRequest = [httpRequestMethod,canonicalURI,canonicalQueryString,canonicalheaders + "\n",""].join("\n");.
Reference:
Signing AWS requests with Signature Version 4
I looked at all the responses on post AWS S3 - How to fix 'The request signature we calculated does not match the signature' error? but I still have issues with my Python API sign 4 error. It still returns me with error "The request signature we calculated does not match with the signature you provided".
Here are the details:
While I am able to sign successfully: https://abcdef.mycompany.com/issues/P1230 but can't get https://abcdef.mycompany.com/issues?q=assignedFolder%3A(abc123) signed, and keep getting error
import json, hashlib, hmac, datetime, uuid, sys, urllib3, boto3, time, urllib.parse, os, requests
def get_extract(docid):
method = 'GET'
service = 'myservice'
host = 'abcdef.mycompany.com'
region = 'us-east-1'
endpoint = 'https://abcdef.mycompany.com'
request_parameters = '?q=assignedFolder%3A(abc123)' ## docid is abc123: hard-coding for example purpose
def sign(key, msg):
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
def getSignatureKey(key, dateStamp, regionName, serviceName):
kDate = sign(('AWS4' + key).encode('utf-8'), dateStamp)
kRegion = sign(kDate, regionName)
kService = sign(kRegion, serviceName)
kSigning = sign(kService, 'aws4_request')
return kSigning
access_key = 'mykey123'
secret_key = 'mykey123'
if access_key is None or secret_key is None:
print('No access key is available.')
sys.exit()
t = datetime.datetime.utcnow()
amzdate = t.strftime('%Y%m%dT%H%M%SZ')
datestamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope
canonical_uri = '/issues'
canonical_querystring = request_parameters
canonical_headers = 'host:' + host + '\n' + 'x-amz-date:' + amzdate + '\n'
signed_headers = 'host;x-amz-date'
payload_hash = hashlib.sha256(('').encode('utf-8')).hexdigest()
canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash
algorithm = 'AWS4-HMAC-SHA256'
credential_scope = datestamp + '/' + region + '/' + service + '/' + 'aws4_request'
string_to_sign = algorithm + '\n' + amzdate + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
signing_key = getSignatureKey(secret_key, datestamp, region, service)
signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest()
authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature
headers = {'x-amz-date':amzdate,'Host': host,'Authorization':authorization_header}
request_url = endpoint + canonical_uri + canonical_querystring
http = urllib3.PoolManager()
res = http.request('GET',request_url,headers=headers)
print(len(res.data.decode('utf-8')))
print(res.data.decode('utf-8'))
Error:
<InvalidSignatureException>
<Message>The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.</Message>
</InvalidSignatureException>
Need some help with this request, have been struggling to get this working for last 4-5 days. Any help is appreciated
How can I programmatically download the PDF monthly invoice the accounting department ask me every month?
I can get them from AWS Console (eg. https://console.aws.amazon.com/billing/home?region=eu-west-3#/bills?year=2019&month=3)
Where there is a link to the invoice.
The moment I click to download the invoice, I can see HTTP requests to the following URL:
https://console.aws.amazon.com/billing/rest/v1.0/bill/invoice/generate?generatenew=true&invoiceGroupId=_SOME_ID_&invoicenumber=_SOME_ID_
Then a final request to the URL that actually serves the PDF file:
https://console.aws.amazon.com/billing/rest/v1.0/bill/invoice/download?invoiceGroupId=_SOME_ID_&invoicenumber=_SOME_ID_
I cannot find documentation on the AWS API to fetch such invoice document (there is some for billing reports and other stuff, but none for the "official" document) so I start to ask myself if it is even available?
Before going scraping the AWS Console (via Scrapy, Selenium, Puppeteer) I ask the community.
NB: I know AWS can send the invoice PDF via e-mail but I rather fetch it directly from AWS instead of fetching from an IMAP/POP e-mail server.
You can use aws cli or aws sdk to get the data in json format. And then convert the json into pdf (not covered in this answer).
AWS cli
aws cli provides get-cost-and-usage command. By fiddling with parameters you can get the output that matches the one that is produced by billing invoice.
Example usage of this command:
aws ce get-cost-and-usage \
--time-period Start=2019-03-01,End=2019-04-01 \
--granularity MONTHLY \
--metrics "BlendedCost" "UnblendedCost" "UsageQuantity" \
--group-by Type=DIMENSION,Key=SERVICE
Which produces the following output
{
"GroupDefinitions": [
{
"Type": "DIMENSION",
"Key": "SERVICE"
}
],
"ResultsByTime": [
{
"TimePeriod": {
"Start": "2019-03-01",
"End": "2019-04-01"
},
"Total": {},
"Groups": [
{
"Keys": [
"AWS Budgets"
],
"Metrics": {
"BlendedCost": {
"Amount": "3.0392156805",
"Unit": "USD"
},
"UnblendedCost": {
"Amount": "3",
"Unit": "USD"
},
"UsageQuantity": {
"Amount": "155",
"Unit": "N/A"
}
}
},
{
"Keys": [
"AWS CloudTrail"
],
"Metrics": {
"BlendedCost": {
"Amount": "0",
"Unit": "USD"
},
"UnblendedCost": {
"Amount": "0",
"Unit": "USD"
},
"UsageQuantity": {
"Amount": "720042",
"Unit": "N/A"
}
}
},
...
AWS SDK
You can also get the same kind of data programmatically. The easiest way to do it is to use aws sdk. Refer to the documentation of the sdk you want to use. For example information on this functionality for python sdk can be found here.
Specific to invoices, it is unfortunate but still to this day there is no native way to download them other than manually downloading them or being a lucky one to get and have to deal with all of them via email https://aws.amazon.com/premiumsupport/knowledge-center/download-pdf-invoice/
There is https://github.com/iann0036/aws-bill-export (it does not use a native API but instead scrapes the webpage and is setup via lambda and nodejs) and also Puppeteer among other dependencies.
I just finished writing some Python + Selenium that is far more "monstrous" but gets the job done (for today's UI/Jan.2023 at least)...
I thought I'd share both of those since you mentioned them in the OP and no other solutions have come up.
import os
import sys
import time
import argparse
from os.path import expanduser
from datetime import datetime
from dateutil.relativedelta import relativedelta
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
home = expanduser("~")
# Variables grabbed from CLI arguments
parser = argparse.ArgumentParser(
description='AWS Console Login, programming the unprogrammatically-accessible (via CLI/API, using selenium instead).')
parser.add_argument(
'-i', '--interactive',
help="Use False for Headless mode",
default=False,
required=False
)
args = parser.parse_args()
# ChromeDriver options
options = webdriver.ChromeOptions()
if args.interactive == False:
options.add_argument('--headless')
download_directory = "./aws_invoice_downloads"
if not os.path.exists(download_directory):
os.makedirs(download_directory)
else:
download_directory = home + "/Downloads"
options.add_argument("--window-size=1920x1080")
options.add_argument("--remote-debugging-port=9222")
options.add_argument('--no-sandbox')
options.add_argument("--disable-gpu")
options.add_argument('--disable-dev-shm-usage')
options.add_experimental_option("prefs", {
"download.default_directory": download_directory,
"download.prompt_for_download": False
})
# Initiate ChromeDriver
driver = webdriver.Chrome(executable_path='chromedriver', options=options)
# create action chain object
action = ActionChains(driver)
# Set the default selenium timeout
delay = 30 # seconds
# Abort function
def abort_function():
print ("Aborting!")
driver.close()
sys.exit(1)
# Wait for download function
def download_wait(path_to_downloads):
seconds = 0
dl_wait = True
while dl_wait and seconds < 30:
time.sleep(1)
dl_wait = False
for fname in os.listdir(path_to_downloads):
if fname.endswith('.crdownload'):
dl_wait = True
seconds += 1
return seconds
def download_invoices(Id, Network):
print("Switching to the " + Network + "/" + Id + " org account...")
# remove_existing_conflicts(Network)
driver.get("https://signin.aws.amazon.com/switchrole?account=" + Id + "&roleName=YOUR_ROLE_NAME&displayName=" + Network + "%20Org%20Master")
time.sleep(1)
elem = WebDriverWait(driver, delay).until(
EC.presence_of_element_located((By.XPATH, '//*[#type="submit"]'))
)
elem.click()
time.sleep(3)
print("Downloading invoices...")
# Notes
# Can provide YYYY and MM in the URL to get a specific YYYY/MM billing period
# https://us-east-1.console.aws.amazon.com/billing/home?region=us-east-1#/bills?year=2023&month=1
# Get today's YYYY
today = datetime.now()
last_month = today - relativedelta(months=1)
year = last_month.strftime("%Y")
month = last_month.strftime("%m")
driver.get("https://us-east-1.console.aws.amazon.com/billing/home?region=us-east-1#/bills?year=" + year + "&month=" + month)
WebDriverWait(driver, 13).until(
EC.presence_of_element_located((By.XPATH, '//*[#data-testid="main-spinner"]'))
)
time.sleep(2)
elem = WebDriverWait(driver, 13).until(
EC.presence_of_all_elements_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1]'))
)
# Count the number of items in the list
elem_count = len(elem)
print("Found " + str(elem_count) + " items in the list...")
# Loop through the list and expand each item
for i in range(1, elem_count + 1):
print("Expanding item " + str(i) + " of " + str(elem_count) + "...")
# (//*[text()[contains(., " Charges")]])[position() < last() - 1][i]
elem = WebDriverWait(driver, 13).until(
EC.presence_of_element_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']'))
)
desired_y = (elem.size['height'] / 2) + elem.location['y']
current_y = (driver.execute_script('return window.innerHeight') / 2) + driver.execute_script('return window.pageYOffset')
scroll_y_by = desired_y - current_y
driver.execute_script("window.scrollBy(0, arguments[0]);", scroll_y_by)
time.sleep(2) # Fixes content shift and ElementClickInterceptedException by waiting, checking the elem, and scrolling again
elem = WebDriverWait(driver, delay).until(
EC.visibility_of_element_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']')))
driver.execute_script("arguments[0].scrollIntoView(true); window.scrollBy(0, -100);", elem)
action.move_to_element(elem).move_by_offset(0,0).click().perform()
# Count the number of invoices with that item
# (//*[text()[contains(., " Charges")]])[position() < last() - 1][2]/following-sibling::div//*[#title="Download Invoice"]
elem = WebDriverWait(driver, 13).until(
EC.presence_of_all_elements_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']/following-sibling::div//*[#title="Download Invoice"]'))
)
# Count the number of items in the list
invoice_count = len(elem)
# Loop through the list and download each invoice
for j in range(1, invoice_count + 1):
print("Downloading invoice " + str(j) + " of " + str(invoice_count) + "...")
# (//*[text()[contains(., " Charges")]])[position() < last() - 1][2]/following-sibling::div//*[#title="Download Invoice"][1]
elem = WebDriverWait(driver, 13).until(
EC.presence_of_element_located((By.XPATH, '((//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']/following-sibling::div//*[#title="Download Invoice"])[' + str(j) + ']'))
)
desired_y = (elem.size['height'] / 2) + elem.location['y']
current_y = (driver.execute_script('return window.innerHeight') / 2) + driver.execute_script('return window.pageYOffset')
scroll_y_by = desired_y - current_y
driver.execute_script("window.scrollBy(0, arguments[0]);", scroll_y_by)
time.sleep(2) # Fixes content shift and ElementClickInterceptedException by waiting, checking the elem, and scrolling again
elem = WebDriverWait(driver, delay).until(
EC.visibility_of_element_located((By.XPATH, '((//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']/following-sibling::div//*[#title="Download Invoice"])[' + str(j) + ']')))
driver.execute_script("arguments[0].scrollIntoView(true); window.scrollBy(0, -100);", elem)
action.move_to_element(elem).move_by_offset(0,0).click().perform()
download_wait(download_directory)
time.sleep(3)
# Find the parent again
elem = WebDriverWait(driver, 13).until(
EC.presence_of_element_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']'))
)
# Collapse the parent
desired_y = (elem.size['height'] / 2) + elem.location['y']
current_y = (driver.execute_script('return window.innerHeight') / 2) + driver.execute_script('return window.pageYOffset')
scroll_y_by = desired_y - current_y
driver.execute_script("window.scrollBy(0, arguments[0]);", scroll_y_by)
time.sleep(2) # Fixes content shift and ElementClickInterceptedException by waiting, checking the elem, and scrolling again
elem = WebDriverWait(driver, delay).until(
EC.visibility_of_element_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']')))
action.move_to_element(elem).move_by_offset(0,0).click().perform()
I've got a weird problem here. It hurts my brain thinking about it.
I've got a Django project with multiple apps. Today I added another app.
(views.py)
from %app_name%.%class_file% import %class_name%
def api(request):
t = %class_name%()
data = {}
data['listOtherDevices'] = t.listOtherDevices
logger = logging.getLogger(__name__)
logger.error(len(t.listOtherDevices))
return JsonResponse(data)
The imported class fills the 'listOtherDevices'-array via
__init__ perfectly fine when I run it inside a console. When I do so, there are exactly 3 elements inside this array (as there are 3 devices in my LAN the class could find). So when I visit the url (development server -> manage.py runserver) linked to this method I can see a JSON with exactly 3 entries. So far so good but now comes the weird part. When I refresh the url in my browser or visit it one more time, there are more than 3 entries. The scheme is like this:
opened url 1 time: 3 entries
opened url 2 times: 9 entries (+ 6)
opened url 3 times: 18 entries (+ 9)
opened url 4 times: 30 entries (+ 12)
opened url 5 times: 45 entries (+ 15)
opened url 6 times: 63 entries (+ 18)
I can see a pattern there but I cannot understand why this happends.
sudo sysdig -c spy_users
tells me, that the class is gathering information exactly 3 times using
subprocess.check_output
The responded JSON is syntactically OK. Seems like the class would 'find' 9, or 18 devices.
Please help me, because as I said earlier: this makes my brain hurt :)
import json
import subprocess
class tradfri:
tradfriIPv4 = 'blablabla'
tradfriUser = 'blablabla'
tradfriPassword = 'blablabla'
pathToCoap = '/blablabla/coap-client'
listDevices = []
listGroups = []
listDevicesDetails = []
listGroupsDetails = []
listLightbulbs = []
listOtherDevices = []
def __init__(self):
self.getDevices()
self.getGroups()
self.getArrayLightbulbs()
self.getArrayOtherDevices()
def getDevices(self):
method = 'get'
stdoutdata = subprocess.check_output( self.pathToCoap
+ ' -m ' + method
+ ' -u "' + self.tradfriUser + '"'
+ ' -k "' + self.tradfriPassword + '"'
+ ' coaps://' + self.tradfriIPv4 + ':5684/15001'
+ " | awk 'NR==4'",
shell=True).decode("utf-8")
self.listDevices = json.loads(stdoutdata)
for ID in self.listDevices:
stdoutdata = subprocess.check_output( self.pathToCoap
+ ' -m ' + method
+ ' -u "' + self.tradfriUser + '"'
+ ' -k "' + self.tradfriPassword + '"'
+ ' coaps://' + self.tradfriIPv4 + ':5684/15001/' + str(ID)
+ " | awk 'NR==4'",
shell=True).decode("utf-8")
self.listDevicesDetails.append(json.loads(stdoutdata))
def getGroups(self):
method = 'get'
stdoutdata = subprocess.check_output( self.pathToCoap
+ ' -m ' + method
+ ' -u "' + self.tradfriUser + '"'
+ ' -k "' + self.tradfriPassword + '"'
+ ' coaps://' + self.tradfriIPv4 + ':5684/15004'
+ " | awk 'NR==4'",
shell=True).decode("utf-8")
self.listGroups = json.loads(stdoutdata)
for ID in self.listGroups:
stdoutdata = subprocess.check_output( self.pathToCoap
+ ' -m ' + method
+ ' -u "' + self.tradfriUser + '"'
+ ' -k "' + self.tradfriPassword + '"'
+ ' coaps://' + self.tradfriIPv4 + ':5684/15004/' + str(ID)
+ " | awk 'NR==4'",
shell=True).decode("utf-8")
raw = json.loads(stdoutdata)
tmpMembers = []
for id in raw['9018']['15002']['9003']:
tmpMembers.append( { 'ID': str( id ), 'name': self.getDeviceNameByID(id) } )
self.listGroupsDetails.append( { 'ID': str( raw['9003'] ),
'name': raw['9001'],
'isGroupOn': False,
'members': tmpMembers } )
def getArrayLightbulbs(self):
for item in self.listDevicesDetails:
if item['3']['6'] == 1: # is lightbulb
id = item['9003']
name = item['9001']
groupID = self.getGroupIDByID(id)
groupName = self.getGroupNameByID(id)
manufacturer = item['3']['0']
description = item['3']['1']
isReachable = True
isBulbOn = False
isDimmable = False
isWhiteSpectrum = False
isColorSpectrum = False
brightnessOfBulb = ''
currentColor = ''
#isReachable
if len(item['3311'][0]) == 1:
isReachable = False;
else:
#isBulbOn
if item['3311'][0]['5850'] == 1:
isBulbOn = True
#dimmable & brightnessOfBulb
if '5851' in item['3311'][0]:
brightnessOfBulb = str( item['3311'][0]['5851'] )
isDimmable = True
#currentColor
if '5706' in item['3311'][0]:
currentColor = item['3311'][0]['5706']
#isWhiteSpectrum
if ' WS ' in description:
isWhiteSpectrum = True
#isColorSpectrum
if ' CWS ' in description:
isWhiteSpectrum = True
isColorSpectrum = True
self.listLightbulbs.append( { 'ID': str( id ),
'Name': name,
'groupID': str( groupID ),
'groupName' : groupName,
'manufacturer': manufacturer,
'description' : description,
'isReachable': isReachable,
'isBulbOn': isBulbOn,
'isDimmable': isDimmable,
'isWhiteSpectrum': isWhiteSpectrum,
'isColorSpectrum': isColorSpectrum,
'brightnessOfBulb': brightnessOfBulb,
'currentColor': currentColor } )
def getArrayOtherDevices(self):
for device in self.listDevicesDetails:
if device['3']['6'] == 3:
self.listOtherDevices.append( { 'ID': str( device['9003'] ),
'Name': device['9001'],
'groupID': str( self.getGroupIDByID(str( device['9003']) ) ),
'groupName': self.getGroupNameByID(str( device['9003']) ),
'manufacturer': device['3']['0'],
'description': device['3']['1'] } )
def getDeviceNameByID(self, id):
name = ''
for key in self.listDevicesDetails:
if key['9003'] == id:
name = key['9001']
return name
def getGroupIDByID(self, id):
groupID = ''
for group in self.listGroupsDetails:
for member in group['members']:
if member['ID'] == id:
groupID = group['ID']
return groupID
def getGroupNameByID(self, id):
groupName = ''
for group in self.listGroupsDetails:
for member in group['members']:
if member['ID'] == id:
groupName = group['name']
return groupName
You use class wide attributes to save the data.
Whenever you create a new instance of tradfri your methods work on the same class wide attribute listOtherDevices. Note that the class lives in memory until you restart the server. Therefore the amount of values increases with each request, as you append values to your list.
You should use attributes which are available per instance. In python this is achieved by initializing the attributes inside of __init__(). It might look like that:
class tradfri:
def __init__(self):
self.tradfriIPv4 = 'blablabla'
self.tradfriUser = 'blablabla'
self.tradfriPassword = 'blablabla'
self.pathToCoap = '/blablabla/coap-client'
self.listDevices = []
self.listGroups = []
self.listDevicesDetails = []
self.listGroupsDetails = []
self.listLightbulbs = []
self.listOtherDevices = []
self.getDevices()
self.getGroups()
self.getArrayLightbulbs()
self.getArrayOtherDevices()
Read python class attribute for more details. The official documentation on that topic are also worth reading.