summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarlon Rodriguez Garcia <marlon.rodriguez-garcia@savoirfairelinux.com>2023-12-11 11:47:05 -0500
committerRichard Purdie <richard.purdie@linuxfoundation.org>2023-12-12 15:58:57 +0000
commitdf5c8d6471bf2484db61c7f180c9758fad4182e1 (patch)
treeb27ed4efa0ec97ecee080e5ae887a6e7488cd442
parent4bb222e0d71a4cb159b8a4f1a90b65b1af32ac10 (diff)
downloadpoky-df5c8d6471bf2484db61c7f180c9758fad4182e1.tar.gz
bitbake: toaster: Added new feature to import eventlogs from command line into toaster using replay functionality
Added a new button on the base template to access a new template. Added a model register the information on the builds and generate access links Added a form to include the option to load specific files Added jquery and ajax functions to block screen and redirect to build page when import eventlogs is trigger Added a new button on landing page linked to import build page, and set min-height of buttons in landing page for uniformity Removed test assertion to check command line build in content, because new button contains text Updated toaster_eventreplay to use library Fix test in test_layerdetails_page Rebased from master This feature uses the value from the variable BB_DEFAULT_EVENTLOG to read the files created by bitbake Exclude listing of files that don't contain the allvariables definitions used to replay builds This part of the feature should be revisited. Over a long period of time, the BB_DEFAULT_EVENTLOG will exponentially increase the size of the log file and cause bottlenecks when importing. (Bitbake rev: ab96cafe03d8bab33c1de09602cc62bd6974f157) Signed-off-by: Marlon Rodriguez Garcia <marlon.rodriguez-garcia@savoirfairelinux.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rwxr-xr-xbitbake/bin/toaster-eventreplay74
-rw-r--r--bitbake/lib/bb/ui/eventreplay.py86
-rw-r--r--bitbake/lib/bb/ui/toasterui.py2
-rw-r--r--bitbake/lib/toaster/orm/migrations/0021_eventlogsimports.py22
-rw-r--r--bitbake/lib/toaster/orm/models.py9
-rw-r--r--bitbake/lib/toaster/tests/browser/test_landing_page.py2
-rw-r--r--bitbake/lib/toaster/tests/browser/test_layerdetails_page.py2
-rw-r--r--bitbake/lib/toaster/toastergui/forms.py14
-rw-r--r--bitbake/lib/toaster/toastergui/static/css/default.css28
-rw-r--r--bitbake/lib/toaster/toastergui/templates/base.html3
-rw-r--r--bitbake/lib/toaster/toastergui/templates/command_line_builds.html198
-rw-r--r--bitbake/lib/toaster/toastergui/templates/landing.html10
-rw-r--r--bitbake/lib/toaster/toastergui/urls.py1
-rw-r--r--bitbake/lib/toaster/toastergui/views.py173
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
31import bb.cooker 31import bb.cooker
32from bb.ui import toasterui 32from bb.ui import toasterui
33 33from bb.ui import eventreplay
34class 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
105def main(argv): 35def 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
10import os
11import sys
12import json
13import pickle
14import codecs
15
16
17class 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
3from django.db import migrations, models
4
5
6class 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
1871class 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
1871django.db.models.signals.post_save.connect(invalidate_cache) 1880django.db.models.signals.post_save.connect(invalidate_cache)
1872django.db.models.signals.post_delete.connect(invalidate_cache) 1881django.db.models.signals.post_delete.connect(invalidate_cache)
1873django.db.models.signals.m2m_changed.connect(invalidate_cache) 1882django.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
10from django import forms
11from django.core.validators import FileExtensionValidator
12
13class 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
117function _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
9import ast
9import re 10import re
11import subprocess
12import sys
13
14import bb.cooker
15from bb.ui import toasterui
16from bb.ui import eventreplay
10 17
11from django.db.models import F, Q, Sum 18from django.db.models import F, Q, Sum
12from django.db import IntegrityError 19from django.db import IntegrityError
13from django.shortcuts import render, redirect, get_object_or_404 20from django.shortcuts import render, redirect, get_object_or_404, HttpResponseRedirect
14from django.utils.http import urlencode 21from django.utils.http import urlencode
15from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe 22from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe
16from orm.models import LogMessage, Variable, Package_Dependency, Package 23from orm.models import LogMessage, Variable, Package_Dependency, Package
17from orm.models import Task_Dependency, Package_File 24from orm.models import Task_Dependency, Package_File
18from orm.models import Target_Installed_Package, Target_File 25from orm.models import Target_Installed_Package, Target_File
19from orm.models import TargetKernelFile, TargetSDKFile, Target_Image_File 26from orm.models import TargetKernelFile, TargetSDKFile, Target_Image_File
20from orm.models import BitbakeVersion, CustomImageRecipe 27from orm.models import BitbakeVersion, CustomImageRecipe, EventLogsImports
21 28
22from django.urls import reverse, resolve 29from django.urls import reverse, resolve
30from django.contrib import messages
31
23from django.core.exceptions import ObjectDoesNotExist 32from django.core.exceptions import ObjectDoesNotExist
33from django.core.files.storage import FileSystemStorage
34from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
24from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 35from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
25from django.http import HttpResponseNotFound, JsonResponse 36from django.http import HttpResponseNotFound, JsonResponse
26from django.utils import timezone 37from django.utils import timezone
38from django.views.generic import TemplateView
27from datetime import timedelta, datetime 39from datetime import timedelta, datetime
28from toastergui.templatetags.projecttags import json as jsonfilter 40from toastergui.templatetags.projecttags import json as jsonfilter
29from decimal import Decimal 41from decimal import Decimal
@@ -32,6 +44,10 @@ import os
32from os.path import dirname 44from os.path import dirname
33import mimetypes 45import mimetypes
34 46
47from toastergui.forms import LoadFileForm
48
49from collections import namedtuple
50
35import logging 51import logging
36 52
37from toastermain.logs import log_view_mixin 53from 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
42project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER')) 58project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
43is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC')) 59is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))
60import_page = False
44 61
45class MimeTypeFinder(object): 62class 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
1961class 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/')