~ubuntu-branches/debian/jessie/lava-server/jessie

« back to all changes in this revision

Viewing changes to dashboard_app/xmlrpc.py

  • Committer: Package Import Robot
  • Author(s): Neil Williams
  • Date: 2014-06-29 19:29:34 UTC
  • Revision ID: package-import@ubuntu.com-20140629192934-ue8hrzzpye9isevt
Tags: upstream-2014.05.30.09
ImportĀ upstreamĀ versionĀ 2014.05.30.09

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2010 Linaro Limited
 
2
#
 
3
# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
 
4
#
 
5
# This file is part of LAVA Dashboard
 
6
#
 
7
# Launch Control is free software: you can redistribute it and/or modify
 
8
# it under the terms of the GNU Affero General Public License version 3
 
9
# as published by the Free Software Foundation
 
10
#
 
11
# Launch Control is distributed in the hope that it will be useful,
 
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
# GNU General Public License for more details.
 
15
#
 
16
# You should have received a copy of the GNU Affero General Public License
 
17
# along with Launch Control.  If not, see <http://www.gnu.org/licenses/>.
 
18
 
 
19
"""
 
20
XMP-RPC API
 
21
"""
 
22
 
 
23
import datetime
 
24
import decimal
 
25
import logging
 
26
import re
 
27
import xmlrpclib
 
28
import hashlib
 
29
import json
 
30
import os
 
31
from django.contrib.auth.models import User, Group
 
32
from django.core.urlresolvers import reverse
 
33
from django.db import IntegrityError, DatabaseError
 
34
from linaro_django_xmlrpc.models import (
 
35
    ExposedAPI,
 
36
    Mapper,
 
37
    xml_rpc_signature,
 
38
)
 
39
 
 
40
from dashboard_app.filters import evaluate_filter
 
41
from dashboard_app.models import (
 
42
    Bundle,
 
43
    BundleStream,
 
44
    Test,
 
45
    TestRunFilter,
 
46
    TestDefinition,
 
47
)
 
48
from lava_scheduler_app.models import (
 
49
    TestJob,
 
50
)
 
51
 
 
52
 
 
53
class errors:
 
54
    """
 
55
    A namespace for error codes that may be returned by various XML-RPC
 
56
    methods. Where applicable existing status codes from HTTP protocol
 
57
    are reused
 
58
    """
 
59
    AUTH_FAILED = 100
 
60
    AUTH_BLOCKED = 101
 
61
    BAD_REQUEST = 400
 
62
    AUTH_REQUIRED = 401
 
63
    FORBIDDEN = 403
 
64
    NOT_FOUND = 404
 
65
    CONFLICT = 409
 
66
    INTERNAL_SERVER_ERROR = 500
 
67
    NOT_IMPLEMENTED = 501
 
68
 
 
69
 
 
70
class DashboardAPI(ExposedAPI):
 
71
    """
 
72
    Dashboard API object.
 
73
 
 
74
    All public methods are automatically exposed as XML-RPC methods
 
75
    """
 
76
 
 
77
    @xml_rpc_signature('str')
 
78
    def version(self):
 
79
        """
 
80
        Name
 
81
        ----
 
82
        `version` ()
 
83
 
 
84
        Description
 
85
        -----------
 
86
        Return dashboard server version. The version is a string with
 
87
        dots separating five components.
 
88
 
 
89
        The components are:
 
90
            1. major version
 
91
            2. minor version
 
92
            3. micro version
 
93
            4. release level
 
94
            5. serial
 
95
 
 
96
        See: http://docs.python.org/library/sys.html#sys.version_info
 
97
 
 
98
        Note that this version will change to reflect the new versioning
 
99
        scheme, based on git tags named after release dates instead of
 
100
        arbitrary major and minor versions, once the migration to packaging
 
101
         is complete.
 
102
 
 
103
        Return value
 
104
        -------------
 
105
        Server version string
 
106
        """
 
107
        return ".".join(map(str, (0, 29, 0, "final", 0)))
 
108
 
 
109
    def _put(self, content, content_filename, pathname):
 
110
        try:
 
111
            logging.debug("Getting bundle stream")
 
112
            if self.user and self.user.is_superuser:
 
113
                bundle_stream = BundleStream.objects.get(pathname=pathname)
 
114
            else:
 
115
                bundle_stream = BundleStream.objects.accessible_by_principal(self.user).get(pathname=pathname)
 
116
        except BundleStream.DoesNotExist:
 
117
            logging.debug("Bundle stream does not exist, aborting")
 
118
            raise xmlrpclib.Fault(errors.NOT_FOUND,
 
119
                                  "Bundle stream not found")
 
120
        if not bundle_stream.can_upload(self.user):
 
121
            raise xmlrpclib.Fault(
 
122
                errors.FORBIDDEN, "You cannot upload to this stream")
 
123
        try:
 
124
            logging.debug("Creating bundle object")
 
125
            bundle = Bundle.objects.create_with_content(bundle_stream, self.user, content_filename, content)
 
126
        except (IntegrityError, ValueError) as exc:
 
127
            logging.debug("Raising xmlrpclib.Fault(errors.CONFLICT)")
 
128
            raise xmlrpclib.Fault(errors.CONFLICT, str(exc))
 
129
        except:
 
130
            logging.exception("big oops")
 
131
            raise
 
132
        else:
 
133
            logging.debug("Deserializing bundle")
 
134
            bundle.deserialize()
 
135
            return bundle
 
136
 
 
137
    @xml_rpc_signature('str', 'str', 'str', 'str')
 
138
    def put(self, content, content_filename, pathname):
 
139
        """
 
140
        Name
 
141
        ----
 
142
        `put` (`content`, `content_filename`, `pathname`)
 
143
 
 
144
        Description
 
145
        -----------
 
146
        Upload a bundle to the server.
 
147
 
 
148
        Arguments
 
149
        ---------
 
150
        `content`: string
 
151
            Full text of the bundle. This *SHOULD* be a valid JSON
 
152
            document and it *SHOULD* match the "Dashboard Bundle Format
 
153
            1.0" schema. The SHA1 of the content *MUST* be unique or a
 
154
            ``Fault(409, "...")`` is raised. This is used to protect
 
155
            from simple duplicate submissions.
 
156
        `content_filename`: string
 
157
            Name of the file that contained the text of the bundle. The
 
158
            `content_filename` can be an arbitrary string and will be
 
159
            stored along with the content for reference.
 
160
        `pathname`: string
 
161
            Pathname of the bundle stream where a new bundle should
 
162
            be created and stored. This argument *MUST* designate a
 
163
            pre-existing bundle stream or a ``Fault(404, "...")`` exception
 
164
            is raised. In addition the user *MUST* have access
 
165
            permission to upload bundles there or a ``Fault(403, "...")``
 
166
            exception is raised. See below for access rules.
 
167
 
 
168
        Return value
 
169
        ------------
 
170
        If all goes well this function returns the SHA1 of the content.
 
171
 
 
172
        Exceptions raised
 
173
        -----------------
 
174
        404
 
175
            Either:
 
176
 
 
177
                - Bundle stream not found
 
178
                - Uploading to specified stream is not permitted
 
179
        409
 
180
            Duplicate bundle content
 
181
 
 
182
        Rules for bundle stream access
 
183
        ------------------------------
 
184
        The following rules govern bundle stream upload access rights:
 
185
            - all anonymous streams are accessible
 
186
            - personal streams are accessible to owners
 
187
            - team streams are accessible to team members
 
188
 
 
189
        """
 
190
        bundle = self._put(content, content_filename, pathname)
 
191
        logging.debug("Returning bundle SHA1")
 
192
        return bundle.content_sha1
 
193
 
 
194
    @xml_rpc_signature('str', 'str', 'str', 'str')
 
195
    def put_ex(self, content, content_filename, pathname):
 
196
        """
 
197
        Name
 
198
        ----
 
199
        `put` (`content`, `content_filename`, `pathname`)
 
200
 
 
201
        Description
 
202
        -----------
 
203
        Upload a bundle to the server.  A variant on put_ex that returns the
 
204
        URL of the bundle instead of its SHA1.
 
205
 
 
206
        Arguments
 
207
        ---------
 
208
        `content`: string
 
209
            Full text of the bundle. This *SHOULD* be a valid JSON
 
210
            document and it *SHOULD* match the "Dashboard Bundle Format
 
211
            1.0" schema. The SHA1 of the content *MUST* be unique or a
 
212
            ``Fault(409, "...")`` is raised. This is used to protect
 
213
            from simple duplicate submissions.
 
214
        `content_filename`: string
 
215
            Name of the file that contained the text of the bundle. The
 
216
            `content_filename` can be an arbitrary string and will be
 
217
            stored along with the content for reference.
 
218
        `pathname`: string
 
219
            Pathname of the bundle stream where a new bundle should
 
220
            be created and stored. This argument *MUST* designate a
 
221
            pre-existing bundle stream or a ``Fault(404, "...")`` exception
 
222
            is raised. In addition the user *MUST* have access
 
223
            permission to upload bundles there or a ``Fault(403, "...")``
 
224
            exception is raised. See below for access rules.
 
225
 
 
226
        Return value
 
227
        ------------
 
228
        If all goes well this function returns the full URL of the bundle.
 
229
 
 
230
        Exceptions raised
 
231
        -----------------
 
232
        404
 
233
            Either:
 
234
 
 
235
                - Bundle stream not found
 
236
                - Uploading to specified stream is not permitted
 
237
        409
 
238
            Duplicate bundle content
 
239
 
 
240
        Rules for bundle stream access
 
241
        ------------------------------
 
242
        The following rules govern bundle stream upload access rights:
 
243
            - all anonymous streams are accessible
 
244
            - personal streams are accessible to owners
 
245
            - team streams are accessible to team members
 
246
 
 
247
        """
 
248
        bundle = self._put(content, content_filename, pathname)
 
249
        logging.debug("Returning permalink to bundle")
 
250
        return self._context.request.build_absolute_uri(
 
251
            reverse('dashboard_app.views.redirect_to_bundle',
 
252
                    kwargs={'content_sha1': bundle.content_sha1}))
 
253
 
 
254
    def put_pending(self, content, pathname, group_name):
 
255
        """
 
256
        Name
 
257
        ----
 
258
        `put_pending` (`content`, `pathname`, `group_name`)
 
259
 
 
260
        Description
 
261
        -----------
 
262
        MultiNode internal call.
 
263
 
 
264
        Stores the bundle until the coordinator allows the complete
 
265
        bundle list to be aggregated from the list and submitted by put_group
 
266
 
 
267
        Arguments
 
268
        ---------
 
269
        `content`: string
 
270
            Full text of the bundle. This *MUST* be a valid JSON
 
271
            document and it *SHOULD* match the "Dashboard Bundle Format
 
272
            1.0" schema. The SHA1 of the content *MUST* be unique or a
 
273
            ``Fault(409, "...")`` is raised. This is used to protect
 
274
            from simple duplicate submissions.
 
275
        `pathname`: string
 
276
            Pathname of the bundle stream where a new bundle should
 
277
            be created and stored. This argument *MUST* designate a
 
278
            pre-existing bundle stream or a ``Fault(404, "...")`` exception
 
279
            is raised. In addition the user *MUST* have access
 
280
            permission to upload bundles there or a ``Fault(403, "...")``
 
281
            exception is raised. See below for access rules.
 
282
        `group_name`: string
 
283
            Unique ID of the MultiNode group. Other pending bundles will
 
284
            be aggregated into a single result bundle for this group.
 
285
 
 
286
        Return value
 
287
        ------------
 
288
        If all goes well this function returns the SHA1 of the content.
 
289
 
 
290
        Exceptions raised
 
291
        -----------------
 
292
        404
 
293
            Either:
 
294
 
 
295
                - Bundle stream not found
 
296
                - Uploading to specified stream is not permitted
 
297
        409
 
298
            Duplicate bundle content
 
299
 
 
300
        Rules for bundle stream access
 
301
        ------------------------------
 
302
        The following rules govern bundle stream upload access rights:
 
303
            - all anonymous streams are accessible
 
304
            - personal streams are accessible to owners
 
305
            - team streams are accessible to team members
 
306
 
 
307
        """
 
308
        try:
 
309
            logging.debug("Getting bundle stream")
 
310
            if self.user.is_superuser:
 
311
                bundle_stream = BundleStream.objects.get(pathname=pathname)
 
312
            else:
 
313
                bundle_stream = BundleStream.objects.accessible_by_principal(self.user).get(pathname=pathname)
 
314
        except BundleStream.DoesNotExist:
 
315
            logging.debug("Bundle stream does not exist, aborting")
 
316
            raise xmlrpclib.Fault(errors.NOT_FOUND,
 
317
                                  "Bundle stream not found")
 
318
        if not bundle_stream.can_upload(self.user):
 
319
            raise xmlrpclib.Fault(
 
320
                errors.FORBIDDEN, "You cannot upload to this stream")
 
321
        try:
 
322
            # add this to a list which put_group can use.
 
323
            sha1 = hashlib.sha1()
 
324
            sha1.update(content)
 
325
            hexdigest = sha1.hexdigest()
 
326
            groupfile = "/tmp/%s" % group_name
 
327
            with open(groupfile, "a+") as grp_file:
 
328
                grp_file.write("%s\n" % content)
 
329
            return hexdigest
 
330
        except Exception as e:
 
331
            logging.debug("Dashboard pending submission caused an exception: %s" % e)
 
332
 
 
333
    def put_group(self, content, content_filename, pathname, group_name):
 
334
        """
 
335
        Name
 
336
        ----
 
337
        `put_group` (`content`, `content_filename`, `pathname`, `group_name`)
 
338
 
 
339
        Description
 
340
        -----------
 
341
        MultiNode internal call.
 
342
 
 
343
        Adds the final bundle to the list, aggregates the list
 
344
        into a single group bundle and submits the group bundle.
 
345
 
 
346
        Arguments
 
347
        ---------
 
348
        `content`: string
 
349
            Full text of the bundle. This *MUST* be a valid JSON
 
350
            document and it *SHOULD* match the "Dashboard Bundle Format
 
351
            1.0" schema. The SHA1 of the content *MUST* be unique or a
 
352
            ``Fault(409, "...")`` is raised. This is used to protect
 
353
            from simple duplicate submissions.
 
354
        `content_filename`: string
 
355
            Name of the file that contained the text of the bundle. The
 
356
            `content_filename` can be an arbitrary string and will be
 
357
            stored along with the content for reference.
 
358
        `pathname`: string
 
359
            Pathname of the bundle stream where a new bundle should
 
360
            be created and stored. This argument *MUST* designate a
 
361
            pre-existing bundle stream or a ``Fault(404, "...")`` exception
 
362
            is raised. In addition the user *MUST* have access
 
363
            permission to upload bundles there or a ``Fault(403, "...")``
 
364
            exception is raised. See below for access rules.
 
365
        `group_name`: string
 
366
            Unique ID of the MultiNode group. Other pending bundles will
 
367
            be aggregated into a single result bundle for this group. At
 
368
            least one other bundle must have already been submitted as
 
369
            pending for the specified MultiNode group. LAVA Coordinator
 
370
            causes the parent job to wait until all nodes have been marked
 
371
            as having pending bundles, even if some bundles are empty.
 
372
 
 
373
        Return value
 
374
        ------------
 
375
        If all goes well this function returns the full URL of the bundle.
 
376
 
 
377
        Exceptions raised
 
378
        -----------------
 
379
        ValueError:
 
380
            One or more bundles could not be converted to JSON prior
 
381
            to aggregation.
 
382
        404
 
383
            Either:
 
384
 
 
385
                - Bundle stream not found
 
386
                - Uploading to specified stream is not permitted
 
387
        409
 
388
            Duplicate bundle content
 
389
 
 
390
        Rules for bundle stream access
 
391
        ------------------------------
 
392
        The following rules govern bundle stream upload access rights:
 
393
            - all anonymous streams are accessible
 
394
            - personal streams are accessible to owners
 
395
            - team streams are accessible to team members
 
396
 
 
397
        """
 
398
        grp_file = "/tmp/%s" % group_name
 
399
        bundle_set = {}
 
400
        bundle_set[group_name] = []
 
401
        if os.path.isfile(grp_file):
 
402
            with open(grp_file, "r") as grp_data:
 
403
                grp_list = grp_data.readlines()
 
404
            for testrun in grp_list:
 
405
                bundle_set[group_name].append(json.loads(testrun))
 
406
        # Note: now that we have the data from the group, the group data file could be re-used
 
407
        # as an error log which is simpler than debugging through XMLRPC.
 
408
        else:
 
409
            raise ValueError("Aggregation failure for %s - check coordinator rpc_delay?" % group_name)
 
410
        group_tests = []
 
411
        try:
 
412
            json_data = json.loads(content)
 
413
        except ValueError:
 
414
            logging.debug("Invalid JSON content within the sub_id zero bundle")
 
415
            json_data = None
 
416
        try:
 
417
            bundle_set[group_name].append(json_data)
 
418
        except Exception as e:
 
419
            logging.debug("appending JSON caused exception %s" % e)
 
420
        try:
 
421
            for bundle_list in bundle_set[group_name]:
 
422
                for test_run in bundle_list['test_runs']:
 
423
                    group_tests.append(test_run)
 
424
        except Exception as e:
 
425
            logging.debug("aggregating bundles caused exception %s" % e)
 
426
        group_content = json.dumps({"test_runs": group_tests, "format": json_data['format']})
 
427
        bundle = self._put(group_content, content_filename, pathname)
 
428
        logging.debug("Returning permalink to aggregated bundle for %s" % group_name)
 
429
        permalink = self._context.request.build_absolute_uri(
 
430
            reverse('dashboard_app.views.redirect_to_bundle',
 
431
                    kwargs={'content_sha1': bundle.content_sha1}))
 
432
        # only delete the group file when things go well.
 
433
        if os.path.isfile(grp_file):
 
434
            os.remove(grp_file)
 
435
        return permalink
 
436
 
 
437
    def get(self, content_sha1):
 
438
        """
 
439
        Name
 
440
        ----
 
441
        `get` (`content_sha1`)
 
442
 
 
443
        Description
 
444
        -----------
 
445
        Download a bundle from the server.
 
446
 
 
447
        Arguments
 
448
        ---------
 
449
        `content_sha1`: string
 
450
            SHA1 hash of the content of the bundle to download. This
 
451
            *MUST* designate an bundle or ``Fault(404, "...")`` is raised.
 
452
 
 
453
        Return value
 
454
        ------------
 
455
        This function returns an XML-RPC struct with the following fields:
 
456
 
 
457
        `content_filename`: string
 
458
            The value that was stored on a previous call to put()
 
459
        `content`: string
 
460
            The full text of the bundle
 
461
 
 
462
        Exceptions raised
 
463
        -----------------
 
464
        - 404 Bundle not found
 
465
        - 403 Permission denied
 
466
 
 
467
        Rules for bundle stream access
 
468
        ------------------------------
 
469
        The following rules govern bundle stream download access rights:
 
470
            - all anonymous streams are accessible
 
471
            - personal streams are accessible to owners
 
472
            - team streams are accessible to team members
 
473
        """
 
474
        try:
 
475
            bundle = Bundle.objects.get(content_sha1=content_sha1)
 
476
            if not bundle.bundle_stream.is_accessible_by(self.user):
 
477
                raise xmlrpclib.Fault(
 
478
                    403, "Permission denied.  User does not have permissions "
 
479
                    "to access this bundle.")
 
480
        except Bundle.DoesNotExist:
 
481
            raise xmlrpclib.Fault(errors.NOT_FOUND,
 
482
                                  "Bundle not found")
 
483
        else:
 
484
            return {"content": bundle.content.read(),
 
485
                    "content_filename": bundle.content_filename}
 
486
 
 
487
    @xml_rpc_signature('struct')
 
488
    def streams(self):
 
489
        """
 
490
        Name
 
491
        ----
 
492
        `streams` ()
 
493
 
 
494
        Description
 
495
        -----------
 
496
        List all bundle streams that the user has access to
 
497
 
 
498
        Arguments
 
499
        ---------
 
500
        None
 
501
 
 
502
        Return value
 
503
        ------------
 
504
        This function returns an XML-RPC array of XML-RPC structs with
 
505
        the following fields:
 
506
 
 
507
        `pathname`: string
 
508
            The pathname of the bundle stream
 
509
        `name`: string
 
510
            The user-configurable name of the bundle stream
 
511
        `user`: string
 
512
            The username of the owner of the bundle stream for personal
 
513
            streams or an empty string for public and team streams.
 
514
        `group`: string
 
515
            The name of the team that owsn the bundle stream for team
 
516
            streams or an empty string for public and personal streams.
 
517
        `bundle_count`: int
 
518
            Number of bundles that are in this stream
 
519
 
 
520
        Exceptions raised
 
521
        -----------------
 
522
        None
 
523
 
 
524
        Rules for bundle stream access
 
525
        ------------------------------
 
526
        The following rules govern bundle stream download access rights:
 
527
            - all anonymous streams are accessible
 
528
            - personal streams are accessible to owners
 
529
            - team streams are accessible to team members
 
530
        """
 
531
        if self.user and self.user.is_superuser:
 
532
            bundle_streams = BundleStream.objects.all()
 
533
        else:
 
534
            bundle_streams = BundleStream.objects.accessible_by_principal(
 
535
                self.user)
 
536
        return [
 
537
            {
 
538
                'pathname': bundle_stream.pathname,
 
539
                'name': bundle_stream.name,
 
540
                'user': bundle_stream.user.username if bundle_stream.user else "",
 
541
                'group': bundle_stream.group.name if bundle_stream.group else "",
 
542
                'bundle_count': bundle_stream.bundles.count(),
 
543
            }
 
544
            for bundle_stream in bundle_streams
 
545
        ]
 
546
 
 
547
    def bundles(self, pathname):
 
548
        """
 
549
        Name
 
550
        ----
 
551
        `bundles` (`pathname`)
 
552
 
 
553
        Description
 
554
        -----------
 
555
        List all bundles in a specified bundle stream
 
556
 
 
557
        Arguments
 
558
        ---------
 
559
        `pathname`: string
 
560
            The pathname of the bundle stream to query. This argument
 
561
            *MUST* designate an existing stream or Fault(404, "...") is
 
562
            raised. The user *MUST* have access to this stream or
 
563
            Fault(403, "...") is raised.
 
564
 
 
565
        Return value
 
566
        ------------
 
567
        This function returns an XML-RPC array of XML-RPC structs with
 
568
        the following fields:
 
569
 
 
570
        `uploaded_by`: string
 
571
            The username of the user that uploaded this bundle or
 
572
            empty string if this bundle was uploaded anonymously.
 
573
        `uploaded_on`: datetime
 
574
            The timestamp when the bundle was uploaded
 
575
        `content_filename`: string
 
576
            The filename of the original bundle file
 
577
        `content_sha1`: string
 
578
            The SHA1 hash if the content of the bundle
 
579
        `content_size`: int
 
580
            The size of the content
 
581
        `is_deserialized`: bool
 
582
            True if the bundle was de-serialized successfully, false otherwise
 
583
        `associated_job`: int
 
584
            The job with which this bundle is associated
 
585
 
 
586
 
 
587
        Exceptions raised
 
588
        -----------------
 
589
        404
 
590
            Either:
 
591
 
 
592
                - Bundle stream not found
 
593
                - Listing bundles in this bundle stream is not permitted
 
594
 
 
595
        Rules for bundle stream access
 
596
        ------------------------------
 
597
        The following rules govern bundle stream download access rights:
 
598
            - all anonymous streams are accessible
 
599
            - personal streams are accessible to owners
 
600
            - team streams are accessible to team members
 
601
        """
 
602
        bundles = []
 
603
        try:
 
604
            if self.user and self.user.is_superuser:
 
605
                bundle_stream = BundleStream.objects.get(pathname=pathname)
 
606
            else:
 
607
                bundle_stream = BundleStream.objects.accessible_by_principal(self.user).get(pathname=pathname)
 
608
            for bundle in bundle_stream.bundles.all().order_by("uploaded_on"):
 
609
                job_id = 'NA'
 
610
                try:
 
611
                    job = TestJob.objects.get(_results_bundle=bundle)
 
612
                    job_id = job.id
 
613
                except TestJob.DoesNotExist:
 
614
                    job_id = 'NA'
 
615
                bundles.append({
 
616
                    'uploaded_by': bundle.uploaded_by.username if bundle.uploaded_by else "",
 
617
                    'uploaded_on': bundle.uploaded_on,
 
618
                    'content_filename': bundle.content_filename,
 
619
                    'content_sha1': bundle.content_sha1,
 
620
                    'content_size': bundle.content.size,
 
621
                    'is_deserialized': bundle.is_deserialized,
 
622
                    'associated_job': job_id
 
623
                })
 
624
        except BundleStream.DoesNotExist:
 
625
            raise xmlrpclib.Fault(errors.NOT_FOUND, "Bundle stream not found")
 
626
        return bundles
 
627
 
 
628
    @xml_rpc_signature('str')
 
629
    def get_test_names(self, device_type=None):
 
630
        """
 
631
        Name
 
632
        ----
 
633
        `get_test_names` ([`device_type`]])
 
634
 
 
635
        Description
 
636
        -----------
 
637
        Get the name of all the tests that have run on a particular device type.
 
638
 
 
639
        Arguments
 
640
        ---------
 
641
        `device_type`: string
 
642
            The type of device the retrieved test names should apply to.
 
643
 
 
644
        Return value
 
645
        ------------
 
646
        This function returns an XML-RPC array of test names.
 
647
        """
 
648
        test_names = []
 
649
        if device_type:
 
650
            for test in Test.objects.filter(
 
651
                    test_runs__attributes__name='target.device_type',
 
652
                    test_runs__attributes__value=device_type).distinct():
 
653
                test_names.append(test.test_id)
 
654
        else:
 
655
            for test in Test.objects.all():
 
656
                test_names.append(test.test_id)
 
657
        return test_names
 
658
 
 
659
    def deserialize(self, content_sha1):
 
660
        """
 
661
        Name
 
662
        ----
 
663
        `deserialize` (`content_sha1`)
 
664
 
 
665
        Description
 
666
        -----------
 
667
        Deserialize bundle on the server
 
668
 
 
669
        Arguments
 
670
        ---------
 
671
        `content_sha1`: string
 
672
            SHA1 hash of the content of the bundle to download. This
 
673
            *MUST* designate an bundle or ``Fault(404, "...")`` is raised.
 
674
 
 
675
        Return value
 
676
        ------------
 
677
        True - deserialization okay
 
678
        False - deserialization not needed
 
679
 
 
680
        Exceptions raised
 
681
        -----------------
 
682
        404
 
683
            Bundle not found
 
684
        409
 
685
            Bundle import failed
 
686
        """
 
687
        try:
 
688
            bundle = Bundle.objects.get(content_sha1=content_sha1)
 
689
        except Bundle.DoesNotExist:
 
690
            raise xmlrpclib.Fault(errors.NOT_FOUND, "Bundle not found")
 
691
        if bundle.is_deserialized:
 
692
            return False
 
693
        bundle.deserialize()
 
694
        if bundle.is_deserialized is False:
 
695
            raise xmlrpclib.Fault(
 
696
                errors.CONFLICT,
 
697
                bundle.deserialization_error.error_message)
 
698
        return True
 
699
 
 
700
    def make_stream(self, pathname, name):
 
701
        """
 
702
        Name
 
703
        ----
 
704
        `make_stream` (`pathname`, `name`)
 
705
 
 
706
        Description
 
707
        -----------
 
708
        Create a bundle stream with the specified pathname
 
709
 
 
710
        Arguments
 
711
        ---------
 
712
        `pathname`: string
 
713
            The pathname must refer to an anonymous stream
 
714
        `name`: string
 
715
            The name of the stream (free form description text)
 
716
 
 
717
        Return value
 
718
        ------------
 
719
        pathname is returned
 
720
 
 
721
        Exceptions raised
 
722
        -----------------
 
723
        403
 
724
            Pathname does not designate an anonymous stream
 
725
        409
 
726
            Bundle stream with the specified pathname already exists
 
727
 
 
728
        Available Since
 
729
        ---------------
 
730
        0.3
 
731
        """
 
732
        # Work around bug https://bugs.launchpad.net/lava-dashboard/+bug/771182
 
733
        # Older clients would send None as the name and this would trigger an
 
734
        # IntegrityError to be raised by BundleStream.objects.create() below
 
735
        # which in turn would be captured by the fault handler and reported as
 
736
        # an unrelated issue to the user. Let's work around that by using an
 
737
        # empty string instead.
 
738
        if name is None:
 
739
            name = ""
 
740
        try:
 
741
            user_name, group_name, slug, is_public, is_anonymous = BundleStream.parse_pathname(pathname)
 
742
        except ValueError as ex:
 
743
            raise xmlrpclib.Fault(errors.FORBIDDEN, str(ex))
 
744
 
 
745
        # Start with those to simplify the logic below
 
746
        user = None
 
747
        group = None
 
748
        if is_anonymous is False:
 
749
            if self.user is not None:
 
750
                assert is_anonymous is False
 
751
                assert self.user is not None
 
752
                if user_name is not None:
 
753
                    if not self.user.is_superuser:
 
754
                        if user_name != self.user.username:
 
755
                            raise xmlrpclib.Fault(
 
756
                                errors.FORBIDDEN,
 
757
                                "Only user {user!r} could create this stream".format(user=user_name))
 
758
                    user = self.user  # map to real user object
 
759
                elif group_name is not None:
 
760
                    try:
 
761
                        if self.user.is_superuser:
 
762
                            group = Group.objects.get(name=group_name)
 
763
                        else:
 
764
                            group = self.user.groups.get(name=group_name)
 
765
                    except Group.DoesNotExist:
 
766
                        raise xmlrpclib.Fault(
 
767
                            errors.FORBIDDEN,
 
768
                            "Only a member of group {group!r} could create this stream".format(group=group_name))
 
769
            else:
 
770
                assert is_anonymous is False
 
771
                assert self.user is None
 
772
                raise xmlrpclib.Fault(
 
773
                    errors.FORBIDDEN, "Only anonymous streams can be constructed (you are not signed in)")
 
774
        else:
 
775
            assert is_anonymous is True
 
776
            assert user_name is None
 
777
            assert group_name is None
 
778
            if self.user is not None:
 
779
                user = self.user
 
780
            else:
 
781
                # Hacky but will suffice for now
 
782
                user = User.objects.get_or_create(username="anonymous-owner")[0]
 
783
        try:
 
784
            bundle_stream = BundleStream.objects.create(
 
785
                user=user,
 
786
                group=group,
 
787
                slug=slug,
 
788
                is_public=is_public,
 
789
                is_anonymous=is_anonymous,
 
790
                name=name)
 
791
        except IntegrityError:
 
792
            raise xmlrpclib.Fault(
 
793
                errors.CONFLICT,
 
794
                "Stream with the specified pathname already exists")
 
795
        else:
 
796
            return bundle_stream.pathname
 
797
 
 
798
    def _get_filter_data(self, filter_name):
 
799
        match = re.match("~([-_A-Za-z0-9]+)/([-_A-Za-z0-9]+)", filter_name)
 
800
        if not match:
 
801
            raise xmlrpclib.Fault(errors.BAD_REQUEST, "filter_name must be of form ~owner/filter-name")
 
802
        owner_name, filter_name = match.groups()
 
803
        try:
 
804
            owner = User.objects.get(username=owner_name)
 
805
        except User.NotFound:
 
806
            raise xmlrpclib.Fault(errors.NOT_FOUND, "user %s not found" % owner_name)
 
807
        try:
 
808
            filter = TestRunFilter.objects.get(owner=owner, name=filter_name)
 
809
        except TestRunFilter.NotFound:
 
810
            raise xmlrpclib.Fault(errors.NOT_FOUND, "filter %s not found" % filter_name)
 
811
        if not filter.public and self.user != owner:
 
812
            if self.user:
 
813
                raise xmlrpclib.Fault(
 
814
                    errors.FORBIDDEN, "forbidden")
 
815
            else:
 
816
                raise xmlrpclib.Fault(
 
817
                    errors.AUTH_REQUIRED, "authentication required")
 
818
        return filter.as_data()
 
819
 
 
820
    def get_filter_results(self, filter_name, count=10, offset=0):
 
821
        """
 
822
        Name
 
823
        ----
 
824
         ::
 
825
 
 
826
          get_filter_results(filter_name, count=10, offset=0)
 
827
 
 
828
        Description
 
829
        -----------
 
830
 
 
831
        Return information about the test runs and results that a given filter
 
832
        matches.
 
833
 
 
834
        Arguments
 
835
        ---------
 
836
 
 
837
        ``filter_name``:
 
838
           The name of a filter in the format ~owner/name.
 
839
        ``count``:
 
840
           The maximum number of matches to return.
 
841
        ``offset``:
 
842
           Skip over this many results.
 
843
 
 
844
        Return value
 
845
        ------------
 
846
 
 
847
        A list of "filter matches".  A filter match describes the results of
 
848
        matching a filter against one or more test runs::
 
849
 
 
850
          {
 
851
            'tag': either a stringified date (bundle__uploaded_on) or a build number
 
852
            'test_runs': [{
 
853
                'test_id': test_id
 
854
                'link': link-to-test-run,
 
855
                'passes': int, 'fails': int, 'skips': int, 'total': int,
 
856
                # only present if filter specifies cases for this test:
 
857
                'specific_results': [{
 
858
                    'test_case_id': test_case_id,
 
859
                    'link': link-to-test-result,
 
860
                    'result': pass/fail/skip/unknown,
 
861
                    'measurement': string-containing-decimal-or-None,
 
862
                    'units': units,
 
863
                    }],
 
864
                }]
 
865
            # Only present if filter does not specify tests:
 
866
            'pass_count': int,
 
867
            'fail_count': int,
 
868
          }
 
869
 
 
870
        """
 
871
        filter_data = self._get_filter_data(filter_name)
 
872
        matches = evaluate_filter(self.user, filter_data, descending=False)
 
873
        matches = matches[offset:offset + count]
 
874
        return [match.serializable() for match in matches]
 
875
 
 
876
    def get_filter_results_since(self, filter_name, since=None):
 
877
        """
 
878
        Name
 
879
        ----
 
880
         ::
 
881
 
 
882
          get_filter_results_since(filter_name, since=None)
 
883
 
 
884
        Description
 
885
        -----------
 
886
 
 
887
        Return information about the test runs and results that a given filter
 
888
        matches that are more recent than a previous match -- in more detail,
 
889
        results where the ``tag`` is greater than the value passed in
 
890
        ``since``.
 
891
 
 
892
        The idea of this method is that it will be called from a cron job to
 
893
        update previously accessed results.  Something like this::
 
894
 
 
895
           previous_results = json.load(open('results.json'))
 
896
           results = previous_results + server.dashboard.get_filter_results_since(
 
897
              filter_name, previous_results[-1]['tag'])
 
898
           ... do things with results ...
 
899
           json.save(results, open('results.json', 'w'))
 
900
 
 
901
        If called without passing ``since`` (or with ``since`` set to
 
902
        ``None``), this method returns up to 100 matches from the filter.  In
 
903
        fact, the matches are always capped at 100 -- so set your cronjob to
 
904
        execute frequently enough that there are less than 100 matches
 
905
        generated between calls!
 
906
 
 
907
        Arguments
 
908
        ---------
 
909
 
 
910
        ``filter_name``:
 
911
           The name of a filter in the format ~owner/name.
 
912
        ``since``:
 
913
           The 'tag' of the most recent result that was retrieved from this
 
914
           filter.
 
915
 
 
916
        Return value
 
917
        ------------
 
918
 
 
919
        A list of "filter matches".  A filter match describes the results of
 
920
        matching a filter against one or more test runs::
 
921
 
 
922
          {
 
923
            'tag': either a stringified date (bundle__uploaded_on) or a build number
 
924
            'test_runs': [{
 
925
                'test_id': test_id
 
926
                'link': link-to-test-run,
 
927
                'passes': int, 'fails': int, 'skips': int, 'total': int,
 
928
                # only present if filter specifies cases for this test:
 
929
                'specific_results': [{
 
930
                    'test_case_id': test_case_id,
 
931
                    'link': link-to-test-result,
 
932
                    'result': pass/fail/skip/unknown,
 
933
                    'measurement': string-containing-decimal-or-None,
 
934
                    'units': units,
 
935
                    }],
 
936
                }]
 
937
            # Only present if filter does not specify tests:
 
938
            'pass_count': int,
 
939
            'fail_count': int,
 
940
          }
 
941
 
 
942
        """
 
943
        filter_data = self._get_filter_data(filter_name)
 
944
        matches = evaluate_filter(self.user, filter_data, descending=False)
 
945
        if since is not None:
 
946
            if filter_data.get('build_number_attribute') is not None:
 
947
                try:
 
948
                    since = datetime.datetime.strptime(since, "%Y-%m-%d %H:%M:%S.%f")
 
949
                except ValueError:
 
950
                    raise xmlrpclib.Fault(
 
951
                        errors.BAD_REQUEST, "cannot parse since argument as datetime")
 
952
            matches = matches.since(since)
 
953
        matches = matches[:100]
 
954
        return [match.serializable() for match in matches]
 
955
 
 
956
# Mapper used by the legacy URL
 
957
legacy_mapper = Mapper()
 
958
legacy_mapper.register_introspection_methods()
 
959
legacy_mapper.register(DashboardAPI, '')