diff options
-rwxr-xr-x | bitbake/bin/toaster-eventreplay | 74 | ||||
-rw-r--r-- | bitbake/lib/bb/ui/eventreplay.py | 86 | ||||
-rw-r--r-- | bitbake/lib/bb/ui/toasterui.py | 2 | ||||
-rw-r--r-- | bitbake/lib/toaster/orm/migrations/0021_eventlogsimports.py | 22 | ||||
-rw-r--r-- | bitbake/lib/toaster/orm/models.py | 9 | ||||
-rw-r--r-- | bitbake/lib/toaster/tests/browser/test_landing_page.py | 2 | ||||
-rw-r--r-- | bitbake/lib/toaster/tests/browser/test_layerdetails_page.py | 2 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/forms.py | 14 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/static/css/default.css | 28 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/templates/base.html | 3 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/templates/command_line_builds.html | 198 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/templates/landing.html | 10 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/urls.py | 1 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/views.py | 173 |
14 files changed, 543 insertions, 81 deletions
diff --git a/bitbake/bin/toaster-eventreplay b/bitbake/bin/toaster-eventreplay index f137c71d5c..74a319320e 100755 --- a/bitbake/bin/toaster-eventreplay +++ b/bitbake/bin/toaster-eventreplay | |||
@@ -30,77 +30,7 @@ sys.path.insert(0, join(dirname(dirname(abspath(__file__))), 'lib')) | |||
30 | 30 | ||
31 | import bb.cooker | 31 | import bb.cooker |
32 | from bb.ui import toasterui | 32 | from bb.ui import toasterui |
33 | 33 | from bb.ui import eventreplay | |
34 | class EventPlayer: | ||
35 | """Emulate a connection to a bitbake server.""" | ||
36 | |||
37 | def __init__(self, eventfile, variables): | ||
38 | self.eventfile = eventfile | ||
39 | self.variables = variables | ||
40 | self.eventmask = [] | ||
41 | |||
42 | def waitEvent(self, _timeout): | ||
43 | """Read event from the file.""" | ||
44 | line = self.eventfile.readline().strip() | ||
45 | if not line: | ||
46 | return | ||
47 | try: | ||
48 | decodedline = json.loads(line) | ||
49 | if 'allvariables' in decodedline: | ||
50 | self.variables = decodedline['allvariables'] | ||
51 | return | ||
52 | if not 'vars' in decodedline: | ||
53 | raise ValueError | ||
54 | event_str = decodedline['vars'].encode('utf-8') | ||
55 | event = pickle.loads(codecs.decode(event_str, 'base64')) | ||
56 | event_name = "%s.%s" % (event.__module__, event.__class__.__name__) | ||
57 | if event_name not in self.eventmask: | ||
58 | return | ||
59 | return event | ||
60 | except ValueError as err: | ||
61 | print("Failed loading ", line) | ||
62 | raise err | ||
63 | |||
64 | def runCommand(self, command_line): | ||
65 | """Emulate running a command on the server.""" | ||
66 | name = command_line[0] | ||
67 | |||
68 | if name == "getVariable": | ||
69 | var_name = command_line[1] | ||
70 | variable = self.variables.get(var_name) | ||
71 | if variable: | ||
72 | return variable['v'], None | ||
73 | return None, "Missing variable %s" % var_name | ||
74 | |||
75 | elif name == "getAllKeysWithFlags": | ||
76 | dump = {} | ||
77 | flaglist = command_line[1] | ||
78 | for key, val in self.variables.items(): | ||
79 | try: | ||
80 | if not key.startswith("__"): | ||
81 | dump[key] = { | ||
82 | 'v': val['v'], | ||
83 | 'history' : val['history'], | ||
84 | } | ||
85 | for flag in flaglist: | ||
86 | dump[key][flag] = val[flag] | ||
87 | except Exception as err: | ||
88 | print(err) | ||
89 | return (dump, None) | ||
90 | |||
91 | elif name == 'setEventMask': | ||
92 | self.eventmask = command_line[-1] | ||
93 | return True, None | ||
94 | |||
95 | else: | ||
96 | raise Exception("Command %s not implemented" % command_line[0]) | ||
97 | |||
98 | def getEventHandle(self): | ||
99 | """ | ||
100 | This method is called by toasterui. | ||
101 | The return value is passed to self.runCommand but not used there. | ||
102 | """ | ||
103 | pass | ||
104 | 34 | ||
105 | def main(argv): | 35 | def main(argv): |
106 | with open(argv[-1]) as eventfile: | 36 | with open(argv[-1]) as eventfile: |
@@ -116,7 +46,7 @@ def main(argv): | |||
116 | sys.exit("Cannot find allvariables entry in event log file %s" % argv[-1]) | 46 | sys.exit("Cannot find allvariables entry in event log file %s" % argv[-1]) |
117 | eventfile.seek(0) | 47 | eventfile.seek(0) |
118 | params = namedtuple('ConfigParams', ['observe_only'])(True) | 48 | params = namedtuple('ConfigParams', ['observe_only'])(True) |
119 | player = EventPlayer(eventfile, variables) | 49 | player = eventreplay.EventPlayer(eventfile, variables) |
120 | 50 | ||
121 | return toasterui.main(player, player, params) | 51 | return toasterui.main(player, player, params) |
122 | 52 | ||
diff --git a/bitbake/lib/bb/ui/eventreplay.py b/bitbake/lib/bb/ui/eventreplay.py new file mode 100644 index 0000000000..d62ecbfa56 --- /dev/null +++ b/bitbake/lib/bb/ui/eventreplay.py | |||
@@ -0,0 +1,86 @@ | |||
1 | #!/usr/bin/env python3 | ||
2 | # | ||
3 | # SPDX-License-Identifier: GPL-2.0-only | ||
4 | # | ||
5 | # This file re-uses code spread throughout other Bitbake source files. | ||
6 | # As such, all other copyrights belong to their own right holders. | ||
7 | # | ||
8 | |||
9 | |||
10 | import os | ||
11 | import sys | ||
12 | import json | ||
13 | import pickle | ||
14 | import codecs | ||
15 | |||
16 | |||
17 | class EventPlayer: | ||
18 | """Emulate a connection to a bitbake server.""" | ||
19 | |||
20 | def __init__(self, eventfile, variables): | ||
21 | self.eventfile = eventfile | ||
22 | self.variables = variables | ||
23 | self.eventmask = [] | ||
24 | |||
25 | def waitEvent(self, _timeout): | ||
26 | """Read event from the file.""" | ||
27 | line = self.eventfile.readline().strip() | ||
28 | if not line: | ||
29 | return | ||
30 | try: | ||
31 | decodedline = json.loads(line) | ||
32 | if 'allvariables' in decodedline: | ||
33 | self.variables = decodedline['allvariables'] | ||
34 | return | ||
35 | if not 'vars' in decodedline: | ||
36 | raise ValueError | ||
37 | event_str = decodedline['vars'].encode('utf-8') | ||
38 | event = pickle.loads(codecs.decode(event_str, 'base64')) | ||
39 | event_name = "%s.%s" % (event.__module__, event.__class__.__name__) | ||
40 | if event_name not in self.eventmask: | ||
41 | return | ||
42 | return event | ||
43 | except ValueError as err: | ||
44 | print("Failed loading ", line) | ||
45 | raise err | ||
46 | |||
47 | def runCommand(self, command_line): | ||
48 | """Emulate running a command on the server.""" | ||
49 | name = command_line[0] | ||
50 | |||
51 | if name == "getVariable": | ||
52 | var_name = command_line[1] | ||
53 | variable = self.variables.get(var_name) | ||
54 | if variable: | ||
55 | return variable['v'], None | ||
56 | return None, "Missing variable %s" % var_name | ||
57 | |||
58 | elif name == "getAllKeysWithFlags": | ||
59 | dump = {} | ||
60 | flaglist = command_line[1] | ||
61 | for key, val in self.variables.items(): | ||
62 | try: | ||
63 | if not key.startswith("__"): | ||
64 | dump[key] = { | ||
65 | 'v': val['v'], | ||
66 | 'history' : val['history'], | ||
67 | } | ||
68 | for flag in flaglist: | ||
69 | dump[key][flag] = val[flag] | ||
70 | except Exception as err: | ||
71 | print(err) | ||
72 | return (dump, None) | ||
73 | |||
74 | elif name == 'setEventMask': | ||
75 | self.eventmask = command_line[-1] | ||
76 | return True, None | ||
77 | |||
78 | else: | ||
79 | raise Exception("Command %s not implemented" % command_line[0]) | ||
80 | |||
81 | def getEventHandle(self): | ||
82 | """ | ||
83 | This method is called by toasterui. | ||
84 | The return value is passed to self.runCommand but not used there. | ||
85 | """ | ||
86 | pass | ||
diff --git a/bitbake/lib/bb/ui/toasterui.py b/bitbake/lib/bb/ui/toasterui.py index ec5bd4f105..6bd21f1844 100644 --- a/bitbake/lib/bb/ui/toasterui.py +++ b/bitbake/lib/bb/ui/toasterui.py | |||
@@ -385,7 +385,7 @@ def main(server, eventHandler, params): | |||
385 | main.shutdown = 1 | 385 | main.shutdown = 1 |
386 | 386 | ||
387 | logger.info("ToasterUI build done, brbe: %s", brbe) | 387 | logger.info("ToasterUI build done, brbe: %s", brbe) |
388 | continue | 388 | break |
389 | 389 | ||
390 | if isinstance(event, (bb.command.CommandCompleted, | 390 | if isinstance(event, (bb.command.CommandCompleted, |
391 | bb.command.CommandFailed, | 391 | bb.command.CommandFailed, |
diff --git a/bitbake/lib/toaster/orm/migrations/0021_eventlogsimports.py b/bitbake/lib/toaster/orm/migrations/0021_eventlogsimports.py new file mode 100644 index 0000000000..328eb5753c --- /dev/null +++ b/bitbake/lib/toaster/orm/migrations/0021_eventlogsimports.py | |||
@@ -0,0 +1,22 @@ | |||
1 | # Generated by Django 4.2.5 on 2023-11-23 18:44 | ||
2 | |||
3 | from django.db import migrations, models | ||
4 | |||
5 | |||
6 | class Migration(migrations.Migration): | ||
7 | |||
8 | dependencies = [ | ||
9 | ('orm', '0020_models_bigautofield'), | ||
10 | ] | ||
11 | |||
12 | operations = [ | ||
13 | migrations.CreateModel( | ||
14 | name='EventLogsImports', | ||
15 | fields=[ | ||
16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
17 | ('name', models.CharField(max_length=255)), | ||
18 | ('imported', models.BooleanField(default=False)), | ||
19 | ('build_id', models.IntegerField(blank=True, null=True)), | ||
20 | ], | ||
21 | ), | ||
22 | ] | ||
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py index 1098ad3fd1..19c9686206 100644 --- a/bitbake/lib/toaster/orm/models.py +++ b/bitbake/lib/toaster/orm/models.py | |||
@@ -1868,6 +1868,15 @@ class Distro(models.Model): | |||
1868 | def __unicode__(self): | 1868 | def __unicode__(self): |
1869 | return "Distro " + self.name + "(" + self.description + ")" | 1869 | return "Distro " + self.name + "(" + self.description + ")" |
1870 | 1870 | ||
1871 | class EventLogsImports(models.Model): | ||
1872 | name = models.CharField(max_length=255) | ||
1873 | imported = models.BooleanField(default=False) | ||
1874 | build_id = models.IntegerField(blank=True, null=True) | ||
1875 | |||
1876 | def __str__(self): | ||
1877 | return self.name | ||
1878 | |||
1879 | |||
1871 | django.db.models.signals.post_save.connect(invalidate_cache) | 1880 | django.db.models.signals.post_save.connect(invalidate_cache) |
1872 | django.db.models.signals.post_delete.connect(invalidate_cache) | 1881 | django.db.models.signals.post_delete.connect(invalidate_cache) |
1873 | django.db.models.signals.m2m_changed.connect(invalidate_cache) | 1882 | django.db.models.signals.m2m_changed.connect(invalidate_cache) |
diff --git a/bitbake/lib/toaster/tests/browser/test_landing_page.py b/bitbake/lib/toaster/tests/browser/test_landing_page.py index f6649a3d82..8fe5fea467 100644 --- a/bitbake/lib/toaster/tests/browser/test_landing_page.py +++ b/bitbake/lib/toaster/tests/browser/test_landing_page.py | |||
@@ -219,5 +219,3 @@ class TestLandingPage(SeleniumTestCase): | |||
219 | content = self.get_page_source() | 219 | content = self.get_page_source() |
220 | self.assertTrue(self.PROJECT_NAME in content, | 220 | self.assertTrue(self.PROJECT_NAME in content, |
221 | 'should show builds for project %s' % self.PROJECT_NAME) | 221 | 'should show builds for project %s' % self.PROJECT_NAME) |
222 | self.assertFalse(self.CLI_BUILDS_PROJECT_NAME in content, | ||
223 | 'should not show builds for cli project') | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py b/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py index 367c6179c6..05ee88b019 100644 --- a/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py +++ b/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py | |||
@@ -108,7 +108,7 @@ class TestLayerDetailsPage(SeleniumTestCase): | |||
108 | 108 | ||
109 | self.wait_until_visible("#save-changes-for-switch", poll=3) | 109 | self.wait_until_visible("#save-changes-for-switch", poll=3) |
110 | btn_save_chg_for_switch = self.find("#save-changes-for-switch") | 110 | btn_save_chg_for_switch = self.find("#save-changes-for-switch") |
111 | btn_save_chg_for_switch.click() | 111 | self.driver.execute_script("arguments[0].click();", btn_save_chg_for_switch) |
112 | self.wait_until_visible("#edit-layer-source") | 112 | self.wait_until_visible("#edit-layer-source") |
113 | 113 | ||
114 | # Refresh the page to see if the new values are returned | 114 | # Refresh the page to see if the new values are returned |
diff --git a/bitbake/lib/toaster/toastergui/forms.py b/bitbake/lib/toaster/toastergui/forms.py new file mode 100644 index 0000000000..0f279e06c5 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/forms.py | |||
@@ -0,0 +1,14 @@ | |||
1 | #!/usr/bin/env python3 | ||
2 | # -*- coding: utf-8 -*- | ||
3 | # BitBake Toaster UI tests implementation | ||
4 | # | ||
5 | # Copyright (C) 2023 Savoir-faire Linux | ||
6 | # | ||
7 | # SPDX-License-Identifier: GPL-2.0-only | ||
8 | # | ||
9 | |||
10 | from django import forms | ||
11 | from django.core.validators import FileExtensionValidator | ||
12 | |||
13 | class LoadFileForm(forms.Form): | ||
14 | eventlog_file = forms.FileField(widget=forms.FileInput(attrs={'accept': '.json'})) | ||
diff --git a/bitbake/lib/toaster/toastergui/static/css/default.css b/bitbake/lib/toaster/toastergui/static/css/default.css index 5cd7e211a0..284355e70b 100644 --- a/bitbake/lib/toaster/toastergui/static/css/default.css +++ b/bitbake/lib/toaster/toastergui/static/css/default.css | |||
@@ -367,3 +367,31 @@ h2.panel-title { font-size: 30px; } | |||
367 | } | 367 | } |
368 | } | 368 | } |
369 | /* End copied in from newer version of Font-Awesome 4.3.0 */ | 369 | /* End copied in from newer version of Font-Awesome 4.3.0 */ |
370 | |||
371 | |||
372 | #overlay { | ||
373 | display: flex; | ||
374 | position: fixed; | ||
375 | top: 0; | ||
376 | left: 0; | ||
377 | width: 100%; | ||
378 | height: 100%; | ||
379 | background-color: rgba(0, 0, 0, 0.7); | ||
380 | align-items: center; | ||
381 | justify-content: center; | ||
382 | z-index: 999; | ||
383 | } | ||
384 | |||
385 | .spinner { | ||
386 | border: 6px solid rgba(255, 255, 255, 0.3); | ||
387 | border-radius: 50%; | ||
388 | border-top: 6px solid #3498db; | ||
389 | width: 50px; | ||
390 | height: 50px; | ||
391 | animation: spin 1s linear infinite; | ||
392 | } | ||
393 | |||
394 | @keyframes spin { | ||
395 | 0% { transform: rotate(0deg); } | ||
396 | 100% { transform: rotate(360deg); } | ||
397 | } | ||
diff --git a/bitbake/lib/toaster/toastergui/templates/base.html b/bitbake/lib/toaster/toastergui/templates/base.html index 041448d180..e90be69620 100644 --- a/bitbake/lib/toaster/toastergui/templates/base.html +++ b/bitbake/lib/toaster/toastergui/templates/base.html | |||
@@ -132,7 +132,8 @@ | |||
132 | {% if project_enable %} | 132 | {% if project_enable %} |
133 | <a class="btn btn-default navbar-btn navbar-right" id="new-project-button" href="{% url 'newproject' %}">New project</a> | 133 | <a class="btn btn-default navbar-btn navbar-right" id="new-project-button" href="{% url 'newproject' %}">New project</a> |
134 | {% endif %} | 134 | {% endif %} |
135 | </div> | 135 | <a class="btn btn-default navbar-btn navbar-right" id="import_page" style="margin-right: 5px !important" id="import-cmdline-button" href="{% url 'cmdlines' %}">Import command line builds</a> |
136 | </div> | ||
136 | </div> | 137 | </div> |
137 | </nav> | 138 | </nav> |
138 | 139 | ||
diff --git a/bitbake/lib/toaster/toastergui/templates/command_line_builds.html b/bitbake/lib/toaster/toastergui/templates/command_line_builds.html new file mode 100644 index 0000000000..d6ff685cdf --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/command_line_builds.html | |||
@@ -0,0 +1,198 @@ | |||
1 | {% extends "base.html" %} | ||
2 | {% load projecttags %} | ||
3 | {% load humanize %} | ||
4 | |||
5 | {% block title %} Import Builds from eventlogs - Toaster {% endblock %} | ||
6 | |||
7 | {% block pagecontent %} | ||
8 | |||
9 | <div class="container-fluid"> | ||
10 | <div id="overlay" class="hide"> | ||
11 | <div class="spinner"> | ||
12 | <div class="fa-spin"> | ||
13 | </div> | ||
14 | </div> | ||
15 | </div> | ||
16 | <div class="row"> | ||
17 | <div class="col-md-12"> | ||
18 | <div class="page-header"> | ||
19 | <div class="row"> | ||
20 | <div class="col-md-6"> | ||
21 | <h1>Import command line builds</h1> | ||
22 | </div> | ||
23 | {% if import_all %} | ||
24 | <div class="col-md-6"> | ||
25 | <button id="import_all" type="button" class="btn btn-primary navbar-btn navbar-right"> | ||
26 | <span class="glyphicon glyphicon-upload" style="vertical-align: top;"></span> Import All | ||
27 | </button> | ||
28 | </div> | ||
29 | {% endif %} | ||
30 | </div> | ||
31 | </div> | ||
32 | {% if messages %} | ||
33 | <div class="row-fluid" id="empty-state-{{table_name}}"> | ||
34 | {% for message in messages %} | ||
35 | <div class="alert alert-danger">{{message}}</div> | ||
36 | {%endfor%} | ||
37 | </div> | ||
38 | {% endif %} | ||
39 | <div class="row"> | ||
40 | <h4 style="margin-left: 15px;"><strong>Import eventlog file</strong></h4> | ||
41 | <form method="POST" enctype="multipart/form-data" action="{% url 'cmdlines' %}" id="form_file"> | ||
42 | {% csrf_token %} | ||
43 | <div class="col-md-6" style="padding-left: 20px;"> | ||
44 | <div class="row"> | ||
45 | <input type="hidden" value="{{dir}}" name="dir"> | ||
46 | <div class="col-md-3"> {{ form.eventlog_file}} </div> | ||
47 | </div> | ||
48 | <div class="row" style="padding-top: 10px;"> | ||
49 | <div class="col-md-6"> | ||
50 | <button id="file_import" type="submit" disabled="disabled" class="btn btn-default navbar-btn" > | ||
51 | <span class="glyphicon glyphicon-upload" style="vertical-align: top;"></span> Import | ||
52 | </button> | ||
53 | </div> | ||
54 | </div> | ||
55 | </div> | ||
56 | </form> | ||
57 | </div> | ||
58 | |||
59 | <div class="row" style="padding-top: 20px;"> | ||
60 | <div class="col-md-8 "> | ||
61 | <h4><strong>Eventlogs from existing build directory: </strong> | ||
62 | <a href="#" data-toggle="tooltip" title="{{dir}}"> | ||
63 | <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16" data-toggle="tooltip"> | ||
64 | <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> | ||
65 | <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/> | ||
66 | </svg> | ||
67 | </a> | ||
68 | </h4> | ||
69 | {% if files %} | ||
70 | <div class="table-responsive"> | ||
71 | <table class="table col-md-6 table-bordered table-hover"> | ||
72 | <thead> | ||
73 | <tr class="row"> | ||
74 | <th scope="col">Name</th> | ||
75 | <th scope="col">Size</th> | ||
76 | <th scope="col">Action</th> | ||
77 | </tr> | ||
78 | </thead> | ||
79 | <tbody> | ||
80 | {% for file in files %} | ||
81 | <tr class="row" style="height: 48px;"> | ||
82 | <th scope="row" class="col-md-4" style="vertical-align: middle;"> | ||
83 | <input type="hidden" value="{{file.name}}" name="{{file.name}}">{{file.name}} | ||
84 | </th> | ||
85 | <td class="col-md-4 align-middle" style="vertical-align: middle;">{{file.size|filesizeformat}}</td> | ||
86 | <td class="col-md-4 align-middle" style="vertical-align: middle;"> | ||
87 | {% if file.imported == True and file.build_id is not None %} | ||
88 | <a href="{% url 'builddashboard' file.build_id %}">Build Details</a> | ||
89 | {% elif request.session.file == file.name or request.session.all_builds %} | ||
90 | <a data-toggle="tooltip" title="Build in progress"> | ||
91 | <span class="glyphicon glyphicon-upload" style="font-size: 18px; color:grey"></span> | ||
92 | </a> | ||
93 | {%else%} | ||
94 | <a onclick="_ajax_update('{{file.name}}', false, '{{dir}}')" data-toggle="tooltip" title="Import File"> | ||
95 | <span class="glyphicon glyphicon-upload" style="font-size: 18px;"></span> | ||
96 | </a> | ||
97 | {%endif%} | ||
98 | </td> | ||
99 | </tr> | ||
100 | {% endfor%} | ||
101 | </tbody> | ||
102 | </table> | ||
103 | </div> | ||
104 | {% else %} | ||
105 | <div class="row-fluid" id="empty-state-{{table_name}}"> | ||
106 | <div class="alert alert-info">Sorry - no files found</div> | ||
107 | </div> | ||
108 | {%endif%} | ||
109 | </div> | ||
110 | </div> | ||
111 | </div> | ||
112 | </div> | ||
113 | </div> | ||
114 | |||
115 | <script> | ||
116 | |||
117 | function _ajax_update(file, all, dir){ | ||
118 | function getCookie(name) { | ||
119 | var cookieValue = null; | ||
120 | if (document.cookie && document.cookie !== '') { | ||
121 | var cookies = document.cookie.split(';'); | ||
122 | for (var i = 0; i < cookies.length; i++) { | ||
123 | var cookie = jQuery.trim(cookies[i]); | ||
124 | // Does this cookie string begin with the name we want? | ||
125 | if (cookie.substring(0, name.length + 1) === (name + '=')) { | ||
126 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); | ||
127 | break; | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | return cookieValue; | ||
132 | } | ||
133 | var csrftoken = getCookie('csrftoken'); | ||
134 | |||
135 | function csrfSafeMethod(method) { | ||
136 | // these HTTP methods do not require CSRF protection | ||
137 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); | ||
138 | } | ||
139 | $.ajaxSetup({ | ||
140 | beforeSend: function (xhr, settings) { | ||
141 | if (!csrfSafeMethod(settings.type) && !this.crossDomain) { | ||
142 | xhr.setRequestHeader("X-CSRFToken", csrftoken); | ||
143 | } | ||
144 | } | ||
145 | }); | ||
146 | |||
147 | $.ajax({ | ||
148 | url:'/toastergui/cmdline/', | ||
149 | type: "POST", | ||
150 | data: {file: file, all: all, dir: dir}, | ||
151 | success:function(data){ | ||
152 | window.location = '/toastergui/builds/' | ||
153 | }, | ||
154 | complete:function(data){ | ||
155 | }, | ||
156 | error:function (xhr, textStatus, thrownError){ | ||
157 | console.log('fail'); | ||
158 | } | ||
159 | }); | ||
160 | } | ||
161 | |||
162 | $('#import_all').on('click', function(){ | ||
163 | _ajax_update("{{files | safe}}", true, "{{dir | safe}}"); | ||
164 | }); | ||
165 | |||
166 | |||
167 | $('#import_page').hide(); | ||
168 | |||
169 | $(function () { | ||
170 | $('[data-toggle="tooltip"]').tooltip() | ||
171 | }) | ||
172 | |||
173 | |||
174 | $("#id_eventlog_file").change(function(){ | ||
175 | $('#file_import').prop("disabled", false); | ||
176 | $('#file_import').addClass('btn-primary') | ||
177 | $('#file_import').removeClass('btn-default') | ||
178 | }) | ||
179 | |||
180 | $(document).ajaxStart(function(){ | ||
181 | $('#overlay').removeClass('hide'); | ||
182 | window.setTimeout( | ||
183 | function() { | ||
184 | window.location = '/toastergui/builds/' | ||
185 | }, 10000) | ||
186 | }); | ||
187 | |||
188 | $( "#form_file").on( "submit", function( event ) { | ||
189 | $('#overlay').removeClass('hide'); | ||
190 | window.setTimeout( | ||
191 | function() { | ||
192 | window.location = '/toastergui/builds/' | ||
193 | }, 10000) | ||
194 | }); | ||
195 | |||
196 | </script> | ||
197 | |||
198 | {% endblock %} | ||
diff --git a/bitbake/lib/toaster/toastergui/templates/landing.html b/bitbake/lib/toaster/toastergui/templates/landing.html index 22bbed695a..589ee22634 100644 --- a/bitbake/lib/toaster/toastergui/templates/landing.html +++ b/bitbake/lib/toaster/toastergui/templates/landing.html | |||
@@ -15,7 +15,7 @@ | |||
15 | <p>A web interface to <a href="https://www.openembedded.org">OpenEmbedded</a> and <a href="https://docs.yoctoproject.org/bitbake.html">BitBake</a>, the <a href="https://www.yoctoproject.org">Yocto Project</a> build system.</p> | 15 | <p>A web interface to <a href="https://www.openembedded.org">OpenEmbedded</a> and <a href="https://docs.yoctoproject.org/bitbake.html">BitBake</a>, the <a href="https://www.yoctoproject.org">Yocto Project</a> build system.</p> |
16 | 16 | ||
17 | <p class="top-air"> | 17 | <p class="top-air"> |
18 | <a class="btn btn-info btn-lg" href="http://docs.yoctoproject.org/toaster-manual/setup-and-use.html#setting-up-and-using-toaster"> | 18 | <a class="btn btn-info btn-lg" href="http://docs.yoctoproject.org/toaster-manual/setup-and-use.html#setting-up-and-using-toaster" style="min-width: 460px;"> |
19 | Toaster is ready to capture your command line builds | 19 | Toaster is ready to capture your command line builds |
20 | </a> | 20 | </a> |
21 | </p> | 21 | </p> |
@@ -23,7 +23,7 @@ | |||
23 | {% if lvs_nos %} | 23 | {% if lvs_nos %} |
24 | {% if project_enable %} | 24 | {% if project_enable %} |
25 | <p class="top-air"> | 25 | <p class="top-air"> |
26 | <a class="btn btn-primary btn-lg" href="{% url 'newproject' %}"> | 26 | <a class="btn btn-primary btn-lg" href="{% url 'newproject' %}" style="min-width: 460px;"> |
27 | Create your first Toaster project to run manage builds | 27 | Create your first Toaster project to run manage builds |
28 | </a> | 28 | </a> |
29 | </p> | 29 | </p> |
@@ -42,6 +42,12 @@ | |||
42 | </div> | 42 | </div> |
43 | {% endif %} | 43 | {% endif %} |
44 | 44 | ||
45 | <p class="top-air"> | ||
46 | <a class="btn btn-info btn-lg" href="{% url 'cmdlines' %}" style="min-width: 460px;"> | ||
47 | Import command line event logs from build directory | ||
48 | </a> | ||
49 | </p> | ||
50 | |||
45 | <ul class="list-unstyled lead"> | 51 | <ul class="list-unstyled lead"> |
46 | <li> | 52 | <li> |
47 | <a href="http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual"> | 53 | <a href="http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual"> |
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py index 2c138d3d7d..7f8489d3aa 100644 --- a/bitbake/lib/toaster/toastergui/urls.py +++ b/bitbake/lib/toaster/toastergui/urls.py | |||
@@ -95,6 +95,7 @@ urlpatterns = [ | |||
95 | # project URLs | 95 | # project URLs |
96 | url(r'^newproject/$', views.newproject, name='newproject'), | 96 | url(r'^newproject/$', views.newproject, name='newproject'), |
97 | 97 | ||
98 | url(r'^cmdline/$', views.CommandLineBuilds.as_view(), name='cmdlines'), | ||
98 | url(r'^projects/$', | 99 | url(r'^projects/$', |
99 | tables.ProjectsTable.as_view(template_name="projects-toastertable.html"), | 100 | tables.ProjectsTable.as_view(template_name="projects-toastertable.html"), |
100 | name='all-projects'), | 101 | name='all-projects'), |
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py index 735d304ad8..3b5b9f5bd9 100644 --- a/bitbake/lib/toaster/toastergui/views.py +++ b/bitbake/lib/toaster/toastergui/views.py | |||
@@ -6,24 +6,36 @@ | |||
6 | # SPDX-License-Identifier: GPL-2.0-only | 6 | # SPDX-License-Identifier: GPL-2.0-only |
7 | # | 7 | # |
8 | 8 | ||
9 | import ast | ||
9 | import re | 10 | import re |
11 | import subprocess | ||
12 | import sys | ||
13 | |||
14 | import bb.cooker | ||
15 | from bb.ui import toasterui | ||
16 | from bb.ui import eventreplay | ||
10 | 17 | ||
11 | from django.db.models import F, Q, Sum | 18 | from django.db.models import F, Q, Sum |
12 | from django.db import IntegrityError | 19 | from django.db import IntegrityError |
13 | from django.shortcuts import render, redirect, get_object_or_404 | 20 | from django.shortcuts import render, redirect, get_object_or_404, HttpResponseRedirect |
14 | from django.utils.http import urlencode | 21 | from django.utils.http import urlencode |
15 | from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe | 22 | from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe |
16 | from orm.models import LogMessage, Variable, Package_Dependency, Package | 23 | from orm.models import LogMessage, Variable, Package_Dependency, Package |
17 | from orm.models import Task_Dependency, Package_File | 24 | from orm.models import Task_Dependency, Package_File |
18 | from orm.models import Target_Installed_Package, Target_File | 25 | from orm.models import Target_Installed_Package, Target_File |
19 | from orm.models import TargetKernelFile, TargetSDKFile, Target_Image_File | 26 | from orm.models import TargetKernelFile, TargetSDKFile, Target_Image_File |
20 | from orm.models import BitbakeVersion, CustomImageRecipe | 27 | from orm.models import BitbakeVersion, CustomImageRecipe, EventLogsImports |
21 | 28 | ||
22 | from django.urls import reverse, resolve | 29 | from django.urls import reverse, resolve |
30 | from django.contrib import messages | ||
31 | |||
23 | from django.core.exceptions import ObjectDoesNotExist | 32 | from django.core.exceptions import ObjectDoesNotExist |
33 | from django.core.files.storage import FileSystemStorage | ||
34 | from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile | ||
24 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger | 35 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger |
25 | from django.http import HttpResponseNotFound, JsonResponse | 36 | from django.http import HttpResponseNotFound, JsonResponse |
26 | from django.utils import timezone | 37 | from django.utils import timezone |
38 | from django.views.generic import TemplateView | ||
27 | from datetime import timedelta, datetime | 39 | from datetime import timedelta, datetime |
28 | from toastergui.templatetags.projecttags import json as jsonfilter | 40 | from toastergui.templatetags.projecttags import json as jsonfilter |
29 | from decimal import Decimal | 41 | from decimal import Decimal |
@@ -32,6 +44,10 @@ import os | |||
32 | from os.path import dirname | 44 | from os.path import dirname |
33 | import mimetypes | 45 | import mimetypes |
34 | 46 | ||
47 | from toastergui.forms import LoadFileForm | ||
48 | |||
49 | from collections import namedtuple | ||
50 | |||
35 | import logging | 51 | import logging |
36 | 52 | ||
37 | from toastermain.logs import log_view_mixin | 53 | from toastermain.logs import log_view_mixin |
@@ -41,6 +57,7 @@ logger = logging.getLogger("toaster") | |||
41 | # Project creation and managed build enable | 57 | # Project creation and managed build enable |
42 | project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER')) | 58 | project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER')) |
43 | is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC')) | 59 | is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC')) |
60 | import_page = False | ||
44 | 61 | ||
45 | class MimeTypeFinder(object): | 62 | class MimeTypeFinder(object): |
46 | # setting this to False enables additional non-standard mimetypes | 63 | # setting this to False enables additional non-standard mimetypes |
@@ -1940,3 +1957,155 @@ if True: | |||
1940 | except (ObjectDoesNotExist, IOError): | 1957 | except (ObjectDoesNotExist, IOError): |
1941 | return toaster_render(request, "unavailable_artifact.html") | 1958 | return toaster_render(request, "unavailable_artifact.html") |
1942 | 1959 | ||
1960 | |||
1961 | class CommandLineBuilds(TemplateView): | ||
1962 | model = EventLogsImports | ||
1963 | template_name = 'command_line_builds.html' | ||
1964 | |||
1965 | def get_context_data(self, **kwargs): | ||
1966 | context = super(CommandLineBuilds, self).get_context_data(**kwargs) | ||
1967 | #get value from BB_DEFAULT_EVENTLOG defined in bitbake.conf | ||
1968 | eventlog = subprocess.check_output(['bitbake-getvar', 'BB_DEFAULT_EVENTLOG', '--value']) | ||
1969 | if eventlog: | ||
1970 | logs_dir = os.path.dirname(eventlog.decode().strip('\n')) | ||
1971 | files = os.listdir(logs_dir) | ||
1972 | imported_files = EventLogsImports.objects.all() | ||
1973 | files_list = [] | ||
1974 | |||
1975 | # Filter files that end with ".json" | ||
1976 | event_files = [] | ||
1977 | for file in files: | ||
1978 | if file.endswith(".json"): | ||
1979 | # because BB_DEFAULT_EVENTLOG is a directory, we need to check if the file is a valid eventlog | ||
1980 | with open("{}/{}".format(logs_dir, file)) as efile: | ||
1981 | content = efile.read() | ||
1982 | if 'allvariables' in content: | ||
1983 | event_files.append(file) | ||
1984 | |||
1985 | #build dict for template using db data | ||
1986 | for event_file in event_files: | ||
1987 | if imported_files.filter(name=event_file): | ||
1988 | files_list.append({ | ||
1989 | 'name': event_file, | ||
1990 | 'imported': True, | ||
1991 | 'build_id': imported_files.filter(name=event_file)[0].build_id, | ||
1992 | 'size': os.path.getsize("{}/{}".format(logs_dir, event_file)) | ||
1993 | }) | ||
1994 | else: | ||
1995 | files_list.append({ | ||
1996 | 'name': event_file, | ||
1997 | 'imported': False, | ||
1998 | 'build_id': None, | ||
1999 | 'size': os.path.getsize("{}/{}".format(logs_dir, event_file)) | ||
2000 | }) | ||
2001 | context['import_all'] = True | ||
2002 | |||
2003 | context['files'] = files_list | ||
2004 | context['dir'] = logs_dir | ||
2005 | else: | ||
2006 | context['files'] = [] | ||
2007 | context['dir'] = '' | ||
2008 | |||
2009 | # enable session variable | ||
2010 | if not self.request.session.get('file'): | ||
2011 | self.request.session['file'] = "" | ||
2012 | |||
2013 | context['form'] = LoadFileForm() | ||
2014 | context['project_enable'] = project_enable | ||
2015 | return context | ||
2016 | |||
2017 | def post(self, request, **kwargs): | ||
2018 | logs_dir = request.POST.get('dir') | ||
2019 | all_files = request.POST.get('all') | ||
2020 | |||
2021 | imported_files = EventLogsImports.objects.all() | ||
2022 | try: | ||
2023 | if all_files == 'true': | ||
2024 | # use of session variable to deactivate icon for builds in progress | ||
2025 | request.session['all_builds'] = True | ||
2026 | request.session.modified = True | ||
2027 | request.session.save() | ||
2028 | |||
2029 | files = ast.literal_eval(request.POST.get('file')) | ||
2030 | for file in files: | ||
2031 | if imported_files.filter(name=file.get('name')).exists(): | ||
2032 | imported_files.filter(name=file.get('name'))[0].imported = True | ||
2033 | else: | ||
2034 | with open("{}/{}".format(logs_dir, file.get('name'))) as eventfile: | ||
2035 | # load variables from the first line | ||
2036 | variables = None | ||
2037 | while line := eventfile.readline().strip(): | ||
2038 | try: | ||
2039 | variables = json.loads(line)['allvariables'] | ||
2040 | break | ||
2041 | except (KeyError, json.JSONDecodeError): | ||
2042 | continue | ||
2043 | if not variables: | ||
2044 | raise Exception("File content missing build variables") | ||
2045 | eventfile.seek(0) | ||
2046 | params = namedtuple('ConfigParams', ['observe_only'])(True) | ||
2047 | player = eventreplay.EventPlayer(eventfile, variables) | ||
2048 | |||
2049 | toasterui.main(player, player, params) | ||
2050 | event_log_import = EventLogsImports.objects.create(name=file.get('name'), imported=True) | ||
2051 | event_log_import.build_id = Build.objects.last().id | ||
2052 | event_log_import.save() | ||
2053 | else: | ||
2054 | if self.request.FILES.get('eventlog_file'): | ||
2055 | file = self.request.FILES['eventlog_file'] | ||
2056 | else: | ||
2057 | file = request.POST.get('file') | ||
2058 | # use of session variable to deactivate icon for build in progress | ||
2059 | request.session['file'] = file | ||
2060 | request.session['all_builds'] = False | ||
2061 | request.session.modified = True | ||
2062 | request.session.save() | ||
2063 | |||
2064 | if imported_files.filter(name=file).exists(): | ||
2065 | imported_files.filter(name=file)[0].imported = True | ||
2066 | else: | ||
2067 | if isinstance(file, InMemoryUploadedFile) or isinstance(file, TemporaryUploadedFile): | ||
2068 | variables = None | ||
2069 | while line := file.readline().strip(): | ||
2070 | try: | ||
2071 | variables = json.loads(line)['allvariables'] | ||
2072 | break | ||
2073 | except (KeyError, json.JSONDecodeError): | ||
2074 | continue | ||
2075 | if not variables: | ||
2076 | raise Exception("File content missing build variables") | ||
2077 | file.seek(0) | ||
2078 | params = namedtuple('ConfigParams', ['observe_only'])(True) | ||
2079 | player = eventreplay.EventPlayer(file, variables) | ||
2080 | if not os.path.exists('{}/{}'.format(logs_dir, file.name)): | ||
2081 | fs = FileSystemStorage(location=logs_dir) | ||
2082 | fs.save(file.name, file) | ||
2083 | toasterui.main(player, player, params) | ||
2084 | else: | ||
2085 | with open("{}/{}".format(logs_dir, file)) as eventfile: | ||
2086 | # load variables from the first line | ||
2087 | variables = None | ||
2088 | while line := eventfile.readline().strip(): | ||
2089 | try: | ||
2090 | variables = json.loads(line)['allvariables'] | ||
2091 | break | ||
2092 | except (KeyError, json.JSONDecodeError): | ||
2093 | continue | ||
2094 | if not variables: | ||
2095 | raise Exception("File content missing build variables") | ||
2096 | eventfile.seek(0) | ||
2097 | params = namedtuple('ConfigParams', ['observe_only'])(True) | ||
2098 | player = eventreplay.EventPlayer(eventfile, variables) | ||
2099 | toasterui.main(player, player, params) | ||
2100 | event_log_import = EventLogsImports.objects.create(name=file, imported=True) | ||
2101 | event_log_import.build_id = Build.objects.last().id | ||
2102 | event_log_import.save() | ||
2103 | request.session['file'] = "" | ||
2104 | except Exception: | ||
2105 | messages.add_message( | ||
2106 | self.request, | ||
2107 | messages.ERROR, | ||
2108 | "The file content is not in the correct format. Update file content or upload a different file." | ||
2109 | ) | ||
2110 | return HttpResponseRedirect("/toastergui/cmdline/") | ||
2111 | return HttpResponseRedirect('/toastergui/builds/') | ||