Completed
Push — master ( cc0c00...971d08 )
by Dieter
01:03
created

buildtimetrend.get_dashboard_config()   A

Complexity

Conditions 1

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 17
rs 9.4286
1
# vim: set expandtab sw=4 ts=4:
2
"""
3
Interface to Keen IO.
4
5
Copyright (C) 2014-2015 Dieter Adriaenssens <[email protected]>
6
7
This file is part of buildtimetrend/python-lib
8
<https://github.com/buildtimetrend/python-lib/>
9
10
This program is free software: you can redistribute it and/or modify
11
it under the terms of the GNU Affero General Public License as published by
12
the Free Software Foundation, either version 3 of the License, or
13
any later version.
14
15
This program is distributed in the hope that it will be useful,
16
but WITHOUT ANY WARRANTY; without even the implied warranty of
17
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
GNU Affero General Public License for more details.
19
20
You should have received a copy of the GNU Affero General Public License
21
along with this program. If not, see <http://www.gnu.org/licenses/>.
22
"""
23
24
from __future__ import division
25
from builtins import str
26
import os
27
from buildtimetrend import logger
28
import copy
29
import keen
30
import requests
31
from keen import scoped_keys
32
from buildtimetrend.settings import Settings
33
from buildtimetrend.tools import check_file
34
from buildtimetrend.tools import check_dict
35
from buildtimetrend.tools import is_list
36
from buildtimetrend.tools import is_string
37
38
39
TIME_INTERVALS = {
40
    'week': {'name': 'week', 'timeframe': 'this_7_days', 'max_age': 600},
41
    'month': {'name': 'month', 'timeframe': 'this_30_days', 'max_age': 600},
42
    'year': {'name': 'year', 'timeframe': 'this_52_weeks', 'max_age': 1800}
43
}
44
KEEN_PROJECT_INFO_NAME = "buildtime_trend"
45
46
47
def keen_has_project_id():
48
    """Check if Keen.io project ID is set."""
49
    if "KEEN_PROJECT_ID" in os.environ or keen.project_id is not None:
50
        return True
51
52
    logger.warning("Keen.io Project ID is not set")
53
    return False
54
55
56
def keen_has_master_key():
57
    """Check if Keen.io Master API key is set."""
58
    if "KEEN_MASTER_KEY" in os.environ or keen.master_key is not None:
59
        return True
60
61
    logger.warning("Keen.io Master API Key is not set")
62
    return False
63
64
65
def keen_has_write_key():
66
    """Check if Keen.io Write Key is set."""
67
    if "KEEN_WRITE_KEY" in os.environ or keen.write_key is not None:
68
        return True
69
70
    logger.warning("Keen.io Write Key is not set")
71
    return False
72
73
74
def keen_has_read_key():
75
    """Check if Keen.io Read key is set."""
76
    if "KEEN_READ_KEY" in os.environ or keen.read_key is not None:
77
        return True
78
79
    logger.warning("Keen.io Read Key is not set")
80
    return False
81
82
83
def keen_is_writable():
84
    """Check if login keys for Keen IO API are set, to allow writing."""
85
    if keen_has_project_id() and keen_has_write_key():
86
        return True
87
88
    logger.warning("Keen.io Write Key is not set")
89
    return False
90
91
92
def keen_is_readable():
93
    """Check if login keys for Keen IO API are set, to allow reading."""
94
    if keen_has_project_id() and keen_has_read_key():
95
        return True
96
97
    logger.warning("Keen.io Read Key is not set")
98
    return False
99
100
101
def keen_io_generate_read_key(repo):
102
    """
103
    Create scoped key for reading only the build-stages related data.
104
105
    Param repo : github repository slug (fe. buildtimetrend/python-lib)
106
    """
107
    if not keen_has_master_key():
108
        logger.warning("Keen.io Read Key was not created,"
109
                       " keen.master_key is not defined.")
110
        return None
111
112
    master_key = keen.master_key or os.environ.get("KEEN_MASTER_KEY")
113
114
    privileges = {
115
        "allowed_operations": ["read"]
116
    }
117
118
    if repo is not None:
119
        privileges["filters"] = [get_repo_filter(repo)]
120
121
    logger.info("Keen.io Read Key is created for %s", repo)
122
    return scoped_keys.encrypt(master_key, privileges)
123
124
125
def keen_io_generate_write_key():
126
    """Create scoped key for write access to Keen.io database."""
127
    if not keen_has_master_key():
128
        logger.warning("Keen.io Write Key was not created,"
129
                       " keen.master_key is not defined.")
130
        return None
131
132
    master_key = keen.master_key or os.environ.get("KEEN_MASTER_KEY")
133
134
    privileges = {
135
        "allowed_operations": ["write"]
136
    }
137
138
    logger.info("Keen.io Write Key is created")
139
    return scoped_keys.encrypt(master_key, privileges)
140
141
142
def send_build_data(build):
143
    """Send build data generated by client to keen.io."""
144
    if keen_is_writable():
145
        logger.info("Sending client build data to Keen.io")
146
        keen_add_event("build_jobs", {"job": build.to_dict()})
147
        keen_add_events("build_stages", build.stages_to_list())
148
149
150
def send_build_data_service(build):
151
    """Send build data generated by service to keen.io."""
152
    if keen_is_writable():
153
        logger.info("Sending service build data to Keen.io")
154
        keen_add_event("build_jobs", {"job": build.to_dict()})
155
        keen_add_events("build_substages", build.stages_to_list())
156
157
158
def keen_add_event(event_collection, payload):
159
    """
160
    Wrapper for keen.add_event(), adds project info.
161
162
    Param event_collection : collection event data is submitted to
163
    Param payload : data that is submitted
164
    """
165
    # add project info to this event
166
    payload = add_project_info_dict(payload)
167
168
    # submit list of events to Keen.io
169
    keen.add_event(event_collection, payload)
170
171
172
def keen_add_events(event_collection, payload):
173
    """
174
    Wrapper for keen.add_events(), adds project info to each event.
175
176
    Param event_collection : collection event data is submitted to
177
    Param payload : array of events that is submitted
178
    """
179
    # add project info to each event
180
    payload = add_project_info_list(payload)
181
182
    # submit list of events to Keen.io
183
    keen.add_events({event_collection: payload})
184
185
186
def add_project_info_dict(payload):
187
    """
188
    Add project info to a dictonary.
189
190
    Param payload: dictonary payload
191
    """
192
    # check if payload is a dictionary, throws an exception if it isn't
193
    check_dict(payload, "payload")
194
195
    payload_as_dict = copy.deepcopy(payload)
196
197
    payload_as_dict[KEEN_PROJECT_INFO_NAME] = Settings().get_project_info()
198
199
    if "job" in payload:
200
        # override project_name, set to build_job repo
201
        if "repo" in payload["job"]:
202
            payload_as_dict[KEEN_PROJECT_INFO_NAME]["project_name"] = \
203
                payload["job"]["repo"]
204
205
        # override timestamp, set to finished_at timestamp
206
        if "finished_at" in payload["job"]:
207
            payload_as_dict["keen"] = {
208
                "timestamp": payload["job"]["finished_at"]["isotimestamp"]
209
            }
210
211
    return payload_as_dict
212
213
214
def add_project_info_list(payload):
215
    """
216
    Add project info to a list of dictionaries.
217
218
    Param payload: list of dictionaries
219
    """
220
    # check if payload is a list, throws an exception if it isn't
221
    is_list(payload, "payload")
222
223
    payload_as_list = []
224
225
    # loop over dicts in payload and add project info to each one
226
    for event_dict in payload:
227
        payload_as_list.append(add_project_info_dict(event_dict))
228
229
    return payload_as_list
230
231
232
def get_dashboard_keen_config(repo):
233
    """
234
    Generate the Keen.io settings for the configuration of the dashboard.
235
236
    The dashboard is Javascript powered HTML file that contains the
237
    graphs generated by Keen.io.
238
    """
239
    # initialise config settings
240
    keen_config = {}
241
242
    if not keen_has_project_id() or not keen_has_master_key():
243
        logger.warning("Keen.io related config settings could not be created,"
244
                       " keen.project_id and/or keen.master_key"
245
                       " are not defined.")
246
        return keen_config
247
248
    # set keen project ID
249
    if keen.project_id is None:
250
        keen_project_id = os.environ["KEEN_PROJECT_ID"]
251
    else:
252
        keen_project_id = keen.project_id
253
    keen_config['projectId'] = str(keen_project_id)
254
255
    # generate read key
256
    read_key = keen_io_generate_read_key(repo)
257
    if read_key is not None:
258
        # convert bytes to string
259
        if isinstance(read_key, bytes):
260
            read_key = read_key.decode('utf-8')
261
262
        keen_config['readKey'] = str(read_key)
263
264
    # return keen config settings
265
    return keen_config
266
267
268
def get_dashboard_config_dict(repo, extra=None):
269
    """
270
    Generate the configuration settings for the dashboard.
271
272
    The dashboard is Javascript powered HTML file that contains the
273
    graphs generated by Keen.io.
274
275
    Parameters:
276
    - repo : repo name (fe. buildtimetrend/service)
277
    - extra : dictionary of extra config settings, format : {"name" : "value"}
278
    """
279
    # initialise config settings dictionaries
280
    config = {}
281
282
    # add repo and project name
283
    if repo is not None and not repo == "":
284
        # merge extra settings into existing config dictionary
285
        config.update({
286
            'projectName': str(repo),
287
            'repoName': str(repo)
288
        })
289
290
    # add extra config parameters
291
    if extra is not None and check_dict(extra, "extra"):
292
        config.update(extra)
293
294
    return config
295
296
def get_dashboard_config(repo, extra=None):
297
    """
298
    Generate the configuration settings for the dashboard.
299
300
    The dashboard is Javascript powered HTML file that contains the
301
    graphs generated by Keen.io.
302
303
    Parameters:
304
    - repo : repo name (fe. buildtimetrend/service)
305
    - extra : dictionary of extra config settings, format : {"name" : "value"}
306
    """
307
    # initialise config settings dictionaries
308
    config = get_dashboard_config_dict(repo, extra)
309
    keen_config = get_dashboard_keen_config(repo)
310
311
    # create configuration as a string
312
    return "var config = {};\nvar keenConfig = {};".format(config, keen_config)
313
314
315
def generate_dashboard_config_file(repo):
316
    """
317
    Generate the configuration file for the dashboard.
318
319
    The dashboard is Javascript powered HTML file that contains the
320
    graphs generated by Keen.io.
321
322
    Parameters:
323
    - repo : repo name (fe. buildtimetrend/service)
324
    """
325
    # get config settings
326
    config_string = get_dashboard_config(repo)
327
328
    # write config file
329
    config_file = Settings().get_setting("dashboard_configfile")
330
    with open(config_file, 'w') as outfile:
331
        outfile.write(config_string)
332
333
    if check_file(config_file):
334
        logger.info("Created trends dashboard config file %s", config_file)
335
        return True
336
    else:
337
        logger.warning("The dashboard config file was not created")
338
        return False
339
340
341
def get_avg_buildtime(repo=None, interval=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
342
    """
343
    Query Keen.io database and retrieve average build time.
344
345
    Parameters :
346
    - repo : repo name (fe. buildtimetrend/service)
347
    - interval : timeframe, possible values : 'week', 'month', 'year',
348
                 anything else defaults to 'week'
349
    """
350
    if repo is None or not keen_is_readable():
351
        return -1
352
353
    interval_data = check_time_interval(interval)
354
355
    try:
356
        return keen.average(
357
            "build_jobs",
358
            target_property="job.duration",
359
            timeframe=interval_data['timeframe'],
360
            max_age=interval_data['max_age'],
361
            filters=[get_repo_filter(repo)]
362
        )
363
    except requests.ConnectionError:
364
        logger.error("Connection to Keen.io API failed")
365
        return -1
366
    except keen.exceptions.KeenApiError as msg:
367
        logger.error("Error in keenio.get_avg_buildtime() : " + str(msg))
368
        return -1
369
370
371
def get_total_build_jobs(repo=None, interval=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
372
    """
373
    Query Keen.io database and retrieve total number of build jobs.
374
375
    Parameters :
376
    - repo : repo name (fe. buildtimetrend/service)
377
    - interval : timeframe, possible values : 'week', 'month', 'year',
378
                 anything else defaults to 'week'
379
    """
380
    if repo is None or not keen_is_readable():
381
        return -1
382
383
    interval_data = check_time_interval(interval)
384
385
    try:
386
        return keen.count_unique(
387
            "build_jobs",
388
            target_property="job.job",
389
            timeframe=interval_data['timeframe'],
390
            max_age=interval_data['max_age'],
391
            filters=[get_repo_filter(repo)]
392
        )
393
    except requests.ConnectionError:
394
        logger.error("Connection to Keen.io API failed")
395
        return -1
396
    except keen.exceptions.KeenApiError as msg:
397
        logger.error("Error in keenio.get_total_build_jobs() : " + str(msg))
398
        return -1
399
400
401
def get_passed_build_jobs(repo=None, interval=None):
402
    """
403
    Query Keen.io database and retrieve total number of build jobs that passed.
404
405
    Parameters :
406
    - repo : repo name (fe. buildtimetrend/service)
407
    - interval : timeframe, possible values : 'week', 'month', 'year',
408
                 anything else defaults to 'week'
409
    """
410
    if repo is None or not keen_is_readable():
411
        return -1
412
413
    interval_data = check_time_interval(interval)
414
415
    try:
416
        return keen.count_unique(
417
            "build_jobs",
418
            target_property="job.job",
419
            timeframe=interval_data['timeframe'],
420
            max_age=interval_data['max_age'],
421
            filters=[
422
                get_repo_filter(repo),
423
                {
424
                    "property_name": "job.result",
425
                    "operator": "eq",
426
                    "property_value": "passed"
427
                }
428
            ]
429
        )
430
    except requests.ConnectionError:
431
        logger.error("Connection to Keen.io API failed")
432
        return -1
433
    except keen.exceptions.KeenApiError as msg:
434
        logger.error("Error in keenio.get_passed_build_jobs() : " + str(msg))
435
        return -1
436
437
438
def get_pct_passed_build_jobs(repo=None, interval=None):
439
    """
440
    Calculate percentage of passed build jobs.
441
442
    Parameters :
443
    - repo : repo name (fe. buildtimetrend/service)
444
    - interval : timeframe, possible values : 'week', 'month', 'year',
445
                 anything else defaults to 'week'
446
    """
447
    total_jobs = get_total_build_jobs(repo, interval)
448
    passed_jobs = get_passed_build_jobs(repo, interval)
449
450
    logger.debug("passed/total build jobs : %d/%d", passed_jobs, total_jobs)
451
452
    # calculate percentage if at least one job was executed
453
    # passed is a valid number (not -1)
454
    if total_jobs > 0 and passed_jobs >= 0:
455
        return int(float(passed_jobs) / float(total_jobs) * 100.0)
456
457
    return -1
458
459
460
def get_result_color(value=0, ok_thershold=90, warning_thershold=70):
461
    """
462
    Get color code that corresponds to the result.
463
464
    Parameters:
465
    - value : value to check
466
    - ok_thershold : OK threshold value
467
    - warning_thershold : warning thershold value
468
    """
469
    if not(type(value) in (int, float) and
470
           type(ok_thershold) in (int, float) and
471
           type(warning_thershold) in (int, float)):
472
        return "lightgrey"
473
474
    # check thresholds
475
    if value >= ok_thershold:
476
        return "green"
477
    elif value >= warning_thershold:
478
        return "yellow"
479
    else:
480
        return "red"
481
482
483
def get_total_builds(repo=None, interval=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
484
    """
485
    Query Keen.io database and retrieve total number of builds.
486
487
    Parameters :
488
    - repo : repo name (fe. buildtimetrend/service)
489
    - interval : timeframe, possible values : 'week', 'month', 'year',
490
                 anything else defaults to 'week'
491
    """
492
    if repo is None or not keen_is_readable():
493
        return -1
494
495
    interval_data = check_time_interval(interval)
496
497
    try:
498
        return keen.count_unique(
499
            "build_jobs",
500
            target_property="job.build",
501
            timeframe=interval_data['timeframe'],
502
            max_age=interval_data['max_age'],
503
            filters=[get_repo_filter(repo)]
504
        )
505
    except requests.ConnectionError:
506
        logger.error("Connection to Keen.io API failed")
507
        return -1
508
    except keen.exceptions.KeenApiError as msg:
509
        logger.error("Error in keenio.get_total_builds() : " + str(msg))
510
        return -1
511
512
513
def get_latest_buildtime(repo=None):
514
    """
515
    Query Keen.io database and retrieve buildtime duration of last build.
516
517
    Parameters :
518
    - repo : repo name (fe. buildtimetrend/python-lib)
519
    """
520
    if repo is None or not keen_is_readable():
521
        return -1
522
523
    try:
524
        result = keen.extraction(
525
            "build_jobs",
526
            property_names="job.duration",
527
            latest=1,
528
            filters=[get_repo_filter(repo)]
529
        )
530
    except requests.ConnectionError:
531
        logger.error("Connection to Keen.io API failed")
532
        return -1
533
    except keen.exceptions.KeenApiError as msg:
534
        logger.error("Error in keenio.get_latest_buildtime() : " + str(msg))
535
        return -1
536
537
    if result is not None and len(result) > 0:
538
        return result[0]['job']['duration']
539
540
    return -1
541
542
543
def has_build_id(repo=None, build_id=None):
544
    """
545
    Check if build_id exists in Keen.io database.
546
547
    Parameters :
548
    - repo : repo name (fe. buildtimetrend/python-lib)
549
    - build_id : ID of the build
550
    """
551
    if repo is None or build_id is None:
552
        logger.error("Repo or build_id is not set")
553
        raise ValueError("Repo or build_id is not set")
554
    if not keen_is_readable():
555
        raise SystemError("Keen.io Project ID or API Read Key is not set")
556
557
    try:
558
        count = keen.count(
559
            "build_jobs",
560
            filters=[get_repo_filter(repo), {
561
                "property_name": "job.build",
562
                "operator": "eq",
563
                "property_value": str(build_id)}]
564
        )
565
    except requests.ConnectionError:
566
        logger.error("Connection to Keen.io API failed")
567
        raise SystemError("Connection to Keen.io API failed")
568
    except keen.exceptions.KeenApiError as msg:
569
        logger.error("Error in keenio.has_build_id : " + str(msg))
570
        raise SystemError(msg)
571
572
    return count > 0
573
574
575
def get_all_projects():
576
    """Query Keen.io database and retrieve a list of all projects."""
577
    if not keen_is_readable():
578
        return []
579
580
    try:
581
        result = keen.select_unique(
582
            "build_jobs",
583
            "buildtime_trend.project_name",
584
            max_age=3600 * 24  # cache for 24 hours
585
        )
586
    except requests.ConnectionError:
587
        logger.error("Connection to Keen.io API failed")
588
        return []
589
    except keen.exceptions.KeenApiError as msg:
590
        logger.error("Error in keenio.get_all_projects() : " + str(msg))
591
        return []
592
593
    if type(result) is list:
594
        return result
595
596
    return []
597
598
599
def get_repo_filter(repo=None):
600
    """
601
    Return filter for analysis request.
602
603
    Parameters
604
    - repo : repo slug name, fe. buildtimetrend/python-lib
605
    """
606
    if repo is None:
607
        return None
608
609
    return {
610
        "property_name": "%s.project_name" % KEEN_PROJECT_INFO_NAME,
611
        "operator": "eq",
612
        "property_value": str(repo)
613
    }
614
615
616
def check_time_interval(interval=None):
617
    """
618
    Check time interval and returns corresponding parameters.
619
620
    Parameters :
621
    - interval : timeframe, possible values : 'week', 'month', 'year',
622
                 anything else defaults to 'week'
623
    """
624
    if is_string(interval):
625
        # convert to lowercase
626
        interval = interval.lower()
627
628
        if interval in TIME_INTERVALS:
629
            return TIME_INTERVALS[interval]
630
631
    return TIME_INTERVALS['week']
632