Merge lp:~canonical-platform-qa/qakit/trigger_jenkins_builds into lp:qakit

Proposed by Santiago Baldassin
Status: Approved
Approved by: Sergio Cazzolato
Approved revision: 157
Proposed branch: lp:~canonical-platform-qa/qakit/trigger_jenkins_builds
Merge into: lp:qakit
Diff against target: 444 lines (+381/-0)
10 files modified
qakit/config/__init__.py (+14/-0)
qakit/config/config.py (+26/-0)
qakit/launchpad/__init__.py (+14/-0)
qakit/launchpad/ppa.py (+49/-0)
qakit/practitest/data/__init__.py (+14/-0)
qakit/practitest/data/domains.json (+8/-0)
qakit/practitest/practitest.py (+19/-0)
qakit/ust/__init__.py (+26/-0)
qakit/ust/worker.py (+180/-0)
start_worker.py (+31/-0)
To merge this branch: bzr merge lp:~canonical-platform-qa/qakit/trigger_jenkins_builds
Reviewer Review Type Date Requested Status
Sergio Cazzolato Approve
Richard Huddie (community) Needs Fixing
Jean-Baptiste Lallement Pending
Review via email: mp+304847@code.launchpad.net

Commit message

Ubuntu system tests worker

Description of the change

This mp includes a worker that could be used by Bileto to trigger the ubuntu system tests. There are only two public apis that should be used and that can be invoked directly with python or via an amqp message. Those apis are

run_ust(ppa)
get_results(ppa)

Every single request in Bileto gets it's own ppa with the source packages involved in the change.

The first api will execute the following steps:
        1. Get the source packages from launchpad that are
        included in the ppa under test
        2. Use the packages versions to create a request id that will be use to track
        the test results for the source packages. The idea is to run the tests for every
        version of the source packages in the ppa
        3. Get the practitest filter id associated to the source packages in the ppa
        4. Get the list of test cases to be run from practitest
        5. Trigger a jenkins build that will:
            a. Flash the device
            b. Update the device with the ppa under test
            c. Setup the device for testing
            d. Run the tests

The second api will retrive the test results for a given ppa

To post a comment you must log in.
Revision history for this message
Sergio Cazzolato (sergio-j-cazzolato) wrote :

Some minor comments inline. In parallel you could start testing it integrated with jenkins to see how it works.

Please make sure in the final solution the jenkins jobs invoked have timeouts set.

review: Needs Fixing
Revision history for this message
Santiago Baldassin (sbaldassin) wrote :

Thanks for your review Sergio. I've addressed all your comments. By the way, I'm already testing this using real Jenkins job. This is the job that I'm using: https://platform-qa-jenkins.ubuntu.com/job/test_alesage_ust_rc-proposed_arale_add_ppa/

154. By Santiago Baldassin

Addressing comments from the reviews

Revision history for this message
Richard Huddie (rhuddie) wrote :

Some minor comments and questions below.

review: Needs Fixing
155. By Santiago Baldassin

Updating the worker and the domains

156. By Santiago Baldassin

Addressing comments from the reviews

157. By Santiago Baldassin

Merge from trunk

Revision history for this message
Sergio Cazzolato (sergio-j-cazzolato) wrote :

Approved, code lgtm

review: Approve

Unmerged revisions

157. By Santiago Baldassin

Merge from trunk

156. By Santiago Baldassin

Addressing comments from the reviews

155. By Santiago Baldassin

Updating the worker and the domains

154. By Santiago Baldassin

Addressing comments from the reviews

153. By Santiago Baldassin

Ubuntu system tests worker

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'qakit/config'
2=== added file 'qakit/config/__init__.py'
3--- qakit/config/__init__.py 1970-01-01 00:00:00 +0000
4+++ qakit/config/__init__.py 2016-09-15 18:52:25 +0000
5@@ -0,0 +1,14 @@
6+# Copyright (C) 2016 Canonical
7+#
8+# This program is free software: you can redistribute it and/or modify
9+# it under the terms of the GNU General Public License as published by
10+# the Free Software Foundation, either version 3 of the License, or
11+# (at your option) any later version.
12+#
13+# This program is distributed in the hope that it will be useful,
14+# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+# GNU General Public License for more details.
17+#
18+# You should have received a copy of the GNU General Public License
19+# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21=== added file 'qakit/config/config.py'
22--- qakit/config/config.py 1970-01-01 00:00:00 +0000
23+++ qakit/config/config.py 2016-09-15 18:52:25 +0000
24@@ -0,0 +1,26 @@
25+# Copyright (C) 2016 Canonical
26+#
27+# This program is free software: you can redistribute it and/or modify
28+# it under the terms of the GNU General Public License as published by
29+# the Free Software Foundation, either version 3 of the License, or
30+# (at your option) any later version.
31+#
32+# This program is distributed in the hope that it will be useful,
33+# but WITHOUT ANY WARRANTY; without even the implied warranty of
34+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35+# GNU General Public License for more details.
36+#
37+# You should have received a copy of the GNU General Public License
38+# along with this program. If not, see <http://www.gnu.org/licenses/>.
39+
40+import os
41+from configparser import ConfigParser
42+
43+
44+class Config:
45+
46+ def __init__(self):
47+ self.parser = ConfigParser()
48+ conf_file = os.getenv('QAKIT_CONFIG',
49+ os.path.expanduser('~/.config/qakit.ini'))
50+ self.parser.read(conf_file)
51
52=== added directory 'qakit/launchpad'
53=== added file 'qakit/launchpad/__init__.py'
54--- qakit/launchpad/__init__.py 1970-01-01 00:00:00 +0000
55+++ qakit/launchpad/__init__.py 2016-09-15 18:52:25 +0000
56@@ -0,0 +1,14 @@
57+# Copyright (C) 2016 Canonical
58+#
59+# This program is free software: you can redistribute it and/or modify
60+# it under the terms of the GNU General Public License as published by
61+# the Free Software Foundation, either version 3 of the License, or
62+# (at your option) any later version.
63+#
64+# This program is distributed in the hope that it will be useful,
65+# but WITHOUT ANY WARRANTY; without even the implied warranty of
66+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
67+# GNU General Public License for more details.
68+#
69+# You should have received a copy of the GNU General Public License
70+# along with this program. If not, see <http://www.gnu.org/licenses/>.
71
72=== added file 'qakit/launchpad/ppa.py'
73--- qakit/launchpad/ppa.py 1970-01-01 00:00:00 +0000
74+++ qakit/launchpad/ppa.py 2016-09-15 18:52:25 +0000
75@@ -0,0 +1,49 @@
76+# Copyright (C) 2016 Canonical
77+#
78+# This program is free software: you can redistribute it and/or modify
79+# it under the terms of the GNU General Public License as published by
80+# the Free Software Foundation, either version 3 of the License, or
81+# (at your option) any later version.
82+#
83+# This program is distributed in the hope that it will be useful,
84+# but WITHOUT ANY WARRANTY; without even the implied warranty of
85+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
86+# GNU General Public License for more details.
87+#
88+# You should have received a copy of the GNU General Public License
89+# along with this program. If not, see <http://www.gnu.org/licenses/>.
90+import hashlib
91+
92+from launchpadlib.launchpad import Launchpad
93+
94+
95+class Ppa:
96+
97+ def __init__(self, name, team):
98+ self.name = name
99+ self.team = team
100+
101+ def get_source_packages(self, status='Published'):
102+ """
103+ This method returns from Launchpad, the list of source packages
104+ included in the ppa. By default, only Published source packages
105+ are returned
106+ :param status the source package status to filter the packages
107+ in the ppa
108+ :return a list of source packages
109+ """
110+ launchpad = Launchpad.login_anonymously('ci job', 'production')
111+ team = launchpad.people.findTeam(text=self.team)[0]
112+ archive = team.getPPAByName(name=self.name)
113+ return archive.getPublishedSources(status=status)
114+
115+ def calculate_hash(self):
116+ """
117+ This method get the list of published source packages in the ppa
118+ and use their version to calculate a hash which uniquely identify
119+ the current set of published packages in the ppa
120+ :return: the hash that identifies the list of packages in the ppa
121+ """
122+ src_pkgs = self.get_source_packages()
123+ versions = ''.join([package.source_package_version for package in src_pkgs])
124+ return hashlib.md5(versions.encode('utf-8')).hexdigest()
125
126=== added directory 'qakit/practitest/data'
127=== added file 'qakit/practitest/data/__init__.py'
128--- qakit/practitest/data/__init__.py 1970-01-01 00:00:00 +0000
129+++ qakit/practitest/data/__init__.py 2016-09-15 18:52:25 +0000
130@@ -0,0 +1,14 @@
131+# Copyright (C) 2016 Canonical
132+#
133+# This program is free software: you can redistribute it and/or modify
134+# it under the terms of the GNU General Public License as published by
135+# the Free Software Foundation, either version 3 of the License, or
136+# (at your option) any later version.
137+#
138+# This program is distributed in the hope that it will be useful,
139+# but WITHOUT ANY WARRANTY; without even the implied warranty of
140+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
141+# GNU General Public License for more details.
142+#
143+# You should have received a copy of the GNU General Public License
144+# along with this program. If not, see <http://www.gnu.org/licenses/>.
145
146=== added file 'qakit/practitest/data/domains.json'
147--- qakit/practitest/data/domains.json 1970-01-01 00:00:00 +0000
148+++ qakit/practitest/data/domains.json 2016-09-15 18:52:25 +0000
149@@ -0,0 +1,8 @@
150+{
151+ "unity-scope-click": ["126288"],
152+ "pay-service": ["126288"],
153+ "account-plugins": ["126288"],
154+ "signon-plugin-oauth2": ["126288"],
155+ "messaging-app": ["126288"],
156+ "goget-ubuntu-touch": ["126288"]
157+}
158
159=== modified file 'qakit/practitest/practitest.py'
160--- qakit/practitest/practitest.py 2015-11-30 18:24:48 +0000
161+++ qakit/practitest/practitest.py 2016-09-15 18:52:25 +0000
162@@ -18,6 +18,8 @@
163 import datetime
164 import json
165 import logging
166+import os
167+
168 import requests
169
170 from pprint import pformat
171@@ -384,3 +386,20 @@
172 logging.debug(pformat(result))
173 logging.info('Updating issue pt:{} / lp:{}'.format(ptid, lpid))
174 return self._put(url, data=result)
175+
176+ @staticmethod
177+ def get_filters_for_src_pkgs(src_pkgs):
178+ """
179+ This method return the list of practitest filter ids associated
180+ to the src pacckages in the params
181+ :param src_pkgs: list of source packages
182+ :return: list of filters
183+ """
184+ practitest_dir = os.path.dirname(__file__)
185+ domains_file = os.path.join(practitest_dir, 'data', 'domains.json')
186+ with open(domains_file) as domains:
187+ domains_json = json.loads(domains.read())
188+ filters = [domains_json[package] for package in src_pkgs]
189+
190+ # Flat the lists and remove duplicates
191+ return list(set([item for sublist in filters for item in sublist]))
192
193=== added directory 'qakit/ust'
194=== added file 'qakit/ust/__init__.py'
195--- qakit/ust/__init__.py 1970-01-01 00:00:00 +0000
196+++ qakit/ust/__init__.py 2016-09-15 18:52:25 +0000
197@@ -0,0 +1,26 @@
198+# Copyright (C) 2016 Canonical
199+#
200+# This program is free software: you can redistribute it and/or modify
201+# it under the terms of the GNU General Public License as published by
202+# the Free Software Foundation, either version 3 of the License, or
203+# (at your option) any later version.
204+#
205+# This program is distributed in the hope that it will be useful,
206+# but WITHOUT ANY WARRANTY; without even the implied warranty of
207+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
208+# GNU General Public License for more details.
209+#
210+# You should have received a copy of the GNU General Public License
211+# along with this program. If not, see <http://www.gnu.org/licenses/>.
212+import logging
213+import sys
214+
215+root = logging.getLogger()
216+root.setLevel(logging.DEBUG)
217+
218+ch = logging.StreamHandler(sys.stdout)
219+ch.setLevel(logging.INFO)
220+formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
221+ch.setFormatter(formatter)
222+root.addHandler(ch)
223+
224
225=== added file 'qakit/ust/worker.py'
226--- qakit/ust/worker.py 1970-01-01 00:00:00 +0000
227+++ qakit/ust/worker.py 2016-09-15 18:52:25 +0000
228@@ -0,0 +1,180 @@
229+# Copyright (C) 2016 Canonical
230+#
231+# This program is free software: you can redistribute it and/or modify
232+# it under the terms of the GNU General Public License as published by
233+# the Free Software Foundation, either version 3 of the License, or
234+# (at your option) any later version.
235+#
236+# This program is distributed in the hope that it will be useful,
237+# but WITHOUT ANY WARRANTY; without even the implied warranty of
238+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
239+# GNU General Public License for more details.
240+#
241+# You should have received a copy of the GNU General Public License
242+# along with this program. If not, see <http://www.gnu.org/licenses/>.
243+import json
244+import logging
245+
246+from jenkins import Jenkins
247+from pika import BlockingConnection
248+from pika import ConnectionParameters
249+
250+from qakit.config.config import Config
251+from qakit.launchpad.ppa import Ppa
252+from qakit.practitest.practitest import PractitestSession
253+
254+
255+class Worker:
256+
257+ bileto_results = {"PASSED": "Approved", "FAILURE": "Failed", "QUEUED": "Queued"}
258+
259+ def __init__(self):
260+ self.conf = Config()
261+ self.jenkins = Jenkins(self.conf.parser.get('jenkins', 'server'),
262+ self.conf.parser.get('jenkins', 'username'),
263+ self.conf.parser.get('jenkins', 'password'))
264+
265+ def run_ust(self, ppa):
266+ """"
267+ This method will execute the following steps:
268+ 1. Get the source packages from launchpad that are
269+ included in the ppa under test
270+ 2. Use the packages versions to create a request id that will be use to track
271+ the test results for the source packages. The idea is to run the tests for every
272+ version of the source packages in the ppa
273+ 3. Get the practitest filter id associated to the source packages in the ppa
274+ 4. Get the list of test cases to be run from practitest
275+ 5. Trigger a jenkins build that will:
276+ a. Flash the device
277+ b. Update the device with the ppa under test
278+ c. Setup the device for testing
279+ d. Run the tests
280+
281+ :param ppa is the ppa associated to the request in bileto and it is
282+ expected to be in the format team/ppa_name.
283+ For example: ci-train-ppa-service/landing-059
284+
285+ This method has two entry points, it can be called by importing the
286+ qakit module and creating a Worker instance like this
287+ Worker().run_ust("ci-train-ppa-service/landing-059")
288+ And there's also an amqp entry point. The start method will start an amqp
289+ client which will be subscribed to /ubuntu-system-test, whenever a message
290+ is published in such a topic, it will be process by the
291+ process_ust_request method which will call this one to run the tests
292+ """
293+ team, ppa_name = ppa.split('/')
294+ launchpad_ppa = Ppa(team=team, name=ppa_name)
295+ src_pkgs = launchpad_ppa.get_source_packages()
296+ packages = list(
297+ set([package.source_package_name for package in src_pkgs]))
298+ request_id = launchpad_ppa.calculate_hash()
299+
300+ pt = PractitestSession(self.conf.parser.get('practitest', 'project_id'),
301+ self.conf.parser.get('practitest', 'api_key'),
302+ self.conf.parser.get('practitest', 'api_secret_key'))
303+ filters = pt.get_filters_for_src_pkgs(packages)
304+ tests = pt.get_tests(filters)
305+
306+ try:
307+ tests_ids = ' '.join([test['___f_10589']['value'] for test in tests])
308+ except KeyError:
309+ tests_ids = ' '
310+
311+ self.jenkins.build_job(self.conf.parser.get('jenkins', 'job_name'),
312+ {"PPA": ppa, "TESTS_TO_RUN": tests_ids, "REQUEST_ID": request_id},
313+ {'token': self.conf.parser.get('jenkins', 'token')})
314+
315+ def get_result(self, ppa):
316+ """
317+ The tests results are retrieved from jenkins and the ppa is used to get the
318+ appropriate tests results. As when the tests were triggered, the list of
319+ source packages belonging to the ppa are retrieved from launchpad and their
320+ versions is used to get the request id associated to those group of packages
321+
322+ We'll first iterate on the builds returned by the get_job_info and we'll return
323+ the results associated to the build which request id matches the request id calculated
324+ by this method.
325+
326+ If the correct build is not found in the get_job_info results, then we'll check the
327+ get_queue_info and if the build is not found there, then an error is returned
328+ """
329+ team, ppa_name = ppa.split('/')
330+ launchpad_ppa = Ppa(team=team, name=ppa_name)
331+ request_id = launchpad_ppa.calculate_hash()
332+
333+ builds = self.jenkins.get_job_info(
334+ self.conf.parser.get('jenkins', 'job_name'), depth=2)['builds']
335+ build = self._get_build_by_request_id(builds, request_id)
336+
337+ if build:
338+ build_info = self.jenkins.get_build_info(
339+ self.conf.parser.get('jenkins', 'job_name'), build['number'])
340+ return UstResult(self.bileto_results[build_info['result']], build_info['url'])
341+ else:
342+ builds = self.jenkins.get_queue_info()
343+ build = self._get_build_by_request_id(builds, request_id)
344+ if build:
345+ return UstResult("QUEUED", "")
346+ else:
347+ return None
348+
349+ @staticmethod
350+ def _get_build_by_request_id(builds, request_id):
351+ """
352+ This method iterates over a list of buils and return the one which
353+ request id in the parameters matches the request_id
354+ :param builds: List of builds
355+ :param request_id: request id to filter the builds
356+ :return: build
357+ """
358+ for build in builds:
359+ parameters = list(filter(
360+ lambda action: action.get('parameters', None),
361+ build['actions']))[0]['parameters']
362+ try:
363+ req_id = list(filter(
364+ lambda kv: kv['name'] == 'REQUEST_ID', parameters))[0]['value']
365+ if request_id == req_id:
366+ return build
367+ except IndexError:
368+ logging.info("Build does not have a request id set")
369+ return None
370+
371+ def start(self):
372+ """
373+ This method would start an amqp client and subscribe to the queue
374+ set in the configuration file
375+ """
376+ try:
377+ logging.info('Connecting to AMQP server: {}'.format(
378+ self.conf.parser.get('amqp', 'host')))
379+ connection = BlockingConnection(ConnectionParameters(
380+ host=self.conf.parser.get('amqp', 'host')))
381+ channel = connection.channel()
382+ queue = self.conf.parser.get('amqp', 'queue')
383+
384+ logging.info('Subscribing to queue: {}'.format(
385+ self.conf.parser.get('amqp', 'queue')))
386+ channel.queue_declare(queue, durable=True, auto_delete=False)
387+ channel.basic_consume(
388+ self.process_ust_request, queue=queue, no_ack=True)
389+ channel.start_consuming()
390+ except KeyboardInterrupt:
391+ channel.stop_consuming()
392+ connection.close()
393+
394+ def process_ust_request(self, ch, method, properties, body):
395+ """Callback for ubuntu system test request"""
396+ logging.info('Message received {}'.format(body))
397+
398+ ppa = json.loads(body.decode('utf-8'))['ppa']
399+ self.run_ust(ppa)
400+
401+
402+class UstResult:
403+ def __init__(self, status, build_url):
404+ self.status = status
405+ self.build_url = build_url
406+
407+if __name__ == '__main__':
408+ Worker().start()
409
410=== added file 'start_worker.py'
411--- start_worker.py 1970-01-01 00:00:00 +0000
412+++ start_worker.py 2016-09-15 18:52:25 +0000
413@@ -0,0 +1,31 @@
414+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
415+
416+#
417+# Ubuntu System Tests
418+# Copyright (C) 2016 Canonical
419+#
420+# This program is free software: you can redistribute it and/or modify
421+# it under the terms of the GNU General Public License as published by
422+# the Free Software Foundation, either version 3 of the License, or
423+# (at your option) any later version.
424+#
425+# This program is distributed in the hope that it will be useful,
426+# but WITHOUT ANY WARRANTY; without even the implied warranty of
427+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
428+# GNU General Public License for more details.
429+#
430+# You should have received a copy of the GNU General Public License
431+# along with this program. If not, see <http://www.gnu.org/licenses/>.
432+#
433+
434+import sys
435+
436+from qakit.ust.worker import Worker
437+
438+
439+def main():
440+ worker = Worker()
441+ return worker.start()
442+
443+if __name__ == '__main__':
444+ sys.exit(main())

Subscribers

People subscribed via source and target branches

to all changes: