ci: Generate MinIO credentials within LAVA jobs
[mesa.git] / .gitlab-ci / tracie / tracie.py
1 import argparse
2 import base64
3 import datetime
4 import enum
5 import glob
6 import hashlib
7 import hmac
8 import json
9 import os
10 import requests
11 import sys
12 import tempfile
13 import time
14 import yaml
15 import shutil
16 import xml.etree.ElementTree as ET
17
18 from email.utils import formatdate
19 from pathlib import Path
20 from PIL import Image
21 from urllib import parse
22
23 import dump_trace_images
24
25 TRACES_DB_PATH = "./traces-db/"
26 RESULTS_PATH = "./results/"
27 MINIO_HOST = "minio-packet.freedesktop.org"
28 DASHBOARD_URL = "https://tracie.freedesktop.org/dashboard"
29
30 minio_credentials = None
31
32 def replay(trace_path, device_name):
33 success = dump_trace_images.dump_from_trace(trace_path, [], device_name)
34
35 if not success:
36 print("[check_image] Trace %s couldn't be replayed. See above logs for more information." % (str(trace_path)))
37 return None, None, None
38 else:
39 base_path = trace_path.parent
40 file_name = trace_path.name
41 files = glob.glob(str(base_path / "test" / device_name / (file_name + "-*" + ".png")))
42 assert(files)
43 image_file = files[0]
44 files = glob.glob(str(base_path / "test" / device_name / (file_name + ".log")))
45 assert(files)
46 log_file = files[0]
47 return hashlib.md5(Image.open(image_file).tobytes()).hexdigest(), image_file, log_file
48
49 def gitlab_ensure_trace(project_url, trace):
50 trace_path = TRACES_DB_PATH + trace['path']
51 if project_url is None:
52 if not os.path.exists(trace_path):
53 print("{} missing".format(trace_path))
54 sys.exit(1)
55 return
56
57 os.makedirs(os.path.dirname(trace_path), exist_ok=True)
58
59 if os.path.exists(trace_path):
60 return
61
62 print("[check_image] Downloading trace %s" % (trace['path']), end=" ", flush=True)
63 download_time = time.time()
64 r = requests.get(project_url + trace['path'])
65 open(trace_path, "wb").write(r.content)
66 print("took %ds." % (time.time() - download_time), flush=True)
67
68 def sign_with_hmac(key, message):
69 key = key.encode("UTF-8")
70 message = message.encode("UTF-8")
71
72 signature = hmac.new(key, message, hashlib.sha1).digest()
73
74 return base64.encodebytes(signature).strip().decode()
75
76 def ensure_minio_credentials():
77 global minio_credentials
78
79 if minio_credentials is None:
80 minio_credentials = {}
81
82 params = {'Action': 'AssumeRoleWithWebIdentity',
83 'Version': '2011-06-15',
84 'RoleArn': 'arn:aws:iam::123456789012:role/FederatedWebIdentityRole',
85 'RoleSessionName': '%s:%s' % (os.environ['CI_PROJECT_PATH'], os.environ['CI_JOB_ID']),
86 'DurationSeconds': 900,
87 'WebIdentityToken': os.environ['CI_JOB_JWT']}
88 r = requests.post('https://%s' % (MINIO_HOST), params=params)
89 if r.status_code >= 400:
90 print(r.text)
91 r.raise_for_status()
92
93 root = ET.fromstring(r.text)
94 for attr in root.iter():
95 if attr.tag == '{https://sts.amazonaws.com/doc/2011-06-15/}AccessKeyId':
96 minio_credentials['AccessKeyId'] = attr.text
97 elif attr.tag == '{https://sts.amazonaws.com/doc/2011-06-15/}SecretAccessKey':
98 minio_credentials['SecretAccessKey'] = attr.text
99 elif attr.tag == '{https://sts.amazonaws.com/doc/2011-06-15/}SessionToken':
100 minio_credentials['SessionToken'] = attr.text
101
102 def upload_to_minio(file_name, resource, content_type):
103 ensure_minio_credentials()
104
105 minio_key = minio_credentials['AccessKeyId']
106 minio_secret = minio_credentials['SecretAccessKey']
107 minio_token = minio_credentials['SessionToken']
108
109 date = formatdate(timeval=None, localtime=False, usegmt=True)
110 url = 'https://%s%s' % (MINIO_HOST, resource)
111 to_sign = "PUT\n\n%s\n%s\nx-amz-security-token:%s\n%s" % (content_type, date, minio_token, resource)
112 signature = sign_with_hmac(minio_secret, to_sign)
113
114 with open(file_name, 'rb') as data:
115 headers = {'Host': MINIO_HOST,
116 'Date': date,
117 'Content-Type': content_type,
118 'Authorization': 'AWS %s:%s' % (minio_key, signature),
119 'x-amz-security-token': minio_token}
120 print("Uploading artifact to %s" % url);
121 r = requests.put(url, headers=headers, data=data)
122 if r.status_code >= 400:
123 print(r.text)
124 r.raise_for_status()
125
126 def upload_artifact(file_name, key, content_type):
127 resource = '/artifacts/%s/%s/%s/%s' % (os.environ['CI_PROJECT_PATH'],
128 os.environ['CI_PIPELINE_ID'],
129 os.environ['CI_JOB_ID'],
130 key)
131 upload_to_minio(file_name, resource, content_type)
132
133 def ensure_reference_image(file_name, checksum):
134 resource = '/mesa-tracie-results/%s/%s.png' % (os.environ['CI_PROJECT_PATH'], checksum)
135 url = 'https://%s%s' % (MINIO_HOST, resource)
136 r = requests.head(url, allow_redirects=True)
137 if r.status_code == 200:
138 return
139 upload_to_minio(file_name, resource, 'image/png')
140
141 def gitlab_check_trace(project_url, device_name, trace, expectation):
142 gitlab_ensure_trace(project_url, trace)
143
144 result = {}
145 result[trace['path']] = {}
146 result[trace['path']]['expected'] = expectation['checksum']
147
148 trace_path = Path(TRACES_DB_PATH + trace['path'])
149 checksum, image_file, log_file = replay(trace_path, device_name)
150 if checksum is None:
151 result[trace['path']]['actual'] = 'error'
152 return False, result
153 elif checksum == expectation['checksum']:
154 print("[check_image] Images match for %s" % (trace['path']))
155 ok = True
156 else:
157 print("[check_image] Images differ for %s (expected: %s, actual: %s)" %
158 (trace['path'], expectation['checksum'], checksum))
159 print("[check_image] For more information see "
160 "https://gitlab.freedesktop.org/mesa/mesa/blob/master/.gitlab-ci/tracie/README.md")
161 image_diff_url = "%s/imagediff/%s/%s/%s/%s/%s" % (DASHBOARD_URL,
162 os.environ['CI_PROJECT_PATH'],
163 os.environ['CI_PIPELINE_ID'],
164 os.environ['CI_JOB_ID'],
165 expectation['checksum'],
166 checksum)
167 print("[check_image] %s" % image_diff_url)
168 ok = False
169
170 trace_dir = os.path.split(trace['path'])[0]
171 dir_in_results = os.path.join(trace_dir, "test", device_name)
172 results_path = os.path.join(RESULTS_PATH, dir_in_results)
173 os.makedirs(results_path, exist_ok=True)
174 shutil.move(log_file, os.path.join(results_path, os.path.split(log_file)[1]))
175 if os.environ.get('TRACIE_UPLOAD_TO_MINIO', '0') == '1':
176 if ok:
177 if os.environ['CI_PROJECT_PATH'] == 'mesa/mesa':
178 ensure_reference_image(image_file, checksum)
179 else:
180 upload_artifact(image_file, 'traces/%s.png' % checksum, 'image/png')
181 if not ok or os.environ.get('TRACIE_STORE_IMAGES', '0') == '1':
182 image_name = os.path.split(image_file)[1]
183 shutil.move(image_file, os.path.join(results_path, image_name))
184 result[trace['path']]['image'] = os.path.join(dir_in_results, image_name)
185
186 result[trace['path']]['actual'] = checksum
187
188 return ok, result
189
190 def run(filename, device_name):
191
192 with open(filename, 'r') as f:
193 y = yaml.safe_load(f)
194
195 if "traces-db" in y:
196 project_url = y["traces-db"]["download-url"]
197 else:
198 project_url = None
199
200 traces = y['traces'] or []
201 all_ok = True
202 results = {}
203 for trace in traces:
204 for expectation in trace['expectations']:
205 if expectation['device'] == device_name:
206 ok, result = gitlab_check_trace(project_url,
207 device_name, trace,
208 expectation)
209 all_ok = all_ok and ok
210 results.update(result)
211
212 os.makedirs(RESULTS_PATH, exist_ok=True)
213 with open(os.path.join(RESULTS_PATH, 'results.yml'), 'w') as f:
214 yaml.safe_dump(results, f, default_flow_style=False)
215 if os.environ.get('TRACIE_UPLOAD_TO_MINIO', '0') == '1':
216 upload_artifact(os.path.join(RESULTS_PATH, 'results.yml'), 'traces/results.yml', 'text/yaml')
217
218 return all_ok
219
220 def main(args):
221 parser = argparse.ArgumentParser()
222 parser.add_argument('--file', required=True,
223 help='the name of the traces.yml file listing traces and their checksums for each device')
224 parser.add_argument('--device-name', required=True,
225 help="the name of the graphics device used to replay traces")
226
227 args = parser.parse_args(args)
228 return run(args.file, args.device_name)
229
230 if __name__ == "__main__":
231 all_ok = main(sys.argv[1:])
232 sys.exit(0 if all_ok else 1)