Merge ~ubuntu-release/britney/+git/britney2-ubuntu:sil2100/private-runs into ~ubuntu-release/britney/+git/britney2-ubuntu:master
- Git
- lp:~ubuntu-release/britney/+git/britney2-ubuntu
- sil2100/private-runs
- Merge into master
Status: | Needs review |
---|---|
Proposed branch: | ~ubuntu-release/britney/+git/britney2-ubuntu:sil2100/private-runs |
Merge into: | ~ubuntu-release/britney/+git/britney2-ubuntu:master |
Diff against target: |
716 lines (+476/-43) (has conflicts) 8 files modified
britney.conf (+15/-0) britney.conf.template (+15/-0) britney2/policies/autopkgtest.py (+161/-41) tests/__init__.py (+12/-0) tests/mock_swift.py (+20/-1) tests/mock_swiftclient.py (+70/-0) tests/test_autopkgtest.py (+177/-1) tests/test_policy.py (+6/-0) Conflict in tests/__init__.py |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Steve Langasek | Needs Fixing | ||
Review via email: mp+399727@code.launchpad.net |
Commit message
Add support for running private tests and using private PPAs.
Description of the change
Add support for running private tests and using private PPAs.
The autopkgtest-cloud counterpart is here: https:/
Łukasz Zemczak (sil2100) wrote : | # |
Thank you for the review Steve! On it!
- 3b1699d... by Łukasz Zemczak
-
Fix some comments.
Łukasz Zemczak (sil2100) wrote : | # |
Ok, pushed some comment fixes and also commented on the exception handling part. Could you take a look again? Oh, and in previous inline comments I also addressed the question about private PPAs that are not embargoed.
Łukasz Zemczak (sil2100) wrote : | # |
Actually I almost forgot about one thing and I'm working on it right now: since the test results are private, the britney ADT report will have SWIFT urls to the results which will not works (as the container is private) - but we can have a small SSO protected wrapper to fetch the results for us in webcontrol.
- 1de71db... by Łukasz Zemczak
-
Add ability to share results with selected people.
- a21c904... by Łukasz Zemczak
-
Merge branch 'master' of git+ssh:
//git.launchpad .net/~ubuntu- release/ britney/ +git/britney2- ubuntu into sil2100/ private- runs - d1549b9... by Łukasz Zemczak
-
Commit the work regarding private-results handling.
- 7ff150c... by Łukasz Zemczak
-
Switch from swiftclient for private PPAs to using HTTP with X-Auth-Token instead.
This way there's less secrets that need to be shared and less new code to introduce. We also modified the test tooling to be able to check for authentication tokens in the queries.
- f79ec3b... by Łukasz Zemczak
-
Revert "Switch from swiftclient for private PPAs to using HTTP with X-Auth-Token instead."
This reverts commit 7ff150ced70d0db
565378683909e22 dfac5383e1. Sadly we can't use the X-Auth-Token approach due to implementational details (the token is valid only for an hour). So we need to switch back to using swiftclient.
- 41b606c... by Łukasz Zemczak
-
Commit fixes from staging testing: add required region name parameter.
- 090ccdf... by Łukasz Zemczak
-
Add an option to configure whether all ADT test results should be displayed or not.
- 738cac8... by Łukasz Zemczak
-
Add additional support for additional private test retry capabilities.
- 1848597... by Łukasz Zemczak
-
Support PPAs which have fingerprint enabled in them.
Unmerged commits
- 1848597... by Łukasz Zemczak
-
Support PPAs which have fingerprint enabled in them.
- 738cac8... by Łukasz Zemczak
-
Add additional support for additional private test retry capabilities.
- 090ccdf... by Łukasz Zemczak
-
Add an option to configure whether all ADT test results should be displayed or not.
- 41b606c... by Łukasz Zemczak
-
Commit fixes from staging testing: add required region name parameter.
- f79ec3b... by Łukasz Zemczak
-
Revert "Switch from swiftclient for private PPAs to using HTTP with X-Auth-Token instead."
This reverts commit 7ff150ced70d0db
565378683909e22 dfac5383e1. Sadly we can't use the X-Auth-Token approach due to implementational details (the token is valid only for an hour). So we need to switch back to using swiftclient.
- 7ff150c... by Łukasz Zemczak
-
Switch from swiftclient for private PPAs to using HTTP with X-Auth-Token instead.
This way there's less secrets that need to be shared and less new code to introduce. We also modified the test tooling to be able to check for authentication tokens in the queries.
- d1549b9... by Łukasz Zemczak
-
Commit the work regarding private-results handling.
- a21c904... by Łukasz Zemczak
-
Merge branch 'master' of git+ssh:
//git.launchpad .net/~ubuntu- release/ britney/ +git/britney2- ubuntu into sil2100/ private- runs - 1de71db... by Łukasz Zemczak
-
Add ability to share results with selected people.
- 3b1699d... by Łukasz Zemczak
-
Fix some comments.
Preview Diff
1 | diff --git a/britney.conf b/britney.conf |
2 | index 52f5b4c..00ffe30 100644 |
3 | --- a/britney.conf |
4 | +++ b/britney.conf |
5 | @@ -101,11 +101,26 @@ ADT_SHARED_RESULTS_CACHE = |
6 | # Swift base URL with the results (must be publicly readable and browsable) |
7 | # or file location if results are pre-fetched |
8 | ADT_SWIFT_URL = https://autopkgtest.ubuntu.com/results/ |
9 | +# Swift identity with read access to the private result container |
10 | +# (this is required whenever a private PPA is used for testing) |
11 | +ADT_SWIFT_USER = |
12 | +ADT_SWIFT_PASS = |
13 | +ADT_SWIFT_AUTH_URL = |
14 | +ADT_SWIFT_TENANT = |
15 | +ADT_SWIFT_REGION = |
16 | +# List of launchpad users/teams that should have read access to any private |
17 | +# result logs |
18 | +ADT_PRIVATE_SHARED = |
19 | +ADT_PRIVATE_URL = https://autopkgtest.ubuntu.com/private-results/ |
20 | +ADT_PRIVATE_RETRY = |
21 | # Base URL for autopkgtest site, used for links in the excuses |
22 | ADT_CI_URL = https://autopkgtest.ubuntu.com/ |
23 | # URL for the autopkgtest database, if used |
24 | ADT_DB_URL = https://autopkgtest.ubuntu.com/static/autopkgtest.db |
25 | ADT_HUGE = 20 |
26 | +# Change to 'yes' for excuses to include all ADT results, even those requiring |
27 | +# no action (like passing or always-failed) |
28 | +ADT_SHOW_IRRELEVANT = no |
29 | |
30 | # Autopkgtest results can be used to influence the aging |
31 | ADT_REGRESSION_PENALTY = |
32 | diff --git a/britney.conf.template b/britney.conf.template |
33 | index b1df068..f662aa4 100644 |
34 | --- a/britney.conf.template |
35 | +++ b/britney.conf.template |
36 | @@ -124,11 +124,26 @@ ADT_SHARED_RESULTS_CACHE = |
37 | # or file location if results are pre-fetched |
38 | #ADT_SWIFT_URL = https://example.com/some/url |
39 | ADT_SWIFT_URL = file:///path/to/britney/state/debci.json |
40 | +# Swift identity with read access to the private result container |
41 | +# (this is required whenever a private PPA is used for testing) |
42 | +ADT_SWIFT_USER = |
43 | +ADT_SWIFT_PASS = |
44 | +ADT_SWIFT_AUTH_URL = |
45 | +ADT_SWIFT_TENANT = |
46 | +ADT_SWIFT_REGION = |
47 | +# List of launchpad users/teams that should have read access to any private |
48 | +# result logs |
49 | +ADT_PRIVATE_SHARED = |
50 | +ADT_PRIVATE_URL = |
51 | +ADT_PRIVATE_RETRY = |
52 | # Base URL for autopkgtest site, used for links in the excuses |
53 | ADT_CI_URL = https://example.com/ |
54 | # Enable the huge queue for packages that trigger vast amounts of tests to not |
55 | # starve the regular queue |
56 | #ADT_HUGE = 20 |
57 | +# Change to 'yes' for excuses to include all ADT results, even those requiring |
58 | +# no action (like passing or always-failed) |
59 | +ADT_SHOW_IRRELEVANT = no |
60 | |
61 | # Autopkgtest results can be used to influence the aging, leave |
62 | # ADT_REGRESSION_PENALTY empty to have regressions block migration |
63 | diff --git a/britney2/policies/autopkgtest.py b/britney2/policies/autopkgtest.py |
64 | index 7ff8c1f..d007ba0 100644 |
65 | --- a/britney2/policies/autopkgtest.py |
66 | +++ b/britney2/policies/autopkgtest.py |
67 | @@ -150,12 +150,26 @@ class AutopkgtestPolicy(BasePolicy): |
68 | |
69 | try: |
70 | self.options.adt_ppas = self.options.adt_ppas.strip().split() |
71 | + # We also allow, for certain other use-cases, passing the PPA |
72 | + # fingerprint to for each of the PPAs. This however is not |
73 | + # currently used by the ADT policy, so get rid of it. |
74 | + for i, ppa in enumerate(self.options.adt_ppas): |
75 | + if '@' not in ppa and ':' in ppa: |
76 | + self.options.adt_ppas[i] = ppa.split(':')[0] |
77 | except AttributeError: |
78 | self.options.adt_ppas = [] |
79 | |
80 | + try: |
81 | + self.options.adt_private_shared = self.options.adt_private_shared.strip().split() |
82 | + except AttributeError: |
83 | + self.options.adt_private_shared = [] |
84 | + |
85 | self.swift_container = 'autopkgtest-' + options.series |
86 | if self.options.adt_ppas: |
87 | - self.swift_container += '-' + options.adt_ppas[-1].replace('/', '-') |
88 | + # private PPAs require the auth credentials given + we allow the |
89 | + # PPA fingerprint attached at the end |
90 | + # those need to be removed before the ppa-based name can be used |
91 | + self.swift_container += '-' + options.adt_ppas[-1].rpartition('@')[2].replace('/', '-').partition(':')[0] |
92 | |
93 | # restrict adt_arches to architectures we actually run for |
94 | self.adt_arches = [] |
95 | @@ -233,6 +247,48 @@ class AutopkgtestPolicy(BasePolicy): |
96 | else: |
97 | self.logger.info('%s does not exist, re-downloading all results from swift', self.results_cache_file) |
98 | |
99 | + # log into swift in case we need to fetch some private results |
100 | + # this is optional - if there are no credentials present, results will |
101 | + # be fetched without authentication |
102 | + if self.options.adt_swift_user: |
103 | + if (not self.options.adt_swift_pass or |
104 | + not self.options.adt_swift_auth_url or |
105 | + not self.options.adt_swift_tenant or |
106 | + not self.options.adt_swift_region): |
107 | + raise RuntimeError('Incomplete swift credentials given') |
108 | + |
109 | + # once swift credentials are given, the results will be published |
110 | + # to a private container |
111 | + self.swift_container = 'private-' + self.swift_container |
112 | + |
113 | + # check if all private PPAs have a fingerprint provided |
114 | + # private PPAs need to follow the following pattern: |
115 | + # user:token@team/name:fingerprint |
116 | + for ppa in self.options.adt_ppas: |
117 | + # TODO: write a test for this |
118 | + if '@' in ppa and not re.match(r'^.+:.+@.+:.+$', ppa): |
119 | + raise RuntimeError('Private PPA %s not following required format (user:token@team/name:fingerprint)', ppa) |
120 | + |
121 | + import swiftclient |
122 | + |
123 | + if '/v2.0' not in self.options.adt_swift_auth_url: |
124 | + raise RuntimeError('Unsupported swift auth version') |
125 | + |
126 | + self.logger.info('Creating an authenticated swift connection for user %s', self.options.adt_swift_user) |
127 | + self.swift_conn = swiftclient.Connection( |
128 | + authurl=self.options.adt_swift_auth_url, |
129 | + user=self.options.adt_swift_user, |
130 | + key=self.options.adt_swift_pass, |
131 | + tenant_name=self.options.adt_swift_tenant, |
132 | + auth_version='2.0', |
133 | + os_options={'region_name': self.options.adt_swift_region} |
134 | + ) |
135 | + else: |
136 | + if any('@' in ppa for ppa in self.options.adt_ppas): |
137 | + raise RuntimeError('Private PPA configured but no swift credentials given') |
138 | + |
139 | + self.swift_conn = None |
140 | + |
141 | # read in the new results |
142 | if self.options.adt_swift_url.startswith('file://'): |
143 | debci_file = self.options.adt_swift_url[7:] |
144 | @@ -478,7 +534,22 @@ class AutopkgtestPolicy(BasePolicy): |
145 | if status in ['REGRESSION', 'RUNNING-REFERENCE']: |
146 | if self.options.adt_retry_url_mech == 'run_id': |
147 | retry_url = self.options.adt_ci_url + 'api/v1/retry/' + run_id |
148 | - else: |
149 | + elif self.options.adt_private_retry: |
150 | + # if a custom retry url mechanism has been given, |
151 | + # use that - but instead of passing PPAs with |
152 | + # sensitive credentials, we pass additional context |
153 | + # that will help the backend identify what env |
154 | + # needs to be used |
155 | + retry_url = self.options.adt_private_retry + \ |
156 | + urllib.parse.urlencode([('release', self.options.series), |
157 | + ('arch', arch), |
158 | + ('package', testsrc), |
159 | + ('trigger', trigger), |
160 | + ('context', self.swift_container)]) |
161 | + elif not any('@' in ppa for ppa in self.options.adt_ppas): |
162 | + # otherwise private PPAs currently should not |
163 | + # display a retry button as we can not guarantee |
164 | + # that the secrets will not leak |
165 | retry_url = self.options.adt_ci_url + 'request.cgi?' + \ |
166 | urllib.parse.urlencode([('release', self.options.series), |
167 | ('arch', arch), |
168 | @@ -502,8 +573,10 @@ class AutopkgtestPolicy(BasePolicy): |
169 | html_archmsg.append(message) |
170 | |
171 | # render HTML line for testsrc entry, but only when action is |
172 | - # or may be required |
173 | - if r - {'PASS', 'NEUTRAL', 'RUNNING-ALWAYSFAIL', 'ALWAYSFAIL', 'IGNORE-FAIL'}: |
174 | + # or may be required or when britney is configured to print |
175 | + # everything |
176 | + if (self.options.adt_show_irrelevant or |
177 | + r - {'PASS', 'NEUTRAL', 'RUNNING-ALWAYSFAIL', 'ALWAYSFAIL', 'IGNORE-FAIL'}): |
178 | results_info.append("autopkgtest for %s: %s" % (testname, ', '.join(html_archmsg))) |
179 | |
180 | if verdict != PolicyVerdict.PASS: |
181 | @@ -891,55 +964,93 @@ class AutopkgtestPolicy(BasePolicy): |
182 | query['marker'] = query['prefix'] + latest_run_id |
183 | |
184 | # request new results from swift |
185 | - url = os.path.join(swift_url, self.swift_container) |
186 | - url += '?' + urllib.parse.urlencode(query) |
187 | - f = None |
188 | - try: |
189 | - f = self.download_retry(url) |
190 | - if f.getcode() == 200: |
191 | - result_paths = f.read().decode().strip().splitlines() |
192 | - elif f.getcode() == 204: # No content |
193 | - result_paths = [] |
194 | - else: |
195 | - # we should not ever end up here as we expect a HTTPError in |
196 | - # other cases; e. g. 3XX is something that tells us to adjust |
197 | - # our URLS, so fail hard on those |
198 | - raise NotImplementedError('fetch_swift_results(%s): cannot handle HTTP code %i' % |
199 | - (url, f.getcode())) |
200 | - except IOError as e: |
201 | - # 401 "Unauthorized" is swift's way of saying "container does not exist" |
202 | - if hasattr(e, 'code') and e.code == 401: |
203 | - self.logger.info('fetch_swift_results: %s does not exist yet or is inaccessible', url) |
204 | - return |
205 | - # Other status codes are usually a transient |
206 | - # network/infrastructure failure. Ignoring this can lead to |
207 | - # re-requesting tests which we already have results for, so |
208 | - # fail hard on this and let the next run retry. |
209 | - self.logger.error('Failure to fetch swift results from %s: %s', url, str(e)) |
210 | - sys.exit(1) |
211 | - finally: |
212 | - if f is not None: |
213 | - f.close() |
214 | + if self.swift_conn: |
215 | + # when we have an authenticated swift connection, use that to |
216 | + # fetch the result_path list as we might be fetching from an |
217 | + # otherwise unaccessible container |
218 | + from swiftclient.exceptions import ClientException |
219 | + |
220 | + try: |
221 | + _, returned_paths = self.swift_conn.get_container( |
222 | + self.swift_container, |
223 | + query_string=urllib.parse.urlencode(query)) |
224 | + except ClientException as e: |
225 | + # 401 "Unauthorized" is swift's way of saying "container does not exist" |
226 | + if e.http_status == 401 or e.http_status == 404: |
227 | + self.logger.info('fetch_swift_results: %s does not exist yet or is inaccessible', self.swift_container) |
228 | + return |
229 | + # Other status codes are usually a transient |
230 | + # network/infrastructure failure. Ignoring this can lead to |
231 | + # re-requesting tests which we already have results for, so |
232 | + # fail hard on this and let the next run retry. |
233 | + self.logger.error('Failure to fetch swift results from %s: %s', self.swift_container, str(e)) |
234 | + sys.exit(1) |
235 | + result_paths = [p['subdir'] for p in returned_paths] |
236 | + else: |
237 | + url = os.path.join(swift_url, self.swift_container) |
238 | + url += '?' + urllib.parse.urlencode(query) |
239 | + f = None |
240 | + try: |
241 | + f = self.download_retry(url) |
242 | + if f.getcode() == 200: |
243 | + result_paths = f.read().decode().strip().splitlines() |
244 | + elif f.getcode() == 204: # No content |
245 | + result_paths = [] |
246 | + else: |
247 | + # we should not ever end up here as we expect a HTTPError in |
248 | + # other cases; e. g. 3XX is something that tells us to adjust |
249 | + # our URLS, so fail hard on those |
250 | + raise NotImplementedError('fetch_swift_results(%s): cannot handle HTTP code %i' % |
251 | + (url, f.getcode())) |
252 | + except IOError as e: |
253 | + # 401 "Unauthorized" is swift's way of saying "container does not exist" |
254 | + if hasattr(e, 'code') and e.code == 401: |
255 | + self.logger.info('fetch_swift_results: %s does not exist yet or is inaccessible', url) |
256 | + return |
257 | + # same as above in the swift authenticated case |
258 | + self.logger.error('Failure to fetch swift results from %s: %s', url, str(e)) |
259 | + sys.exit(1) |
260 | + finally: |
261 | + if f is not None: |
262 | + f.close() |
263 | |
264 | for p in result_paths: |
265 | self.fetch_one_result( |
266 | - os.path.join(swift_url, self.swift_container, p, 'result.tar'), src, arch) |
267 | + swift_url, self.swift_container, p, 'result.tar', src, arch) |
268 | |
269 | fetch_swift_results._done = set() |
270 | |
271 | - def fetch_one_result(self, url, src, arch): |
272 | + def fetch_one_result(self, swift_url, container, path, name, src, arch): |
273 | '''Download one result URL for source/arch |
274 | |
275 | Remove matching pending_tests entries. |
276 | ''' |
277 | + |
278 | f = None |
279 | try: |
280 | - f = self.download_retry(url) |
281 | - if f.getcode() == 200: |
282 | - tar_bytes = io.BytesIO(f.read()) |
283 | + if self.swift_conn: |
284 | + from swiftclient.exceptions import ClientException |
285 | + |
286 | + # We don't need any additional retry logic as swiftclient |
287 | + # already performs retries (5 by default). |
288 | + url = os.path.join(path, name) |
289 | + try: |
290 | + _, contents = self.swift_conn.get_object(container, url) |
291 | + except ClientException as e: |
292 | + self.logger.error('Failure to fetch %s from container %s: %s', |
293 | + url, container, str(e)) |
294 | + if e.http_status == 404: |
295 | + return |
296 | + sys.exit(1) |
297 | + tar_bytes = io.BytesIO(contents) |
298 | else: |
299 | - raise NotImplementedError('fetch_one_result(%s): cannot handle HTTP code %i' % |
300 | - (url, f.getcode())) |
301 | + url = os.path.join(swift_url, container, path, name) |
302 | + f = self.download_retry(url) |
303 | + if f.getcode() == 200: |
304 | + tar_bytes = io.BytesIO(f.read()) |
305 | + else: |
306 | + raise NotImplementedError('fetch_one_result(%s): cannot handle HTTP code %i' % |
307 | + (url, f.getcode())) |
308 | except IOError as e: |
309 | self.logger.error('Failure to fetch %s: %s', url, str(e)) |
310 | # we tolerate "not found" (something went wrong on uploading the |
311 | @@ -1122,6 +1233,8 @@ class AutopkgtestPolicy(BasePolicy): |
312 | |
313 | params = {'triggers': triggers} |
314 | if self.options.adt_ppas: |
315 | + # Note: the PPA might be a private PPA, and then the PPA parameter |
316 | + # includes the authorization token. |
317 | params['ppas'] = self.options.adt_ppas |
318 | qname = 'debci-ppa-%s-%s' % (self.options.series, arch) |
319 | elif huge: |
320 | @@ -1130,6 +1243,11 @@ class AutopkgtestPolicy(BasePolicy): |
321 | qname = 'debci-%s-%s' % (self.options.series, arch) |
322 | params['submit-time'] = datetime.strftime(datetime.utcnow(), '%Y-%m-%d %H:%M:%S%z') |
323 | |
324 | + if self.swift_conn: |
325 | + params['swiftuser'] = self.options.adt_swift_user |
326 | + if self.options.adt_private_shared: |
327 | + params['readable-by'] = self.options.adt_private_shared |
328 | + |
329 | if self.amqp_channel: |
330 | import amqplib.client_0_8 as amqp |
331 | params = json.dumps(params) |
332 | @@ -1358,7 +1476,9 @@ class AutopkgtestPolicy(BasePolicy): |
333 | run_id, |
334 | 'log.gz') |
335 | else: |
336 | - url = os.path.join(self.options.adt_swift_url, |
337 | + # Private runs have a different base url |
338 | + results_url = self.options.adt_private_url if self.swift_conn else self.options.adt_swift_url |
339 | + url = os.path.join(results_url, |
340 | self.swift_container, |
341 | self.options.series, |
342 | arch, |
343 | diff --git a/tests/__init__.py b/tests/__init__.py |
344 | index eaa9436..a277747 100644 |
345 | --- a/tests/__init__.py |
346 | +++ b/tests/__init__.py |
347 | @@ -395,9 +395,21 @@ ADT_PPAS = |
348 | ADT_SHARED_RESULTS_CACHE = |
349 | |
350 | ADT_SWIFT_URL = http://localhost:18085 |
351 | +ADT_SWIFT_USER = |
352 | +ADT_SWIFT_PASS = |
353 | +ADT_SWIFT_AUTH_URL = |
354 | +ADT_SWIFT_TENANT = |
355 | +ADT_SWIFT_REGION = |
356 | +ADT_PRIVATE_SHARED = |
357 | +ADT_PRIVATE_URL = |
358 | +ADT_PRIVATE_RETRY = |
359 | ADT_CI_URL = https://autopkgtest.ubuntu.com/ |
360 | ADT_HUGE = 20 |
361 | +<<<<<<< tests/__init__.py |
362 | ADT_DB_URL = |
363 | +======= |
364 | +ADT_SHOW_IRRELEVANT = no |
365 | +>>>>>>> tests/__init__.py |
366 | |
367 | ADT_SUCCESS_BOUNTY = |
368 | ADT_REGRESSION_PENALTY = |
369 | diff --git a/tests/mock_swift.py b/tests/mock_swift.py |
370 | index b33c65a..0fc1a4c 100644 |
371 | --- a/tests/mock_swift.py |
372 | +++ b/tests/mock_swift.py |
373 | @@ -19,6 +19,10 @@ except ImportError: |
374 | from urlparse import urlparse, parse_qs |
375 | |
376 | |
377 | +TESTS_DIR = os.path.dirname(os.path.abspath(__file__)) |
378 | +PROJECT_DIR = os.path.dirname(TESTS_DIR) |
379 | + |
380 | + |
381 | class SwiftHTTPRequestHandler(BaseHTTPRequestHandler): |
382 | '''Mock swift container with autopkgtest results |
383 | |
384 | @@ -124,7 +128,15 @@ class AutoPkgTestSwiftServer: |
385 | ''' |
386 | SwiftHTTPRequestHandler.results = results |
387 | |
388 | - def start(self): |
389 | + def start(self, swiftclient=False): |
390 | + if swiftclient: |
391 | + # since we're running britney directly, the only way to reliably |
392 | + # mock out the swiftclient module is to override it in the local |
393 | + # path with the dummy version we created |
394 | + src = os.path.join(TESTS_DIR, 'mock_swiftclient.py') |
395 | + dst = os.path.join(PROJECT_DIR, 'swiftclient.py') |
396 | + os.symlink(src, dst) |
397 | + |
398 | assert self.server_pid is None, 'already started' |
399 | if self.log: |
400 | self.log.close() |
401 | @@ -148,12 +160,19 @@ class AutoPkgTestSwiftServer: |
402 | sys.exit(0) |
403 | |
404 | def stop(self): |
405 | + # in case we were 'mocking out' swiftclient, remove the symlink we |
406 | + # created earlier during start() |
407 | + swiftclient_mod = os.path.join(PROJECT_DIR, 'swiftclient.py') |
408 | + if os.path.islink(swiftclient_mod): |
409 | + os.unlink(swiftclient_mod) |
410 | + |
411 | assert self.server_pid, 'not running' |
412 | os.kill(self.server_pid, 15) |
413 | os.waitpid(self.server_pid, 0) |
414 | self.server_pid = None |
415 | self.log.close() |
416 | |
417 | + |
418 | if __name__ == '__main__': |
419 | srv = AutoPkgTestSwiftServer() |
420 | srv.set_results({'autopkgtest-testing': { |
421 | diff --git a/tests/mock_swiftclient.py b/tests/mock_swiftclient.py |
422 | new file mode 100644 |
423 | index 0000000..5f419ac |
424 | --- /dev/null |
425 | +++ b/tests/mock_swiftclient.py |
426 | @@ -0,0 +1,70 @@ |
427 | +# Mock the swiftclient Python library, the bare minimum for ADT purposes |
428 | +# Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com> |
429 | + |
430 | +import os |
431 | +import sys |
432 | + |
433 | +from urllib.request import urlopen |
434 | + |
435 | + |
436 | +# We want to use this single Python module file to mock out the exception |
437 | +# module as well. |
438 | +sys.modules["swiftclient.exceptions"] = sys.modules[__name__] |
439 | + |
440 | + |
441 | +class ClientException(Exception): |
442 | + def __init__(self, msg, http_status=''): |
443 | + super(ClientException, self).__init__(msg) |
444 | + self.msg = msg |
445 | + self.http_status = http_status |
446 | + |
447 | + |
448 | +class Connection: |
449 | + def __init__(self, authurl, user, key, tenant_name, auth_version): |
450 | + self._mocked_swift = 'http://localhost:18085' |
451 | + |
452 | + def get_container(self, container, marker=None, limit=None, prefix=None, |
453 | + delimiter=None, end_marker=None, path=None, |
454 | + full_listing=False, headers=None, query_string=None): |
455 | + url = os.path.join(self._mocked_swift, container) + '?' + query_string |
456 | + req = None |
457 | + try: |
458 | + req = urlopen(url, timeout=30) |
459 | + code = req.getcode() |
460 | + if code == 200: |
461 | + result_paths = req.read().decode().strip().splitlines() |
462 | + elif code == 204: # No content |
463 | + result_paths = [] |
464 | + else: |
465 | + raise ClientException('MockedError', http_status=str(code)) |
466 | + except IOError as e: |
467 | + # 401 "Unauthorized" is swift's way of saying "container does not exist" |
468 | + # But here we just assume swiftclient handles this via the usual |
469 | + # ClientException. |
470 | + raise ClientException('MockedError', http_status=str(e.code) if hasattr(e, 'code') else '') |
471 | + finally: |
472 | + if req is not None: |
473 | + req.close() |
474 | + |
475 | + return (None, result_paths) |
476 | + |
477 | + def get_object(self, container, obj): |
478 | + url = os.path.join(self._mocked_swift, container, obj) |
479 | + req = None |
480 | + try: |
481 | + req = urlopen(url, timeout=30) |
482 | + code = req.getcode() |
483 | + if code == 200: |
484 | + contents = req.read() |
485 | + else: |
486 | + raise ClientException('MockedError', http_status=str(code)) |
487 | + except IOError as e: |
488 | + # 401 "Unauthorized" is swift's way of saying "container does not exist" |
489 | + # But here we just assume swiftclient handles this via the usual |
490 | + # ClientException. |
491 | + raise ClientException('MockedError', http_status=str(e.code) if hasattr(e, 'code') else '') |
492 | + finally: |
493 | + if req is not None: |
494 | + req.close() |
495 | + |
496 | + return (None, contents) |
497 | diff --git a/tests/test_autopkgtest.py b/tests/test_autopkgtest.py |
498 | index 5c7bb72..906f729 100644 |
499 | --- a/tests/test_autopkgtest.py |
500 | +++ b/tests/test_autopkgtest.py |
501 | @@ -17,6 +17,7 @@ import urllib.parse |
502 | |
503 | import apt_pkg |
504 | import yaml |
505 | +from unittest.mock import patch, Mock |
506 | |
507 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
508 | sys.path.insert(0, PROJECT_DIR) |
509 | @@ -214,7 +215,7 @@ class TestAutopkgtestBase(TestBase): |
510 | with open(email_path, 'w', encoding='utf-8') as email: |
511 | email.write(json.dumps(self.email_cache)) |
512 | |
513 | - self.swift.start() |
514 | + self.swift.start(swiftclient=True) |
515 | (excuses_yaml, excuses_html, out) = self.run_britney() |
516 | self.swift.stop() |
517 | |
518 | @@ -2666,6 +2667,181 @@ class AT(TestAutopkgtestBase): |
519 | self.assertEqual(self.amqp_requests, set()) |
520 | self.assertEqual(self.pending_requests, {}) |
521 | |
522 | + def test_ppas_fingerprint(self): |
523 | + '''Run test requests with PPAs where fingerprint is provided''' |
524 | + |
525 | + self.data.add_default_packages(lightgreen=False) |
526 | + |
527 | + for line in fileinput.input(self.britney_conf, inplace=True): |
528 | + if line.startswith('ADT_PPAS'): |
529 | + print('ADT_PPAS = joe/foo:fingerprint awesome-developers/staging') |
530 | + else: |
531 | + sys.stdout.write(line) |
532 | + |
533 | + exc = self.run_it( |
534 | + [('lightgreen', {'Version': '2'}, 'autopkgtest')], |
535 | + {'lightgreen': (True, {'lightgreen': {'amd64': 'RUNNING-ALWAYSFAIL'}})}, |
536 | + {'lightgreen': [('old-version', '1'), ('new-version', '2')]} |
537 | + )[1] |
538 | + |
539 | + for arch in ['i386', 'amd64']: |
540 | + self.assertTrue( |
541 | + ('debci-ppa-testing-%s:lightgreen {"triggers": ["lightgreen/2"], ' |
542 | + '"ppas": ["joe/foo", "awesome-developers/staging"]}') % arch in self.amqp_requests or |
543 | + ('debci-ppa-testing-%s:lightgreen {"ppas": ["joe/foo", ' |
544 | + '"awesome-developers/staging"], "triggers": ["lightgreen/2"]}') % arch in self.amqp_requests, |
545 | + self.amqp_requests) |
546 | + self.assertEqual(len(self.amqp_requests), 2) |
547 | + |
548 | + def test_private_ppas(self): |
549 | + '''Run test requests with an additional private PPA''' |
550 | + |
551 | + self.data.add_default_packages(lightgreen=False) |
552 | + |
553 | + for line in fileinput.input(self.britney_conf, inplace=True): |
554 | + if line.startswith('ADT_PPAS'): |
555 | + print('ADT_PPAS = first/ppa user:password@joe/foo:DEADBEEF') |
556 | + elif line.startswith('ADT_SWIFT_USER'): |
557 | + print('ADT_SWIFT_USER = user') |
558 | + elif line.startswith('ADT_SWIFT_PASS'): |
559 | + print('ADT_SWIFT_PASS = pass') |
560 | + elif line.startswith('ADT_SWIFT_TENANT'): |
561 | + print('ADT_SWIFT_TENANT = tenant') |
562 | + elif line.startswith('ADT_SWIFT_REGION'): |
563 | + print('ADT_SWIFT_REGION = region') |
564 | + elif line.startswith('ADT_SWIFT_AUTH_URL'): |
565 | + print('ADT_SWIFT_AUTH_URL = http://127.0.0.1:5000/v2.0/') |
566 | + elif line.startswith('ADT_PRIVATE_SHARED'): |
567 | + print('ADT_PRIVATE_SHARED = user1 team2') |
568 | + elif line.startswith('ADT_PRIVATE_URL'): |
569 | + print('ADT_PRIVATE_URL = http://localhost:18085/private-results/') |
570 | + else: |
571 | + sys.stdout.write(line) |
572 | + |
573 | + exc = self.run_it( |
574 | + [('lightgreen', {'Version': '2'}, 'autopkgtest')], |
575 | + {'lightgreen': (True, {'lightgreen': {'amd64': 'RUNNING-ALWAYSFAIL'}})}, |
576 | + {'lightgreen': [('old-version', '1'), ('new-version', '2')]} |
577 | + )[1] |
578 | + |
579 | + # check if the private PPA info is propagated to the AMQP queue |
580 | + self.assertEqual(len(self.amqp_requests), 2) |
581 | + for request in self.amqp_requests: |
582 | + self.assertIn('"triggers": ["lightgreen/2"]', request) |
583 | + self.assertIn('"ppas": ["first/ppa", "user:password@joe/foo:DEADBEEF"]', request) |
584 | + self.assertIn('"swiftuser": "user"', request) |
585 | + self.assertIn('"readable-by": ["user1", "team2"]', request) |
586 | + |
587 | + # add results to PPA specific swift container |
588 | + self.swift.set_results({'private-autopkgtest-testing-joe-foo': { |
589 | + 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('passedbefore/1')), |
590 | + 'testing/i386/l/lightgreen/20150101_100100@': (4, 'lightgreen 2', tr('lightgreen/2')), |
591 | + 'testing/amd64/l/lightgreen/20150101_100101@': (0, 'lightgreen 2', tr('lightgreen/2')), |
592 | + }}) |
593 | + |
594 | + exc = self.run_it( |
595 | + [], |
596 | + {'lightgreen': (False, {'lightgreen/2': {'i386': 'REGRESSION', 'amd64': 'PASS'}})}, |
597 | + {'lightgreen': [('old-version', '1'), ('new-version', '2')]} |
598 | + )[1] |
599 | + # check if the right container name is used and that no secrets are |
600 | + # leaked in the retry url (as it should be None now) |
601 | + self.assertEqual( |
602 | + exc['lightgreen']['policy_info']['autopkgtest'], |
603 | + {'lightgreen/2': { |
604 | + 'amd64': [ |
605 | + 'PASS', |
606 | + 'http://localhost:18085/private-results/private-autopkgtest-testing-joe-foo/' |
607 | + 'testing/amd64/l/lightgreen/20150101_100101@/log.gz', |
608 | + None, |
609 | + 'http://localhost:18085/private-results/private-autopkgtest-testing-joe-foo/' |
610 | + 'testing/amd64/l/lightgreen/20150101_100101@/artifacts.tar.gz', |
611 | + None], |
612 | + 'i386': [ |
613 | + 'REGRESSION', |
614 | + 'http://localhost:18085/private-results/private-autopkgtest-testing-joe-foo/' |
615 | + 'testing/i386/l/lightgreen/20150101_100100@/log.gz', |
616 | + None, |
617 | + 'http://localhost:18085/private-results/private-autopkgtest-testing-joe-foo/' |
618 | + 'testing/i386/l/lightgreen/20150101_100100@/artifacts.tar.gz', |
619 | + None]}, |
620 | + 'verdict': 'REJECTED_PERMANENTLY'}) |
621 | + self.assertEqual(self.amqp_requests, set()) |
622 | + self.assertEqual(self.pending_requests, {}) |
623 | + |
624 | + def test_private_without_ppa(self): |
625 | + '''Run a private test (without using a private PPA)''' |
626 | + |
627 | + self.data.add_default_packages(lightgreen=False) |
628 | + |
629 | + for line in fileinput.input(self.britney_conf, inplace=True): |
630 | + if line.startswith('ADT_SWIFT_USER'): |
631 | + print('ADT_SWIFT_USER = user') |
632 | + elif line.startswith('ADT_SWIFT_PASS'): |
633 | + print('ADT_SWIFT_PASS = pass') |
634 | + elif line.startswith('ADT_SWIFT_TENANT'): |
635 | + print('ADT_SWIFT_TENANT = tenant') |
636 | + elif line.startswith('ADT_SWIFT_REGION'): |
637 | + print('ADT_SWIFT_REGION = region') |
638 | + elif line.startswith('ADT_SWIFT_AUTH_URL'): |
639 | + print('ADT_SWIFT_AUTH_URL = http://127.0.0.1:5000/v2.0/') |
640 | + elif line.startswith('ADT_PRIVATE_URL'): |
641 | + print('ADT_PRIVATE_URL = http://localhost:18085/private-results/') |
642 | + else: |
643 | + sys.stdout.write(line) |
644 | + |
645 | + exc = self.run_it( |
646 | + [('lightgreen', {'Version': '2'}, 'autopkgtest')], |
647 | + {'lightgreen': (True, {'lightgreen': {'amd64': 'RUNNING-ALWAYSFAIL'}})}, |
648 | + {'lightgreen': [('old-version', '1'), ('new-version', '2')]} |
649 | + )[1] |
650 | + |
651 | + # check if the user info is propagated to the AMQP queue |
652 | + self.assertEqual(len(self.amqp_requests), 2) |
653 | + for request in self.amqp_requests: |
654 | + self.assertIn('"triggers": ["lightgreen/2"]', request) |
655 | + self.assertIn('"swiftuser": "user"', request) |
656 | + # we did not give a list of users to give read-access, so make sure |
657 | + # there are none |
658 | + self.assertNotIn('"readable-by"', request) |
659 | + |
660 | + # add results to PPA specific swift container |
661 | + self.swift.set_results({'private-autopkgtest-testing': { |
662 | + 'testing/i386/l/lightgreen/20150101_100000@': (0, 'lightgreen 1', tr('passedbefore/1')), |
663 | + 'testing/i386/l/lightgreen/20150101_100100@': (4, 'lightgreen 2', tr('lightgreen/2')), |
664 | + 'testing/amd64/l/lightgreen/20150101_100101@': (0, 'lightgreen 2', tr('lightgreen/2')), |
665 | + }}) |
666 | + |
667 | + exc = self.run_it( |
668 | + [], |
669 | + {'lightgreen': (False, {'lightgreen/2': {'i386': 'REGRESSION', 'amd64': 'PASS'}})}, |
670 | + {'lightgreen': [('old-version', '1'), ('new-version', '2')]} |
671 | + )[1] |
672 | + |
673 | + # check if the right container name is used and that we still have the |
674 | + # retry url (it should only be hidden for private PPAs) |
675 | + self.assertEqual( |
676 | + exc['lightgreen']['policy_info']['autopkgtest'], |
677 | + {'lightgreen/2': { |
678 | + 'amd64': [ |
679 | + 'PASS', |
680 | + 'http://localhost:18085/private-results/private-autopkgtest-testing/' |
681 | + 'testing/amd64/l/lightgreen/20150101_100101@/log.gz', |
682 | + 'https://autopkgtest.ubuntu.com/packages/l/lightgreen/testing/amd64', |
683 | + None, |
684 | + None], |
685 | + 'i386': [ |
686 | + 'REGRESSION', |
687 | + 'http://localhost:18085/private-results/private-autopkgtest-testing/' |
688 | + 'testing/i386/l/lightgreen/20150101_100100@/log.gz', |
689 | + 'https://autopkgtest.ubuntu.com/packages/l/lightgreen/testing/i386', |
690 | + None, |
691 | + 'https://autopkgtest.ubuntu.com/request.cgi?release=testing&arch=i386&package=lightgreen&' |
692 | + 'trigger=lightgreen%2F2']}, |
693 | + 'verdict': 'REJECTED_PERMANENTLY'}) |
694 | + self.assertEqual(self.amqp_requests, set()) |
695 | + self.assertEqual(self.pending_requests, {}) |
696 | + |
697 | def test_disable_upgrade_tester(self): |
698 | '''Run without second stage upgrade tester''' |
699 | |
700 | diff --git a/tests/test_policy.py b/tests/test_policy.py |
701 | index 1952831..c6600c0 100644 |
702 | --- a/tests/test_policy.py |
703 | +++ b/tests/test_policy.py |
704 | @@ -43,6 +43,12 @@ def initialize_policy(test_name, policy_class, *args, **kwargs): |
705 | architectures=ARCH, |
706 | adt_swift_url='file://' + debci_data, |
707 | adt_ci_url='', |
708 | + adt_swift_user='', |
709 | + adt_swift_pass='', |
710 | + adt_swift_tenant='', |
711 | + adt_swift_auth_url='', |
712 | + adt_private_shared=[], |
713 | + adt_private_url='', |
714 | adt_success_bounty=3, |
715 | adt_regression_penalty=False, |
716 | adt_retry_url_mech='run_id', |
Looks good, minor comments.