Skip to content

Commit 8cd752f

Browse files
authored
Device Advisor CI automation (#291)
Description of changes: Add the device advisor scripts to enable GitHub Actions to automatically run device advisor test on push GitHub Setting Changes: Added Repository secrets: AWS_DATEST_ACCESS_KEY_ID, AWS_DATEST_SECRET_ACCESS_KEY The secrets are set to aws-sdk-common-runtime user: IotSDKDeviceAdvisorCIAutomation By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent ba5f3e8 commit 8cd752f

File tree

8 files changed

+310
-8
lines changed

8 files changed

+310
-8
lines changed

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- '*'
7+
- '!main'
8+
9+
env:
10+
BUILDER_VERSION: v0.8.28
11+
BUILDER_SOURCE: releases
12+
BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net
13+
PACKAGE_NAME: aws-iot-device-sdk-python-v2
14+
LINUX_BASE_IMAGE: ubuntu-16-x64
15+
RUN: ${{ github.run_id }}-${{ github.run_number }}
16+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_DATEST_ACCESS_KEY_ID }}
17+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_DATEST_SECRET_ACCESS_KEY }}
18+
AWS_DEFAULT_REGION: us-east-1
19+
20+
jobs:
21+
22+
al2:
23+
runs-on: ubuntu-latest
24+
steps:
25+
# We can't use the `uses: docker://image` version yet, GitHub lacks authentication for actions -> packages
26+
- name: Build ${{ env.PACKAGE_NAME }}
27+
run: |
28+
aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh
29+
./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-al2-x64 build -p ${{ env.PACKAGE_NAME }}
30+
31+
windows:
32+
runs-on: windows-latest
33+
steps:
34+
- name: Build ${{ env.PACKAGE_NAME }}
35+
run: |
36+
python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')"
37+
python builder.pyz build -p ${{ env.PACKAGE_NAME }}
38+
39+
osx:
40+
runs-on: macos-latest
41+
steps:
42+
- name: Build ${{ env.PACKAGE_NAME }}
43+
run: |
44+
python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')"
45+
chmod a+x builder
46+
./builder build -p ${{ env.PACKAGE_NAME }}
47+
48+

builder.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,18 @@
8484
["{python}", "-m", "pip", "install", ".", "--verbose"],
8585
["{python}", "-m", "pip", "install", "boto3", "autopep8"],
8686
["{python}", "-m", "unittest", "discover", "--verbose"]
87-
]
87+
],
88+
"build_steps": [
89+
"python3 -m pip install ."
90+
],
91+
"test_steps": [
92+
"python3 -m pip install boto3",
93+
"python3 deviceadvisor/script/DATestRun.py"
94+
],
95+
"env": {
96+
"DA_TOPIC": "test/da",
97+
"DA_SHADOW_PROPERTY": "datest",
98+
"DA_SHADOW_VALUE_SET": "ON",
99+
"DA_SHADOW_VALUE_DEFAULT": "OFF"
100+
}
88101
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"tests" :["MQTT Connect", "MQTT Publish", "MQTT Subscribe", "Shadow Publish", "Shadow Update"],
3+
"test_suite_ids" :
4+
{
5+
"MQTT Connect" : "ejbdzmo3hf3v",
6+
"MQTT Publish" : "euw7favf6an4",
7+
"MQTT Subscribe" : "01o8vo6no7sd",
8+
"Shadow Publish" : "elztm2jebc1q",
9+
"Shadow Update" : "vuydgrbbbfce"
10+
},
11+
"test_exe_path" :
12+
{
13+
"MQTT Connect" : "mqtt_connect.py",
14+
"MQTT Publish" : "mqtt_publish.py",
15+
"MQTT Subscribe" : "mqtt_subscribe.py",
16+
"Shadow Publish" : "shadow_update.py",
17+
"Shadow Update" : "shadow_update.py"
18+
}
19+
}

deviceadvisor/script/DATestRun.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import boto3
2+
import uuid
3+
import json
4+
import os
5+
import subprocess
6+
from time import sleep
7+
8+
##############################################
9+
# Cleanup Certificates and Things and created certificate and private key file
10+
def delete_thing_with_certi(thingName, certiId, certiArn):
11+
client.detach_thing_principal(
12+
thingName = thingName,
13+
principal = certiArn)
14+
client.update_certificate(
15+
certificateId =certiId,
16+
newStatus ='INACTIVE')
17+
client.delete_certificate(certificateId = certiId, forceDelete = True)
18+
client.delete_thing(thingName = thingName)
19+
os.remove(os.environ["DA_CERTI"])
20+
os.remove(os.environ["DA_KEY"])
21+
22+
23+
##############################################
24+
# Initialize variables
25+
# create aws clients
26+
client = boto3.client('iot')
27+
dataClient = boto3.client('iot-data')
28+
deviceAdvisor = boto3.client('iotdeviceadvisor')
29+
30+
# load test config
31+
f = open('deviceadvisor/script/DATestConfig.json')
32+
DATestConfig = json.load(f)
33+
f.close()
34+
35+
# create an temporary certificate/key file path
36+
certificate_path = os.path.join(os.getcwd(), 'certificate.pem.crt')
37+
key_path = os.path.join(os.getcwd(), 'private.pem.key')
38+
39+
# load environment variables requried for testing
40+
shadowProperty = os.environ['DA_SHADOW_PROPERTY']
41+
shadowDefault = os.environ['DA_SHADOW_VALUE_DEFAULT']
42+
43+
# test result
44+
test_result = {}
45+
46+
47+
##############################################
48+
# Run device advisor
49+
for test_name in DATestConfig['tests']:
50+
##############################################
51+
# create a test thing
52+
thing_name = "DATest_" + str(uuid.uuid4())
53+
try:
54+
# create_thing_response:
55+
# {
56+
# 'thingName': 'string',
57+
# 'thingArn': 'string',
58+
# 'thingId': 'string'
59+
# }
60+
print("[Device Advisor]Info: Started to create thing...")
61+
create_thing_response = client.create_thing(
62+
thingName=thing_name
63+
)
64+
os.environ["DA_THING_NAME"] = thing_name
65+
66+
except Exception as e:
67+
print("[Device Advisor]Error: Failed to create thing: " + thing_name)
68+
exit(-1)
69+
70+
71+
##############################################
72+
# create certificate and keys used for testing
73+
try:
74+
print("[Device Advisor]Info: Started to create certificate...")
75+
# create_cert_response:
76+
# {
77+
# 'certificateArn': 'string',
78+
# 'certificateId': 'string',
79+
# 'certificatePem': 'string',
80+
# 'keyPair':
81+
# {
82+
# 'PublicKey': 'string',
83+
# 'PrivateKey': 'string'
84+
# }
85+
# }
86+
create_cert_response = client.create_keys_and_certificate(
87+
setAsActive=True
88+
)
89+
# write certificate to file
90+
f = open(certificate_path, "w")
91+
f.write(create_cert_response['certificatePem'])
92+
f.close()
93+
94+
# write private key to file
95+
f = open(key_path, "w")
96+
f.write(create_cert_response['keyPair']['PrivateKey'])
97+
f.close()
98+
99+
# setup environment variable
100+
os.environ["DA_CERTI"] = certificate_path
101+
os.environ["DA_KEY"] = key_path
102+
103+
except:
104+
client.delete_thing(thingName = thing_name)
105+
print("[Device Advisor]Error: Failed to create certificate.")
106+
exit(-1)
107+
108+
##############################################
109+
# attach certification to thing
110+
try:
111+
print("[Device Advisor]Info: Attach certificate to test thing...")
112+
# attache the certificate to thing
113+
client.attach_thing_principal(
114+
thingName = thing_name,
115+
principal = create_cert_response['certificateArn']
116+
)
117+
118+
certificate_arn = create_cert_response['certificateArn']
119+
certificate_id = create_cert_response['certificateId']
120+
121+
except:
122+
delete_thing_with_certi(thing_name, certificate_id ,certificate_arn )
123+
print("[Device Advisor]Error: Failed to attach certificate.")
124+
exit(-1)
125+
126+
127+
try:
128+
######################################
129+
# set default shadow, for shadow update, if the
130+
# shadow does not exists, update will fail
131+
payload_shadow = json.dumps(
132+
{
133+
"state": {
134+
"desired": {
135+
shadowProperty: shadowDefault
136+
},
137+
"reported": {
138+
shadowProperty: shadowDefault
139+
}
140+
}
141+
})
142+
shadow_response = dataClient.update_thing_shadow(
143+
thingName = thing_name,
144+
payload = payload_shadow)
145+
get_shadow_response = dataClient.get_thing_shadow(thingName = thing_name)
146+
# make sure shadow is created before we go to next step
147+
while(get_shadow_response is None):
148+
get_shadow_response = dataClient.get_thing_shadow(thingName = thing_name)
149+
150+
# start device advisor test
151+
# test_start_response
152+
# {
153+
# 'suiteRunId': 'string',
154+
# 'suiteRunArn': 'string',
155+
# 'createdAt': datetime(2015, 1, 1)
156+
# }
157+
print("[Device Advisor]Info: Start device advisor test: " + test_name)
158+
test_start_response = deviceAdvisor.start_suite_run(
159+
suiteDefinitionId=DATestConfig['test_suite_ids'][test_name],
160+
suiteRunConfiguration={
161+
'primaryDevice': {
162+
'thingArn': create_thing_response['thingArn'],
163+
},
164+
'parallelRun': True
165+
})
166+
167+
# get DA endpoint
168+
endpoint_response = deviceAdvisor.get_endpoint(
169+
thingArn = create_thing_response['thingArn']
170+
)
171+
os.environ['DA_ENDPOINT'] = endpoint_response['endpoint']
172+
173+
while True:
174+
# sleep for 1s every loop to avoid TooManyRequestsException
175+
sleep(1)
176+
test_result_responds = deviceAdvisor.get_suite_run(
177+
suiteDefinitionId=DATestConfig['test_suite_ids'][test_name],
178+
suiteRunId=test_start_response['suiteRunId']
179+
)
180+
# If the status is PENDING or the responds does not loaded, the test suite is still loading
181+
if (test_result_responds['status'] == 'PENDING' or
182+
len(test_result_responds['testResult']['groups']) == 0 or # test group has not been loaded
183+
len(test_result_responds['testResult']['groups'][0]['tests']) == 0 or #test case has not been loaded
184+
test_result_responds['testResult']['groups'][0]['tests'][0]['status'] == 'PENDING'):
185+
continue
186+
187+
# Start to run the test sample after the status turns into RUNNING
188+
elif (test_result_responds['status'] == 'RUNNING' and
189+
test_result_responds['testResult']['groups'][0]['tests'][0]['status'] == 'RUNNING'):
190+
exe_path = os.path.join("deviceadvisor/tests/",DATestConfig['test_exe_path'][test_name])
191+
result = subprocess.run('python3 ' + exe_path, timeout = 60*2, shell = True)
192+
# If the test finalizing then store the test result
193+
elif (test_result_responds['status'] != 'RUNNING'):
194+
test_result[test_name] = test_result_responds['status']
195+
if(test_result[test_name] == "PASS"):
196+
delete_thing_with_certi(thing_name, certificate_id ,certificate_arn )
197+
break
198+
except Exception as e:
199+
print("[Device Advisor]Error: Failed to test: "+ test_name + e)
200+
exit(-1)
201+
202+
##############################################
203+
# print result and cleanup things
204+
print(test_result)
205+
failed = False
206+
for test in test_result:
207+
if(test_result[test] != "PASS" and
208+
test_result[test] != "PASS_WITH_WARNINGS"):
209+
print("[Device Advisor]Error: Test \"" + test + "\" Failed with status:" + test_result[test])
210+
failed = True
211+
if failed:
212+
# if the test failed, we dont clean the Thing so that we can track the error
213+
exit(-1)
214+
215+
exit(0)

deviceadvisor/tests/mqtt_connect.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
endpoint=DATestUtils.endpoint,
1717
cert_filepath=DATestUtils.certificatePath,
1818
pri_key_filepath=DATestUtils.keyPath,
19-
client_id = DATestUtils.client_id)
19+
client_id = DATestUtils.client_id,
20+
clean_session = True,
21+
ping_timeout_ms = 6000)
2022

2123
connect_future = mqtt_connection.connect()
2224

deviceadvisor/tests/mqtt_publish.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
endpoint=DATestUtils.endpoint,
1616
cert_filepath=DATestUtils.certificatePath,
1717
pri_key_filepath=DATestUtils.keyPath,
18-
client_id=DATestUtils.client_id)
18+
client_id=DATestUtils.client_id,
19+
clean_session = True,
20+
ping_timeout_ms = 6000)
1921
connect_future = mqtt_connection.connect()
2022

2123
# Future.result() waits until a result is available

deviceadvisor/tests/mqtt_subscribe.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
endpoint=DATestUtils.endpoint,
1616
cert_filepath=DATestUtils.certificatePath,
1717
pri_key_filepath=DATestUtils.keyPath,
18-
client_id = DATestUtils.client_id)
18+
client_id = DATestUtils.client_id,
19+
clean_session = True,
20+
ping_timeout_ms = 6000)
1921

2022
connect_future = mqtt_connection.connect()
2123

@@ -25,10 +27,9 @@
2527
# Subscribe
2628
subscribe_future, packet_id = mqtt_connection.subscribe(
2729
topic=DATestUtils.topic,
28-
qos=mqtt.QoS.AT_LEAST_ONCE)
29-
30+
qos=mqtt.QoS.AT_MOST_ONCE)
3031
subscribe_future.result()
31-
32+
3233
# Disconnect
3334
disconnect_future = mqtt_connection.disconnect()
3435
disconnect_future.result()

deviceadvisor/tests/shadow_update.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
endpoint=DATestUtils.endpoint,
1818
cert_filepath=DATestUtils.certificatePath,
1919
pri_key_filepath=DATestUtils.keyPath,
20-
client_id = DATestUtils.client_id)
20+
client_id = DATestUtils.client_id,
21+
clean_session = True,
22+
ping_timeout_ms = 6000)
2123

2224
connect_future = mqtt_connection.connect()
2325
connect_future.result()

0 commit comments

Comments
 (0)