Merge lp:~dooferlad/linaro-ci-dashboard/get-command into lp:linaro-ci-dashboard
- get-command
- Merge into trunk
Proposed by
James Tunnicliffe
Status: | Merged |
---|---|
Merged at revision: | 70 |
Proposed branch: | lp:~dooferlad/linaro-ci-dashboard/get-command |
Merge into: | lp:linaro-ci-dashboard |
Diff against target: |
902 lines (+605/-23) 25 files modified
HACKING (+12/-0) Makefile (+2/-0) README (+2/-0) config_template/build_scripts/CIJob.sh (+2/-0) config_template/config.xml (+2/-2) config_template/jenkins-jobs/CIJob.xml (+27/-0) dashboard/frontend/api/api.py (+110/-2) dashboard/frontend/api/urls.py (+2/-0) dashboard/frontend/ci_job/migrations/0001_initial.py (+60/-0) dashboard/frontend/ci_job/models/__init__.py (+1/-0) dashboard/frontend/ci_job/models/ci_job.py (+135/-0) dashboard/frontend/ci_job/urls.py (+31/-0) dashboard/frontend/ci_job/views/ci_job_build_view.py (+25/-0) dashboard/frontend/ci_job/views/ci_job_detail_view.py (+24/-0) dashboard/frontend/management/commands/jenkins.py (+3/-3) dashboard/frontend/management/commands/tick.py (+36/-0) dashboard/frontend/models/loop_reference.py (+1/-0) dashboard/frontend/tests/test_api.py (+108/-13) dashboard/frontend/views/index_view.py (+2/-0) dashboard/jenkinsserver/models/jenkins_server.py (+12/-1) dashboard/lib/jenkins_dashboard.py (+4/-1) dashboard/lib/xml_to_dict.py (+1/-1) dashboard/settings.py (+1/-0) dashboard/urls.py (+1/-0) requirements.txt (+1/-0) |
To merge this branch: | bzr merge lp:~dooferlad/linaro-ci-dashboard/get-command |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Milo Casagrande (community) | Approve | ||
Review via email: mp+150548@code.launchpad.net |
Commit message
Description of the change
API changes + tests to support the CLI get command.
Also changed the Jenkins update behaviour so the user is told about a Jenkins update rather than required to perform the update. This is partially because updates can be slow and inconvenient (especially during a demo), but also because sometimes the Jenkins API changes and it can break how the dashboard interacts with it. A better fix would be to have a tested Jenkins version listed in the branch which is the one downloaded and checked against. That version can be bumped as a separate downlaod + test procedure.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'HACKING' |
2 | --- HACKING 2012-09-21 09:24:01 +0000 |
3 | +++ HACKING 2013-02-26 12:48:40 +0000 |
4 | @@ -49,6 +49,18 @@ |
5 | |
6 | Tests should be written and defined inside each sub-application. |
7 | |
8 | +When defining a new application, make sure the model app_label matches what is |
9 | +listed in settings.py: |
10 | + |
11 | +class CIJob(Loop): |
12 | + """Defines the base class for a CI Job.""" |
13 | + class Meta: |
14 | + app_label = 'ci_job' |
15 | + |
16 | +INSTALLED_APPS = ( |
17 | +... |
18 | + 'frontend.ci_job', |
19 | + |
20 | == Class Hierarchy == |
21 | |
22 | === Templates & Views === |
23 | |
24 | === modified file 'Makefile' |
25 | --- Makefile 2012-09-24 13:18:02 +0000 |
26 | +++ Makefile 2013-02-26 12:48:40 +0000 |
27 | @@ -24,6 +24,8 @@ |
28 | echo "No changes to android_textfield_loop models." |
29 | python dashboard/manage.py schemamigration hwpack_loop --auto || \ |
30 | echo "No changes to hwpack_loop models." |
31 | + python dashboard/manage.py schemamigration ci_job --auto || \ |
32 | + echo "No changes to ci_job models." |
33 | python dashboard/manage.py migrate |
34 | |
35 | migrate: syncdb |
36 | |
37 | === modified file 'README' |
38 | --- README 2013-02-01 11:49:24 +0000 |
39 | +++ README 2013-02-26 12:48:40 +0000 |
40 | @@ -17,6 +17,8 @@ |
41 | build-essential |
42 | openjdk-6-jre-headless |
43 | python-tastypie |
44 | + python-simplejson |
45 | + python-requests |
46 | |
47 | To unit test the API that tastypie provides you need Django version of at least |
48 | 1.4. If you have Django 1.3 the API will still work, but not be unit tested. |
49 | |
50 | === added file 'config_template/build_scripts/CIJob.sh' |
51 | --- config_template/build_scripts/CIJob.sh 1970-01-01 00:00:00 +0000 |
52 | +++ config_template/build_scripts/CIJob.sh 2013-02-26 12:48:40 +0000 |
53 | @@ -0,0 +1,2 @@ |
54 | +touch done |
55 | +echo "done" > MANIFEST |
56 | |
57 | === modified file 'config_template/config.xml' |
58 | --- config_template/config.xml 2012-09-20 08:46:37 +0000 |
59 | +++ config_template/config.xml 2013-02-26 12:48:40 +0000 |
60 | @@ -50,7 +50,7 @@ |
61 | </views> |
62 | <primaryView>All</primaryView> |
63 | <slaveAgentPort>0</slaveAgentPort> |
64 | - <label>IntegrationLoop AndroidLoop KernelLoop AndroidTextFieldLoop HwpackLoop</label> |
65 | + <label>IntegrationLoop AndroidLoop KernelLoop AndroidTextFieldLoop HwpackLoop CIJob</label> |
66 | <nodeProperties/> |
67 | <globalNodeProperties/> |
68 | -</hudson> |
69 | \ No newline at end of file |
70 | +</hudson> |
71 | |
72 | === added file 'config_template/jenkins-jobs/CIJob.xml' |
73 | --- config_template/jenkins-jobs/CIJob.xml 1970-01-01 00:00:00 +0000 |
74 | +++ config_template/jenkins-jobs/CIJob.xml 2013-02-26 12:48:40 +0000 |
75 | @@ -0,0 +1,27 @@ |
76 | +<?xml version='1.0' encoding='UTF-8'?> |
77 | +<project> |
78 | + <actions/> |
79 | + <description></description> |
80 | + <keepDependencies>false</keepDependencies> |
81 | + <properties/> |
82 | + <scm class="hudson.scm.NullSCM"/> |
83 | + <assignedNode>{assigned_node}</assignedNode> |
84 | + <canRoam>false</canRoam> |
85 | + <disabled>false</disabled> |
86 | + <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding> |
87 | + <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding> |
88 | + <triggers class="vector"/> |
89 | + <concurrentBuild>false</concurrentBuild> |
90 | + <builders> |
91 | + <hudson.tasks.Shell> |
92 | + <command>{build_script}</command> |
93 | + </hudson.tasks.Shell> |
94 | + </builders> |
95 | + <publishers> |
96 | + <hudson.tasks.ArtifactArchiver> |
97 | + <artifacts>*</artifacts> |
98 | + <latestOnly>false</latestOnly> |
99 | + </hudson.tasks.ArtifactArchiver> |
100 | + </publishers> |
101 | + <buildWrappers/> |
102 | +</project> |
103 | |
104 | === modified file 'dashboard/frontend/api/api.py' |
105 | --- dashboard/frontend/api/api.py 2013-02-04 10:59:49 +0000 |
106 | +++ dashboard/frontend/api/api.py 2013-02-26 12:48:40 +0000 |
107 | @@ -16,9 +16,13 @@ |
108 | # along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. |
109 | |
110 | from tastypie.resources import ModelResource |
111 | -from tastypie.authorization import DjangoAuthorization |
112 | -from tastypie.authentication import ApiKeyAuthentication |
113 | +from tastypie.authorization import DjangoAuthorization, Authorization |
114 | +from tastypie.authentication import ApiKeyAuthentication, Authentication |
115 | from dashboard.frontend.models.loop import Loop |
116 | +from frontend.ci_job.models.ci_job import CIJob |
117 | +from frontend.models.loop_build import LoopBuild |
118 | +from tastypie.constants import ALL, ALL_WITH_RELATIONS |
119 | +from tastypie import fields |
120 | |
121 | """ |
122 | Add API endpoint logic here. The documentation to do this can be found here: |
123 | @@ -54,3 +58,107 @@ |
124 | resource_name = 'loops' |
125 | authorization = DjangoAuthorization() |
126 | authentication = ApiKeyAuthentication() |
127 | + |
128 | + filtering = {'name': ALL_WITH_RELATIONS} |
129 | + |
130 | + |
131 | +class CIJobResource(ModelResource): |
132 | + class Meta: |
133 | + """Returns a list of all loops.""" |
134 | + queryset = CIJob.objects.all() |
135 | + resource_name = 'jobs' |
136 | + list_allowed_methods = ['get', 'post'] |
137 | + detail_allowed_methods = ['get', 'post', 'put', 'delete'] |
138 | + |
139 | + #http://django-tastypie.readthedocs.org/en/latest/authorization.html |
140 | + # Currently everyone who has an API key is equal. Suggest we tie this |
141 | + # down using a custom Authorization class (see above link) so anyone |
142 | + # can create and modify their own jobs, but only some users have |
143 | + # the ability to delete and modify other users' jobs. |
144 | + authorization = Authorization() #DjangoAuthorization() |
145 | + authentication = ApiKeyAuthentication() |
146 | + |
147 | + |
148 | +class CIRunResource(ModelResource): |
149 | + loop = fields.ForeignKey(LoopsResource, 'loop') |
150 | + |
151 | + class Meta: |
152 | + """Returns a list of all loops.""" |
153 | + queryset = LoopBuild.objects.all() |
154 | + resource_name = 'runs' |
155 | + list_allowed_methods = ['get', 'post'] |
156 | + detail_allowed_methods = ['get', 'post', 'put', 'delete'] |
157 | + filtering = {'name': 'exact', |
158 | + 'build_number': 'exact', |
159 | + 'loop': ALL_WITH_RELATIONS} |
160 | + |
161 | + #http://django-tastypie.readthedocs.org/en/latest/authorization.html |
162 | + # Currently everyone who has an API key is equal. Suggest we tie this |
163 | + # down using a custom Authorization class (see above link) so anyone |
164 | + # can create and modify their own jobs, but only some users have |
165 | + # the ability to delete and modify other users' jobs. |
166 | + authorization = Authorization() #DjangoAuthorization() |
167 | + authentication = ApiKeyAuthentication() |
168 | + |
169 | + def dehydrate(self, bundle): |
170 | + data = {} |
171 | + # We entirely rewrite this... |
172 | + data["status"] = bundle.data["status"] |
173 | + data["get_build_result"] = bundle.obj.get_build_result() |
174 | + data["build_finished"] = bundle.obj.build_finished() |
175 | + data["list_artifacts"] = bundle.obj.list_artifacts() |
176 | + data["build_number"] = bundle.data["build_number"] |
177 | + data["name"] = bundle.obj.loop.name |
178 | + #data["get_artifact_url"] = bundle.obj.get_artifact_url() |
179 | + #data["get_artifact_content"] = bundle.obj.get_artifact_content() |
180 | + bundle.data = data |
181 | + return bundle |
182 | + |
183 | + def full_hydrate(self, bundle): |
184 | + """ |
185 | + Given a populated bundle, distill it and turn it back into |
186 | + a full-fledged object instance. |
187 | + |
188 | + This is a copy of the tastypie full_hydrate code with a single |
189 | + modification to not try to fill in LoopBuild.loop from the API supplied |
190 | + data. |
191 | + |
192 | + Note that this won't work because we won't create a full object: |
193 | + loop_object = self.fields["loop"] |
194 | + del(self.fields["loop"]) |
195 | + super(self.__class__, self).full_hydrate(bundle) |
196 | + self.fields["loop"] = loop_object |
197 | + """ |
198 | + if bundle.obj is None: |
199 | + bundle.obj = self._meta.object_class() |
200 | + |
201 | + for field_name, field_object in self.fields.items(): |
202 | + # Fill in all attributes apart from loop, which is created during |
203 | + # the (later) hydrate phase. |
204 | + if field_object.attribute and field_object.attribute != "loop": |
205 | + value = field_object.hydrate(bundle) |
206 | + |
207 | + if value is not None or field_object.null: |
208 | + # We need to avoid populating M2M data here as that will |
209 | + # cause things to blow up. |
210 | + if not getattr(field_object, 'is_related', False): |
211 | + setattr(bundle.obj, field_object.attribute, value) |
212 | + elif not getattr(field_object, 'is_m2m', False): |
213 | + if value is not None: |
214 | + setattr(bundle.obj, field_object.attribute, |
215 | + value.obj) |
216 | + |
217 | + # Check for an optional method to do further hydration. |
218 | + method = getattr(self, "hydrate_%s" % field_name, None) |
219 | + |
220 | + if method: |
221 | + bundle = method(bundle) |
222 | + |
223 | + bundle = self.hydrate(bundle) |
224 | + return bundle |
225 | + |
226 | + def hydrate(self, bundle): |
227 | + bundle.obj.loop = Loop.objects.get(name=bundle.data["name"]) |
228 | + bundle.obj.status = 'unscheduled' |
229 | + bundle.obj.duration = 0.0 |
230 | + return bundle |
231 | |
232 | === modified file 'dashboard/frontend/api/urls.py' |
233 | --- dashboard/frontend/api/urls.py 2013-02-04 17:55:49 +0000 |
234 | +++ dashboard/frontend/api/urls.py 2013-02-26 12:48:40 +0000 |
235 | @@ -7,6 +7,8 @@ |
236 | # Register new resources here (probably defined in api.py) |
237 | v1_api.register(LoginTestResource()) |
238 | v1_api.register(LoopsResource()) |
239 | +v1_api.register(CIJobResource()) |
240 | +v1_api.register(CIRunResource()) |
241 | |
242 | urlpatterns = patterns('', |
243 | (r'^api/', include(v1_api.urls)), |
244 | |
245 | === added directory 'dashboard/frontend/ci_job' |
246 | === added file 'dashboard/frontend/ci_job/__init__.py' |
247 | === added directory 'dashboard/frontend/ci_job/migrations' |
248 | === added file 'dashboard/frontend/ci_job/migrations/0001_initial.py' |
249 | --- dashboard/frontend/ci_job/migrations/0001_initial.py 1970-01-01 00:00:00 +0000 |
250 | +++ dashboard/frontend/ci_job/migrations/0001_initial.py 2013-02-26 12:48:40 +0000 |
251 | @@ -0,0 +1,60 @@ |
252 | +# -*- coding: utf-8 -*- |
253 | +import datetime |
254 | +from south.db import db |
255 | +from south.v2 import SchemaMigration |
256 | +from django.db import models |
257 | + |
258 | + |
259 | +class Migration(SchemaMigration): |
260 | + |
261 | + def forwards(self, orm): |
262 | + # Adding model 'CIJob' |
263 | + db.create_table('ci_job_cijob', ( |
264 | + ('loop_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['frontend.Loop'], unique=True, primary_key=True)), |
265 | + ('vcs_url', self.gf('django.db.models.fields.CharField')(max_length=255)), |
266 | + ('vcs_branch', self.gf('django.db.models.fields.CharField')(default='', max_length=255)), |
267 | + ('vcs_tag', self.gf('django.db.models.fields.CharField')(default='', max_length=255)), |
268 | + ('file_name', self.gf('django.db.models.fields.CharField')(max_length=255)), |
269 | + ('class_name', self.gf('django.db.models.fields.CharField')(max_length=255)), |
270 | + ('run_when_ready', self.gf('django.db.models.fields.BooleanField')(default=False)), |
271 | + )) |
272 | + db.send_create_signal('ci_job', ['CIJob']) |
273 | + |
274 | + |
275 | + def backwards(self, orm): |
276 | + # Deleting model 'CIJob' |
277 | + db.delete_table('ci_job_cijob') |
278 | + |
279 | + |
280 | + models = { |
281 | + 'ci_job.cijob': { |
282 | + 'Meta': {'object_name': 'CIJob', '_ormbases': ['frontend.Loop']}, |
283 | + 'class_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
284 | + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
285 | + 'loop_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['frontend.Loop']", 'unique': 'True', 'primary_key': 'True'}), |
286 | + 'run_when_ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
287 | + 'vcs_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), |
288 | + 'vcs_tag': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), |
289 | + 'vcs_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) |
290 | + }, |
291 | + 'frontend.loop': { |
292 | + 'Meta': {'object_name': 'Loop'}, |
293 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
294 | + 'is_official': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
295 | + 'is_restricted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
296 | + 'lava_tests': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
297 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}), |
298 | + 'next_loop': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'previous_loop'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['frontend.Loop']", 'blank': 'True', 'unique': 'True'}), |
299 | + 'server': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['jenkinsserver.JenkinsServer']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), |
300 | + 'type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}) |
301 | + }, |
302 | + 'jenkinsserver.jenkinsserver': { |
303 | + 'Meta': {'object_name': 'JenkinsServer'}, |
304 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
305 | + 'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
306 | + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
307 | + 'username': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
308 | + } |
309 | + } |
310 | + |
311 | + complete_apps = ['ci_job'] |
312 | \ No newline at end of file |
313 | |
314 | === added file 'dashboard/frontend/ci_job/migrations/__init__.py' |
315 | === added directory 'dashboard/frontend/ci_job/models' |
316 | === added file 'dashboard/frontend/ci_job/models/__init__.py' |
317 | --- dashboard/frontend/ci_job/models/__init__.py 1970-01-01 00:00:00 +0000 |
318 | +++ dashboard/frontend/ci_job/models/__init__.py 2013-02-26 12:48:40 +0000 |
319 | @@ -0,0 +1,1 @@ |
320 | +from frontend.ci_job.models.ci_job import * |
321 | |
322 | === added file 'dashboard/frontend/ci_job/models/ci_job.py' |
323 | --- dashboard/frontend/ci_job/models/ci_job.py 1970-01-01 00:00:00 +0000 |
324 | +++ dashboard/frontend/ci_job/models/ci_job.py 2013-02-26 12:48:40 +0000 |
325 | @@ -0,0 +1,135 @@ |
326 | +# Copyright (C) 2012 Linaro |
327 | +# |
328 | +# This file is part of linaro-ci-dashboard. |
329 | +# |
330 | +# linaro-ci-dashboard is free software: you can redistribute it and/or modify |
331 | +# it under the terms of the GNU Affero General Public License as published by |
332 | +# the Free Software Foundation, either version 3 of the License, or |
333 | +# (at your option) any later version. |
334 | +# |
335 | +# linaro-ci-dashboard is distributed in the hope that it will be useful, |
336 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
337 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
338 | +# GNU Affero General Public License for more details. |
339 | +# |
340 | +# You should have received a copy of the GNU Affero General Public License |
341 | +# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. |
342 | + |
343 | +from django.db import models |
344 | +from frontend.models.loop import Loop |
345 | +from jenkinsserver.models.jenkins_server import JenkinsServer |
346 | +from frontend.models.loop_build import LoopBuild |
347 | +#from django.contrib.auth.models import User |
348 | + |
349 | + |
350 | +class CIJob(Loop): |
351 | + """Defines the base class for a CI Job.""" |
352 | + class Meta: |
353 | + app_label = 'ci_job' |
354 | + |
355 | + vcs_url = models.CharField(max_length=255, |
356 | + verbose_name='Configuration repository') |
357 | + vcs_branch = models.CharField(max_length=255, |
358 | + verbose_name='Configuration branch', |
359 | + default='') |
360 | + vcs_tag = models.CharField(max_length=255, |
361 | + verbose_name='Configuration tag', |
362 | + default='') |
363 | + file_name = models.CharField(max_length=255, |
364 | + verbose_name='Configuration file name') |
365 | + class_name = models.CharField(max_length=255, |
366 | + verbose_name='Configuration class name') |
367 | + run_when_ready = models.BooleanField(default=False) |
368 | + |
369 | + #job_owner = models.ForeignKey(User, editable=False, |
370 | + # verbose_name='Job Owner') |
371 | + |
372 | + def save(self, *args, **kwargs): |
373 | + self.server = JenkinsServer.objects.get(id=1) |
374 | + self.type = self.__class__.__name__ |
375 | + super(self.__class__, self).save(*args, **kwargs) |
376 | + # We do not want a failure in remote server to affect the |
377 | + # CI Loop flow. |
378 | + |
379 | + self.server.create_or_update_loop(self) |
380 | + |
381 | + |
382 | + def schedule_build(self, parameters=None): |
383 | + parameters = {'parameter': [{'delay': '0'}]} |
384 | + return super(self.__class__, self).schedule_build(parameters) |
385 | + |
386 | + def base64_config(self, |
387 | + include=None, |
388 | + exclude=None, |
389 | + capitalize=False, |
390 | + upper=False): |
391 | + exclude = ['id', 'server', 'loop_ptr', 'next_loop', 'name', 'type'] |
392 | + return super(CIJob, self).base64_config(include=include, |
393 | + exclude=exclude, |
394 | + capitalize=capitalize, |
395 | + upper=True) |
396 | + |
397 | + def preconfigure(self, configuration=None): |
398 | + """Makes a build parameter dict from build result configuration. |
399 | + |
400 | + This method should be overridden by subclasses. |
401 | + """ |
402 | + if isinstance(configuration, dict): |
403 | + # The name is the name of the loop that is calling the chained one, |
404 | + # not the one we want to actually configure, we drop it. |
405 | + configuration.pop('name') |
406 | + unmatched_values = u'' |
407 | + for key, value in configuration.iteritems(): |
408 | + if hasattr(self, key.lower()): |
409 | + setattr(self, key.lower(), value) |
410 | + else: |
411 | + # If the attr cannot be found, we throw everything in |
412 | + # user_defined_values since the text field is free form. |
413 | + unmatched_values = u'%s=%s\n' % (key.upper(), str(value)) |
414 | + |
415 | + self.user_defined_values = unmatched_values |
416 | + self.save() |
417 | + else: |
418 | + self.log.error("Parameter was not a 'dict' instance.") |
419 | + return {} |
420 | + |
421 | + @models.permalink |
422 | + def get_detail_url(self): |
423 | + return 'CIJobDetail', (), {'slug': self.name} |
424 | + |
425 | + @models.permalink |
426 | + def get_build_url(self): |
427 | + return 'CIJobBuild', (), {'slug': self.name} |
428 | + |
429 | + @models.permalink |
430 | + def get_update_url(self): |
431 | + return 'CIJobUpdate', (), {'slug': self.name} |
432 | + |
433 | + @classmethod |
434 | + def schedule_unscheduled_jobs(self): |
435 | + unscheduled_builds = LoopBuild.objects.filter(status='unscheduled') |
436 | + for build in unscheduled_builds: |
437 | + build.status = 'scheduled' |
438 | + |
439 | + build.remote_number = build.loop.server.schedule_job_build( |
440 | + build.loop.name) |
441 | + build.save() |
442 | + |
443 | + def process_build_results(self, loop_build): |
444 | + """This method gets called with list of artifact files from a build. |
445 | + Its responsibility is to categorize artifacts, and prepare result data |
446 | + to be passed to downstream loop in chain. If needed, this method can |
447 | + access content of artifacts (not just list of them). |
448 | + """ |
449 | + |
450 | + all_files_zip = "*zip*/archive.zip" |
451 | + urls = { |
452 | + all_files_zip: |
453 | + loop_build.get_artifact_url(all_files_zip), |
454 | + } |
455 | + |
456 | + files = loop_build.list_artifacts() |
457 | + for filename in files: |
458 | + urls[filename] = loop_build.get_artifact_url(filename) |
459 | + |
460 | + return urls |
461 | |
462 | === added file 'dashboard/frontend/ci_job/urls.py' |
463 | --- dashboard/frontend/ci_job/urls.py 1970-01-01 00:00:00 +0000 |
464 | +++ dashboard/frontend/ci_job/urls.py 2013-02-26 12:48:40 +0000 |
465 | @@ -0,0 +1,31 @@ |
466 | +# Copyright (C) 2012 Linaro |
467 | +# |
468 | +# This file is part of linaro-ci-dashboard. |
469 | +# |
470 | +# linaro-ci-dashboard is free software: you can redistribute it and/or modify |
471 | +# it under the terms of the GNU Affero General Public License as published by |
472 | +# the Free Software Foundation, either version 3 of the License, or |
473 | +# (at your option) any later version. |
474 | +# |
475 | +# linaro-ci-dashboard is distributed in the hope that it will be useful, |
476 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
477 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
478 | +# GNU Affero General Public License for more details. |
479 | +# |
480 | +# You should have received a copy of the GNU Affero General Public License |
481 | +# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. |
482 | + |
483 | +from django.conf.urls.defaults import patterns, url |
484 | + |
485 | +from frontend.ci_job.views.ci_job_detail_view \ |
486 | + import CIJobDetailView |
487 | +from frontend.ci_job.views.ci_job_build_view \ |
488 | + import CIJobBuildView |
489 | + |
490 | + |
491 | +urlpatterns = patterns('', |
492 | + url(r'^ci_job/detail/(?P<slug>[\w|\W]+)/$', |
493 | + CIJobDetailView.as_view(), name='CIJobDetail'), |
494 | + url(r'^ci_job/build/(?P<slug>[\w|\W]+)/$', |
495 | + CIJobBuildView.as_view(), name='CIJobBuild'), |
496 | +) |
497 | |
498 | === added directory 'dashboard/frontend/ci_job/views' |
499 | === added file 'dashboard/frontend/ci_job/views/__init__.py' |
500 | === added file 'dashboard/frontend/ci_job/views/ci_job_build_view.py' |
501 | --- dashboard/frontend/ci_job/views/ci_job_build_view.py 1970-01-01 00:00:00 +0000 |
502 | +++ dashboard/frontend/ci_job/views/ci_job_build_view.py 2013-02-26 12:48:40 +0000 |
503 | @@ -0,0 +1,25 @@ |
504 | +#!/usr/bin/env python |
505 | +# Copyright (C) 2012 Linaro |
506 | +# |
507 | +# This file is part of linaro-ci-dashboard. |
508 | +# |
509 | +# linaro-ci-dashboard is free software: you can redistribute it and/or modify |
510 | +# it under the terms of the GNU Affero General Public License as published by |
511 | +# the Free Software Foundation, either version 3 of the License, or |
512 | +# (at your option) any later version. |
513 | +# |
514 | +# linaro-ci-dashboard is distributed in the hope that it will be useful, |
515 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
516 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
517 | +# GNU Affero General Public License for more details. |
518 | +# |
519 | +# You should have received a copy of the GNU Affero General Public License |
520 | +# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. |
521 | + |
522 | +from frontend.ci_job.models.ci_job import CIJob |
523 | +from frontend.views.loop_build_view import LoopBuildView |
524 | + |
525 | + |
526 | +class CIJobBuildView(LoopBuildView): |
527 | + |
528 | + model = CIJob |
529 | |
530 | === added file 'dashboard/frontend/ci_job/views/ci_job_detail_view.py' |
531 | --- dashboard/frontend/ci_job/views/ci_job_detail_view.py 1970-01-01 00:00:00 +0000 |
532 | +++ dashboard/frontend/ci_job/views/ci_job_detail_view.py 2013-02-26 12:48:40 +0000 |
533 | @@ -0,0 +1,24 @@ |
534 | +# Copyright (C) 2012 Linaro |
535 | +# |
536 | +# This file is part of linaro-ci-dashboard. |
537 | +# |
538 | +# linaro-ci-dashboard is free software: you can redistribute it and/or modify |
539 | +# it under the terms of the GNU Affero General Public License as published by |
540 | +# the Free Software Foundation, either version 3 of the License, or |
541 | +# (at your option) any later version. |
542 | +# |
543 | +# linaro-ci-dashboard is distributed in the hope that it will be useful, |
544 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
545 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
546 | +# GNU Affero General Public License for more details. |
547 | +# |
548 | +# You should have received a copy of the GNU Affero General Public License |
549 | +# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. |
550 | + |
551 | +from frontend.ci_job.models.ci_job import CIJob |
552 | +from frontend.views.loop_detail_view import LoopDetailView |
553 | + |
554 | + |
555 | +class CIJobDetailView(LoopDetailView): |
556 | + |
557 | + model = CIJob |
558 | |
559 | === modified file 'dashboard/frontend/management/commands/jenkins.py' |
560 | --- dashboard/frontend/management/commands/jenkins.py 2012-09-18 13:06:35 +0000 |
561 | +++ dashboard/frontend/management/commands/jenkins.py 2013-02-26 12:48:40 +0000 |
562 | @@ -144,9 +144,9 @@ |
563 | downloaded_size = int(os.stat(filename).st_size) |
564 | response.close() |
565 | if actual_size != downloaded_size: |
566 | - raise CommandError("Size mismatch between remote and local "\ |
567 | - "jenkins.war, remove %s to upgrade to new "\ |
568 | - "version" % filename) |
569 | + print "Size mismatch between remote and local "\ |
570 | + "jenkins.war, remove %s to upgrade to new "\ |
571 | + "version" % filename |
572 | |
573 | return 0 |
574 | |
575 | |
576 | === added file 'dashboard/frontend/management/commands/tick.py' |
577 | --- dashboard/frontend/management/commands/tick.py 1970-01-01 00:00:00 +0000 |
578 | +++ dashboard/frontend/management/commands/tick.py 2013-02-26 12:48:40 +0000 |
579 | @@ -0,0 +1,36 @@ |
580 | +# Copyright (C) 2013 Linaro |
581 | +# |
582 | +# This file is part of linaro-ci-dashboard. |
583 | +# |
584 | +# linaro-ci-dashboard is free software: you can redistribute it and/or modify |
585 | +# it under the terms of the GNU Affero General Public License as published by |
586 | +# the Free Software Foundation, either version 3 of the License, or |
587 | +# (at your option) any later version. |
588 | +# |
589 | +# linaro-ci-dashboard is distributed in the hope that it will be useful, |
590 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
591 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
592 | +# GNU Affero General Public License for more details. |
593 | +# |
594 | +# You should have received a copy of the GNU Affero General Public License |
595 | +# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. |
596 | +# Django settings for dashboard project. |
597 | + |
598 | +from django.core.management.base import BaseCommand |
599 | +from frontend.ci_job.models.ci_job import CIJob |
600 | +from jenkinsserver.models.jenkins_server import JenkinsServer |
601 | +from django.conf import settings |
602 | + |
603 | + |
604 | +class Command(BaseCommand): |
605 | + args = "" |
606 | + help = "Run background tasks. Needs to be run periodically." |
607 | + |
608 | + def __init__(self): |
609 | + super(self.__class__, self).__init__() |
610 | + self.jenkins_id = settings.JENKINS_DB_ID |
611 | + |
612 | + def handle(self, *args, **options): |
613 | + CIJob.schedule_unscheduled_jobs() |
614 | + server = JenkinsServer.objects.get(id=self.jenkins_id) |
615 | + server.sync_jobs_and_builds() |
616 | |
617 | === modified file 'dashboard/frontend/models/loop_reference.py' |
618 | --- dashboard/frontend/models/loop_reference.py 2012-09-18 12:16:59 +0000 |
619 | +++ dashboard/frontend/models/loop_reference.py 2013-02-26 12:48:40 +0000 |
620 | @@ -23,3 +23,4 @@ |
621 | INTEGRATION_LOOP = ('IntegrationLoop') |
622 | ANDROID_TEXTFIELD_LOOP = ('AndroidTextFieldLoop') |
623 | HWPACK_LOOP = ('HwpackLoop') |
624 | +CI_JOB = ('CIJob') |
625 | |
626 | === modified file 'dashboard/frontend/tests/test_api.py' |
627 | --- dashboard/frontend/tests/test_api.py 2013-02-04 17:55:49 +0000 |
628 | +++ dashboard/frontend/tests/test_api.py 2013-02-26 12:48:40 +0000 |
629 | @@ -19,9 +19,12 @@ |
630 | from django.test import LiveServerTestCase |
631 | from tastypie.models import ApiKey |
632 | from dashboard.frontend.models.loop import Loop |
633 | +from dashboard.frontend.models.loop_build import LoopBuild |
634 | from dashboard.frontend.android_build.models.android_loop import AndroidLoop |
635 | +from dashboard.frontend.ci_job.models.ci_job import CIJob |
636 | import requests |
637 | from urlparse import urljoin |
638 | +import simplejson |
639 | |
640 | |
641 | class ApiTests(LiveServerTestCase): |
642 | @@ -32,13 +35,18 @@ |
643 | def setUp(self): |
644 | super(ApiTests, self).setUp() |
645 | |
646 | + self.settings = {} |
647 | + self.settings["ci_server"] = "http://localhost:8081" |
648 | + self.settings["api_prefix"] = "/api/dev/" |
649 | + self.settings["username"] = 'daniel' |
650 | + self.settings["password"] = 'pass' |
651 | + self.settings["api_key"] = "0" |
652 | + |
653 | # Create a user. |
654 | - self.username = 'daniel' |
655 | - self.password = 'pass' |
656 | self.user = User.objects.create_user( |
657 | - self.username, 'daniel@example.com', self.password) |
658 | - |
659 | - self.api_key = "0" |
660 | + self.settings["username"], |
661 | + 'daniel@example.com', |
662 | + self.settings["password"]) |
663 | |
664 | # Add a couple of jobs to test against. |
665 | self.android_loop = AndroidLoop() |
666 | @@ -51,16 +59,35 @@ |
667 | self.loop.type = Loop.__class__.__name__ |
668 | self.loop.save() |
669 | |
670 | - def get(self, url): |
671 | - payload = {"username": self.username, |
672 | - "api_key": self.api_key, |
673 | - "format": "json"} |
674 | - url = urljoin("http://localhost:8081", "/api/dev/" + url) |
675 | - return requests.get(url, params=payload) |
676 | + def get(self, url, in_params=None): |
677 | + url, params = self._prep_request(url, in_params) |
678 | + return requests.get(url, params=params) |
679 | + |
680 | + def post(self, url, data, in_params=None): |
681 | + url, params = self._prep_request(url, in_params) |
682 | + return requests.post(url, data=data, params=params, |
683 | + headers={"content-type": "application/json"}) |
684 | + |
685 | + def _prep_request(self, url, in_params=None): |
686 | + params = {"username": self.settings["username"], |
687 | + "api_key": self.settings["api_key"], |
688 | + "format": "json"} |
689 | + |
690 | + url = urljoin(self.settings["ci_server"], |
691 | + self.settings["api_prefix"] + url) |
692 | + |
693 | + # If we don't have a trailing slash, we will just be redirected. Avoid |
694 | + # the extra call. |
695 | + if url[-1] != "/": |
696 | + url += "/" |
697 | + |
698 | + if in_params: |
699 | + params.update(in_params) |
700 | + |
701 | + return url, params |
702 | |
703 | def login(self): |
704 | - self.api_key = ApiKey.objects.create(user=self.user).key |
705 | - #self.create_apikey(self.username, self.api_key) |
706 | + self.settings["api_key"] = ApiKey.objects.create(user=self.user).key |
707 | |
708 | def test_login_test_unauthorzied(self): |
709 | r = self.get("login_test") |
710 | @@ -93,3 +120,71 @@ |
711 | api_test_names = {json[0]['name'], json[1]['name']} |
712 | test_names = {self.android_loop.name, self.loop.name} |
713 | self.assertEqual(api_test_names, test_names) |
714 | + |
715 | + def test_add_job(self): |
716 | + self.login() |
717 | + data = { |
718 | + "vcs_url": "vcsurl", |
719 | + "file_name": "filename", |
720 | + "class_name": "classname", |
721 | + "run": False, |
722 | + "name": "jobname" |
723 | + } |
724 | + json_data = simplejson.dumps(data) |
725 | + |
726 | + r = self.post("jobs", json_data, {}) |
727 | + self.assertEqual(r.status_code, requests.codes.created) |
728 | + |
729 | + jobs = CIJob.objects.all() |
730 | + |
731 | + self.assertEqual(len(jobs), 1) |
732 | + self.assertEqual(jobs[0].name, data["name"]) |
733 | + |
734 | + def test_run_job(self): |
735 | + self.login() |
736 | + |
737 | + # First, create a dummy job |
738 | + job = CIJob() |
739 | + job.vcs_url = "vcsurl" |
740 | + job.name = "jobname" |
741 | + job.save() |
742 | + |
743 | + # Now run it |
744 | + data = { |
745 | + "name": "jobname" |
746 | + } |
747 | + json_data = simplejson.dumps(data) |
748 | + |
749 | + r = self.post("runs", json_data, {}) |
750 | + self.assertEqual(r.status_code, requests.codes.created) |
751 | + |
752 | + runs = LoopBuild.objects.all() |
753 | + |
754 | + self.assertEqual(len(runs), 1) |
755 | + self.assertEqual(runs[0].loop.name, data["name"]) |
756 | + |
757 | + def test_get_job(self): |
758 | + self.login() |
759 | + |
760 | + # Create a dummy job |
761 | + job = CIJob() |
762 | + job.vcs_url = "vcsurl" |
763 | + job.name = "jobname" |
764 | + job.save() |
765 | + |
766 | + # Create a dummy run |
767 | + run = LoopBuild() |
768 | + run.build_number = 1 |
769 | + run.status = "SUCCESS" |
770 | + run.duration = 0 |
771 | + run.loop = job |
772 | + run.save() |
773 | + |
774 | + r = self.get("runs") |
775 | + self.assertEqual(r.status_code, requests.codes.ok) |
776 | + |
777 | + run_data = r.json()["objects"] |
778 | + self.assertEqual(len(run_data), 1) |
779 | + self.assertEqual(run_data[0]["build_number"], run.build_number) |
780 | + self.assertEqual(run_data[0]["status"], run.status) |
781 | + self.assertEqual(run_data[0]["name"], job.name) |
782 | |
783 | === modified file 'dashboard/frontend/views/index_view.py' |
784 | --- dashboard/frontend/views/index_view.py 2012-09-13 11:37:13 +0000 |
785 | +++ dashboard/frontend/views/index_view.py 2013-02-26 12:48:40 +0000 |
786 | @@ -25,6 +25,7 @@ |
787 | from frontend.android_textfield_loop.models.android_textfield_loop \ |
788 | import AndroidTextFieldLoop |
789 | from frontend.hwpack_loop.models.hwpack_loop import HwpackLoop |
790 | +from frontend.ci_job.models.ci_job import CIJob |
791 | |
792 | class IndexView(TemplateView): |
793 | |
794 | @@ -42,4 +43,5 @@ |
795 | context['kernel_loops'] = KernelLoop.objects.all() |
796 | context['android_textfield_loops'] = AndroidTextFieldLoop.objects.all() |
797 | context['hwpack_loops'] = HwpackLoop.objects.all() |
798 | + context['ci_jobs'] = CIJob.objects.all() |
799 | return context |
800 | |
801 | === modified file 'dashboard/jenkinsserver/models/jenkins_server.py' |
802 | --- dashboard/jenkinsserver/models/jenkins_server.py 2012-10-04 15:26:28 +0000 |
803 | +++ dashboard/jenkinsserver/models/jenkins_server.py 2013-02-26 12:48:40 +0000 |
804 | @@ -24,6 +24,7 @@ |
805 | from lib.logger import Logger |
806 | from lib.template import TextTemplate, XMLTemplate |
807 | from dashboard.lib.xml_to_dict import DictToXml |
808 | +import requests |
809 | |
810 | |
811 | class JenkinsServer(models.Model): |
812 | @@ -89,6 +90,8 @@ |
813 | # to store as build results. |
814 | loop = build.loop.get_child_object() |
815 | values = loop.process_build_results(build) |
816 | + values["*console_url*"] = self.jenkins.get_build_output_url( |
817 | + build.loop.name, build.remote_number) |
818 | build.result_xml = DictToXml(values).dict_to_tree() |
819 | build.save() |
820 | |
821 | @@ -152,7 +155,15 @@ |
822 | |
823 | def schedule_job_build(self, jobname, parameters=None): |
824 | """Trigger build job on jenkins. Return the next build number.""" |
825 | - self.jenkins.build_job(jobname, parameters) |
826 | + |
827 | + # Note that python-jenkins uses urllib2 in such a way that new job |
828 | + # API requests will always be an HTTP GET. In recent versions of |
829 | + # Jenkins this is invalid. Using requests to work around this, but |
830 | + # using the python-jenkins library to generate the job URL. |
831 | + requests.post(self.jenkins.build_job_url(jobname), |
832 | + auth=(settings.JENKINS_ADMIN_USER, |
833 | + settings.JENKINS_ADMIN_PASSWD), |
834 | + params=parameters) |
835 | job_info = self.jenkins.get_job_info(jobname) |
836 | return job_info["nextBuildNumber"] |
837 | |
838 | |
839 | === modified file 'dashboard/lib/jenkins_dashboard.py' |
840 | --- dashboard/lib/jenkins_dashboard.py 2012-09-24 08:21:17 +0000 |
841 | +++ dashboard/lib/jenkins_dashboard.py 2013-02-26 12:48:40 +0000 |
842 | @@ -43,11 +43,14 @@ |
843 | raise jenkins.JenkinsException( |
844 | "Could not parse JSON info for job[%s]" % name) |
845 | |
846 | + def get_build_output_url(self, name, build_number): |
847 | + return self.server + self.BUILD_CONSOLE % locals() |
848 | + |
849 | def get_build_output(self, name, build_number): |
850 | |
851 | try: |
852 | response = self.jenkins_open( |
853 | - urllib2.Request(self.server + self.BUILD_CONSOLE % locals())) |
854 | + urllib2.Request(self.get_build_output_url(name, build_number))) |
855 | if response: |
856 | return response |
857 | else: |
858 | |
859 | === modified file 'dashboard/lib/xml_to_dict.py' |
860 | --- dashboard/lib/xml_to_dict.py 2012-09-07 12:48:04 +0000 |
861 | +++ dashboard/lib/xml_to_dict.py 2013-02-26 12:48:40 +0000 |
862 | @@ -48,7 +48,7 @@ |
863 | :param xml: The XML tree as a string. |
864 | :type xml str |
865 | """ |
866 | - if not isinstance(xml, str): |
867 | + if not (isinstance(xml, str) or isinstance(xml, unicode)): |
868 | raise XmlToDictException("Type of parameter is not 'str', " |
869 | "got '%s' instead." % type(xml).__name__) |
870 | super(XmlToDict, self).__init__() |
871 | |
872 | === modified file 'dashboard/settings.py' |
873 | --- dashboard/settings.py 2013-01-30 17:20:40 +0000 |
874 | +++ dashboard/settings.py 2013-02-26 12:48:40 +0000 |
875 | @@ -158,6 +158,7 @@ |
876 | 'frontend.android_textfield_loop', |
877 | 'frontend.hwpack_loop', |
878 | 'frontend.api', |
879 | + 'frontend.ci_job', |
880 | 'tastypie', |
881 | 'south', |
882 | # Uncomment the next line to enable admin documentation: |
883 | |
884 | === modified file 'dashboard/urls.py' |
885 | --- dashboard/urls.py 2013-01-21 16:16:03 +0000 |
886 | +++ dashboard/urls.py 2013-02-26 12:48:40 +0000 |
887 | @@ -40,4 +40,5 @@ |
888 | url(r'^', include('dashboard.frontend.android_textfield_loop.urls')), |
889 | url(r'^', include('dashboard.frontend.hwpack_loop.urls')), |
890 | url(r'^', include('dashboard.frontend.api.urls')), |
891 | + url(r'^', include('dashboard.frontend.ci_job.urls')), |
892 | ) |
893 | |
894 | === modified file 'requirements.txt' |
895 | --- requirements.txt 2013-02-01 11:39:17 +0000 |
896 | +++ requirements.txt 2013-02-26 12:48:40 +0000 |
897 | @@ -12,4 +12,5 @@ |
898 | python-jenkins==0.2 |
899 | python-openid==2.2.5 |
900 | requests==1.1.0 |
901 | +simplejson==3.0.7 |
902 | wsgiref==0.1.2 |
Hello James,
looks good to me. It took me a while to go through, also for fancy tastypie terminology for serializing objects.
Randomly, I have one error running the tests, but have no idea if it is related to Jenkins or something else. Last time I ran the tests I didn't have any.
Speaking of Jenkins, we should try to stick with what we will probably be using in ci.l.o, I guess we will be based on that.