diff options
author | Alexandru DAMIAN <alexandru.damian@intel.com> | 2015-02-16 17:47:07 +0000 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2015-02-20 12:58:19 +0000 |
commit | c368d83bd6b34c2420c3d1d7269d8dc2edba1ce9 (patch) | |
tree | d6a905444fe2ea0f0313bc4b848430108ac17388 | |
parent | a574f293fe16612df446d3b7fef71adcab4773e9 (diff) | |
download | poky-c368d83bd6b34c2420c3d1d7269d8dc2edba1ce9.tar.gz |
bitbake: toaster: bitbake cooker log saving and downloading
This patch brings in cooker log saving and proper download links.
* toasterui will now write the cooker log file if running in managed
mode
* the BuildRequest has a new state, REQ_ARCHIVE, indicating that the
build is completed, and the artifacts are ready to be grabbed
* the runbuild test execution commands will gather needed artifacts,
and save them to a storage directory selected during Toaster setup.
* the build dashboard, project builds and all builds pages have
permanent links for the cooker log
[YOCTO #7220]
[YOCTO #7206]
(Bitbake rev: fad80e36c9da663b000cdf2cb3c75440c6431d84)
Signed-off-by: Alexandru DAMIAN <alexandru.damian@intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
13 files changed, 335 insertions, 43 deletions
diff --git a/bitbake/lib/bb/ui/buildinfohelper.py b/bitbake/lib/bb/ui/buildinfohelper.py index 491fd1566d..1096ccf4de 100644 --- a/bitbake/lib/bb/ui/buildinfohelper.py +++ b/bitbake/lib/bb/ui/buildinfohelper.py | |||
@@ -1114,7 +1114,8 @@ class BuildInfoHelper(object): | |||
1114 | be.save() | 1114 | be.save() |
1115 | br = BuildRequest.objects.get(pk = br_id) | 1115 | br = BuildRequest.objects.get(pk = br_id) |
1116 | if errorcode == 0: | 1116 | if errorcode == 0: |
1117 | br.state = BuildRequest.REQ_COMPLETED | 1117 | # request archival of the project artifacts |
1118 | br.state = BuildRequest.REQ_ARCHIVE | ||
1118 | else: | 1119 | else: |
1119 | br.state = BuildRequest.REQ_FAILED | 1120 | br.state = BuildRequest.REQ_FAILED |
1120 | br.save() | 1121 | br.save() |
diff --git a/bitbake/lib/bb/ui/toasterui.py b/bitbake/lib/bb/ui/toasterui.py index a85ad5a06a..df9f362284 100644 --- a/bitbake/lib/bb/ui/toasterui.py +++ b/bitbake/lib/bb/ui/toasterui.py | |||
@@ -58,7 +58,12 @@ def _log_settings_from_server(server): | |||
58 | if error: | 58 | if error: |
59 | logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error) | 59 | logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error) |
60 | raise BaseException(error) | 60 | raise BaseException(error) |
61 | return includelogs, loglines | 61 | consolelogfile, error = server.runCommand(["getVariable", "BB_CONSOLELOG"]) |
62 | if error: | ||
63 | logger.error("Unable to get the value of BB_CONSOLELOG variable: %s" % error) | ||
64 | raise BaseException(error) | ||
65 | return includelogs, loglines, consolelogfile | ||
66 | |||
62 | 67 | ||
63 | def main(server, eventHandler, params ): | 68 | def main(server, eventHandler, params ): |
64 | 69 | ||
@@ -71,7 +76,7 @@ def main(server, eventHandler, params ): | |||
71 | console.setFormatter(format) | 76 | console.setFormatter(format) |
72 | logger.addHandler(console) | 77 | logger.addHandler(console) |
73 | 78 | ||
74 | includelogs, loglines = _log_settings_from_server(server) | 79 | includelogs, loglines, consolelogfile = _log_settings_from_server(server) |
75 | 80 | ||
76 | # verify and warn | 81 | # verify and warn |
77 | build_history_enabled = True | 82 | build_history_enabled = True |
@@ -96,6 +101,16 @@ def main(server, eventHandler, params ): | |||
96 | 101 | ||
97 | buildinfohelper = BuildInfoHelper(server, build_history_enabled) | 102 | buildinfohelper = BuildInfoHelper(server, build_history_enabled) |
98 | 103 | ||
104 | if buildinfohelper.brbe is not None and consolelogfile: | ||
105 | # if we are under managed mode we have no other UI and we need to write our own file | ||
106 | bb.utils.mkdirhier(os.path.dirname(consolelogfile)) | ||
107 | conlogformat = bb.msg.BBLogFormatter(format_str) | ||
108 | consolelog = logging.FileHandler(consolelogfile) | ||
109 | bb.msg.addDefaultlogFilter(consolelog) | ||
110 | consolelog.setFormatter(conlogformat) | ||
111 | logger.addHandler(consolelog) | ||
112 | |||
113 | |||
99 | while True: | 114 | while True: |
100 | try: | 115 | try: |
101 | event = eventHandler.waitEvent(0.25) | 116 | event = eventHandler.waitEvent(0.25) |
@@ -115,8 +130,12 @@ def main(server, eventHandler, params ): | |||
115 | 130 | ||
116 | if isinstance(event, (bb.build.TaskStarted, bb.build.TaskSucceeded, bb.build.TaskFailedSilent)): | 131 | if isinstance(event, (bb.build.TaskStarted, bb.build.TaskSucceeded, bb.build.TaskFailedSilent)): |
117 | buildinfohelper.update_and_store_task(event) | 132 | buildinfohelper.update_and_store_task(event) |
133 | logger.warn("Logfile for task %s" % event.logfile) | ||
118 | continue | 134 | continue |
119 | 135 | ||
136 | if isinstance(event, bb.build.TaskBase): | ||
137 | logger.info(event._message) | ||
138 | |||
120 | if isinstance(event, bb.event.LogExecTTY): | 139 | if isinstance(event, bb.event.LogExecTTY): |
121 | logger.warn(event.msg) | 140 | logger.warn(event.msg) |
122 | continue | 141 | continue |
@@ -162,7 +181,12 @@ def main(server, eventHandler, params ): | |||
162 | if isinstance(event, bb.event.CacheLoadCompleted): | 181 | if isinstance(event, bb.event.CacheLoadCompleted): |
163 | continue | 182 | continue |
164 | if isinstance(event, bb.event.MultipleProviders): | 183 | if isinstance(event, bb.event.MultipleProviders): |
184 | logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "", | ||
185 | event._item, | ||
186 | ", ".join(event._candidates)) | ||
187 | logger.info("consider defining a PREFERRED_PROVIDER entry to match %s", event._item) | ||
165 | continue | 188 | continue |
189 | |||
166 | if isinstance(event, bb.event.NoProvider): | 190 | if isinstance(event, bb.event.NoProvider): |
167 | return_value = 1 | 191 | return_value = 1 |
168 | errors = errors + 1 | 192 | errors = errors + 1 |
@@ -229,6 +253,7 @@ def main(server, eventHandler, params ): | |||
229 | buildinfohelper.store_log_event(event) | 253 | buildinfohelper.store_log_event(event) |
230 | errors += 1 | 254 | errors += 1 |
231 | errorcode = 1 | 255 | errorcode = 1 |
256 | logger.error("Command execution failed: %s", event.error) | ||
232 | 257 | ||
233 | buildinfohelper.update_build_information(event, errors, warnings, taskfailures) | 258 | buildinfohelper.update_build_information(event, errors, warnings, taskfailures) |
234 | buildinfohelper.close(errorcode) | 259 | buildinfohelper.close(errorcode) |
diff --git a/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py b/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py index 5d80bc7155..1ff5c92833 100644 --- a/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py +++ b/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py | |||
@@ -62,6 +62,23 @@ class Command(NoArgsCommand): | |||
62 | 62 | ||
63 | 63 | ||
64 | def handle(self, **options): | 64 | def handle(self, **options): |
65 | # verify that we have a settings for downloading artifacts | ||
66 | while ToasterSetting.objects.filter(name="ARTIFACTS_STORAGE_DIR").count() == 0: | ||
67 | guessedpath = os.getcwd() + "/toaster_build_artifacts/" | ||
68 | print("Toaster needs to know in which directory it can download build log files and other artifacts.\n Toaster suggests \"%s\"." % guessedpath) | ||
69 | artifacts_storage_dir = raw_input(" Press Enter to select \"%s\" or type the full path to a different directory: " % guessedpath) | ||
70 | if len(artifacts_storage_dir) == 0: | ||
71 | artifacts_storage_dir = guessedpath | ||
72 | if len(artifacts_storage_dir) > 0 and artifacts_storage_dir.startswith("/"): | ||
73 | try: | ||
74 | os.makedirs(artifacts_storage_dir) | ||
75 | except OSError as ose: | ||
76 | if "File exists" in str(ose): | ||
77 | pass | ||
78 | else: | ||
79 | raise ose | ||
80 | ToasterSetting.objects.create(name="ARTIFACTS_STORAGE_DIR", value=artifacts_storage_dir) | ||
81 | |||
65 | self.guesspath = DN(DN(DN(DN(DN(DN(DN(__file__))))))) | 82 | self.guesspath = DN(DN(DN(DN(DN(DN(DN(__file__))))))) |
66 | # refuse to start if we have no build environments | 83 | # refuse to start if we have no build environments |
67 | while BuildEnvironment.objects.count() == 0: | 84 | while BuildEnvironment.objects.count() == 0: |
diff --git a/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py b/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py index 3de582cc86..808318f14f 100644 --- a/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py +++ b/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py | |||
@@ -1,6 +1,6 @@ | |||
1 | from django.core.management.base import NoArgsCommand, CommandError | 1 | from django.core.management.base import NoArgsCommand, CommandError |
2 | from django.db import transaction | 2 | from django.db import transaction |
3 | from orm.models import Build | 3 | from orm.models import Build, ToasterSetting |
4 | from bldcontrol.bbcontroller import getBuildEnvironmentController, ShellCmdException, BuildSetupException | 4 | from bldcontrol.bbcontroller import getBuildEnvironmentController, ShellCmdException, BuildSetupException |
5 | from bldcontrol.models import BuildRequest, BuildEnvironment, BRError, BRVariable | 5 | from bldcontrol.models import BuildRequest, BuildEnvironment, BRError, BRVariable |
6 | import os | 6 | import os |
@@ -93,7 +93,33 @@ class Command(NoArgsCommand): | |||
93 | bec.be.lock = BuildEnvironment.LOCK_FREE | 93 | bec.be.lock = BuildEnvironment.LOCK_FREE |
94 | bec.be.save() | 94 | bec.be.save() |
95 | 95 | ||
96 | def archive(self): | ||
97 | ''' archives data from the builds ''' | ||
98 | artifact_storage_dir = ToasterSetting.objects.get(name="ARTIFACTS_STORAGE_DIR").value | ||
99 | for br in BuildRequest.objects.filter(state = BuildRequest.REQ_ARCHIVE): | ||
100 | # save cooker log | ||
101 | if br.build == None: | ||
102 | br.state = BuildRequest.REQ_FAILED | ||
103 | br.save() | ||
104 | continue | ||
105 | build_artifact_storage_dir = os.path.join(artifact_storage_dir, "%d" % br.build.pk) | ||
106 | try: | ||
107 | os.makedirs(build_artifact_storage_dir) | ||
108 | except OSError as ose: | ||
109 | if "File exists" in str(ose): | ||
110 | pass | ||
111 | else: | ||
112 | raise ose | ||
113 | |||
114 | file_name = os.path.join(build_artifact_storage_dir, "cooker_log.txt") | ||
115 | try: | ||
116 | with open(file_name, "w") as f: | ||
117 | f.write(br.environment.get_artifact(br.build.cooker_log_path).read()) | ||
118 | except IOError: | ||
119 | os.unlink(file_name) | ||
96 | 120 | ||
121 | br.state = BuildRequest.REQ_COMPLETED | ||
122 | br.save() | ||
97 | 123 | ||
98 | def cleanup(self): | 124 | def cleanup(self): |
99 | from django.utils import timezone | 125 | from django.utils import timezone |
@@ -104,4 +130,5 @@ class Command(NoArgsCommand): | |||
104 | 130 | ||
105 | def handle_noargs(self, **options): | 131 | def handle_noargs(self, **options): |
106 | self.cleanup() | 132 | self.cleanup() |
133 | self.archive() | ||
107 | self.schedule() | 134 | self.schedule() |
diff --git a/bitbake/lib/toaster/bldcontrol/migrations/0008_brarchive.py b/bitbake/lib/toaster/bldcontrol/migrations/0008_brarchive.py new file mode 100644 index 0000000000..f5469607f3 --- /dev/null +++ b/bitbake/lib/toaster/bldcontrol/migrations/0008_brarchive.py | |||
@@ -0,0 +1,138 @@ | |||
1 | # -*- coding: utf-8 -*- | ||
2 | from south.utils import datetime_utils as datetime | ||
3 | from south.db import db | ||
4 | from south.v2 import DataMigration | ||
5 | from django.db import models | ||
6 | |||
7 | class Migration(DataMigration): | ||
8 | # ids that cannot be imported from BuildRequest | ||
9 | |||
10 | def forwards(self, orm): | ||
11 | REQ_COMPLETED = 3 | ||
12 | REQ_ARCHIVE = 6 | ||
13 | "Write your forwards methods here." | ||
14 | # Note: Don't use "from appname.models import ModelName". | ||
15 | # Use orm.ModelName to refer to models in this application, | ||
16 | # and orm['appname.ModelName'] for models in other applications. | ||
17 | orm.BuildRequest.objects.filter(state=REQ_COMPLETED).update(state=REQ_ARCHIVE) | ||
18 | |||
19 | def backwards(self, orm): | ||
20 | REQ_COMPLETED = 3 | ||
21 | REQ_ARCHIVE = 6 | ||
22 | "Write your backwards methods here." | ||
23 | orm.BuildRequest.objects.filter(state=REQ_ARCHIVE).update(state=REQ_COMPLETED) | ||
24 | |||
25 | models = { | ||
26 | u'bldcontrol.brbitbake': { | ||
27 | 'Meta': {'object_name': 'BRBitbake'}, | ||
28 | 'commit': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
29 | 'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
30 | 'giturl': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
31 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
32 | 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']", 'unique': 'True'}) | ||
33 | }, | ||
34 | u'bldcontrol.brerror': { | ||
35 | 'Meta': {'object_name': 'BRError'}, | ||
36 | 'errmsg': ('django.db.models.fields.TextField', [], {}), | ||
37 | 'errtype': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
38 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
39 | 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}), | ||
40 | 'traceback': ('django.db.models.fields.TextField', [], {}) | ||
41 | }, | ||
42 | u'bldcontrol.brlayer': { | ||
43 | 'Meta': {'object_name': 'BRLayer'}, | ||
44 | 'commit': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
45 | 'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
46 | 'giturl': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
47 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
48 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
49 | 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}) | ||
50 | }, | ||
51 | u'bldcontrol.brtarget': { | ||
52 | 'Meta': {'object_name': 'BRTarget'}, | ||
53 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
54 | 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}), | ||
55 | 'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
56 | 'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}) | ||
57 | }, | ||
58 | u'bldcontrol.brvariable': { | ||
59 | 'Meta': {'object_name': 'BRVariable'}, | ||
60 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
61 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
62 | 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}), | ||
63 | 'value': ('django.db.models.fields.TextField', [], {'blank': 'True'}) | ||
64 | }, | ||
65 | u'bldcontrol.buildenvironment': { | ||
66 | 'Meta': {'object_name': 'BuildEnvironment'}, | ||
67 | 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
68 | 'bbaddress': ('django.db.models.fields.CharField', [], {'max_length': '254', 'blank': 'True'}), | ||
69 | 'bbport': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), | ||
70 | 'bbstate': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
71 | 'bbtoken': ('django.db.models.fields.CharField', [], {'max_length': '126', 'blank': 'True'}), | ||
72 | 'betype': ('django.db.models.fields.IntegerField', [], {}), | ||
73 | 'builddir': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), | ||
74 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
75 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
76 | 'lock': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
77 | 'sourcedir': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), | ||
78 | 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) | ||
79 | }, | ||
80 | u'bldcontrol.buildrequest': { | ||
81 | 'Meta': {'object_name': 'BuildRequest'}, | ||
82 | 'build': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['orm.Build']", 'unique': 'True', 'null': 'True'}), | ||
83 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
84 | 'environment': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildEnvironment']", 'null': 'True'}), | ||
85 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
86 | 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), | ||
87 | 'state': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
88 | 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) | ||
89 | }, | ||
90 | u'orm.bitbakeversion': { | ||
91 | 'Meta': {'object_name': 'BitbakeVersion'}, | ||
92 | 'branch': ('django.db.models.fields.CharField', [], {'max_length': '32'}), | ||
93 | 'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '255'}), | ||
94 | 'giturl': ('django.db.models.fields.URLField', [], {'max_length': '200'}), | ||
95 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
96 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) | ||
97 | }, | ||
98 | u'orm.build': { | ||
99 | 'Meta': {'object_name': 'Build'}, | ||
100 | 'bitbake_version': ('django.db.models.fields.CharField', [], {'max_length': '50'}), | ||
101 | 'build_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
102 | 'completed_on': ('django.db.models.fields.DateTimeField', [], {}), | ||
103 | 'cooker_log_path': ('django.db.models.fields.CharField', [], {'max_length': '500'}), | ||
104 | 'distro': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
105 | 'distro_version': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
106 | 'errors_no': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
107 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
108 | 'machine': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
109 | 'outcome': ('django.db.models.fields.IntegerField', [], {'default': '2'}), | ||
110 | 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']", 'null': 'True'}), | ||
111 | 'started_on': ('django.db.models.fields.DateTimeField', [], {}), | ||
112 | 'timespent': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
113 | 'warnings_no': ('django.db.models.fields.IntegerField', [], {'default': '0'}) | ||
114 | }, | ||
115 | u'orm.project': { | ||
116 | 'Meta': {'object_name': 'Project'}, | ||
117 | 'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']"}), | ||
118 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
119 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
120 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
121 | 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']"}), | ||
122 | 'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), | ||
123 | 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), | ||
124 | 'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}) | ||
125 | }, | ||
126 | u'orm.release': { | ||
127 | 'Meta': {'object_name': 'Release'}, | ||
128 | 'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']"}), | ||
129 | 'branch_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50'}), | ||
130 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), | ||
131 | 'helptext': ('django.db.models.fields.TextField', [], {'null': 'True'}), | ||
132 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
133 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) | ||
134 | } | ||
135 | } | ||
136 | |||
137 | complete_apps = ['bldcontrol'] | ||
138 | symmetrical = True | ||
diff --git a/bitbake/lib/toaster/bldcontrol/models.py b/bitbake/lib/toaster/bldcontrol/models.py index dc4afca2f7..25d94cd3fe 100644 --- a/bitbake/lib/toaster/bldcontrol/models.py +++ b/bitbake/lib/toaster/bldcontrol/models.py | |||
@@ -94,6 +94,7 @@ class BuildRequest(models.Model): | |||
94 | REQ_COMPLETED = 3 | 94 | REQ_COMPLETED = 3 |
95 | REQ_FAILED = 4 | 95 | REQ_FAILED = 4 |
96 | REQ_DELETED = 5 | 96 | REQ_DELETED = 5 |
97 | REQ_ARCHIVE = 6 | ||
97 | 98 | ||
98 | REQUEST_STATE = ( | 99 | REQUEST_STATE = ( |
99 | (REQ_CREATED, "created"), | 100 | (REQ_CREATED, "created"), |
@@ -102,6 +103,7 @@ class BuildRequest(models.Model): | |||
102 | (REQ_COMPLETED, "completed"), | 103 | (REQ_COMPLETED, "completed"), |
103 | (REQ_FAILED, "failed"), | 104 | (REQ_FAILED, "failed"), |
104 | (REQ_DELETED, "deleted"), | 105 | (REQ_DELETED, "deleted"), |
106 | (REQ_ARCHIVE, "archive"), | ||
105 | ) | 107 | ) |
106 | 108 | ||
107 | search_allowed_fields = ("brtarget__target",) | 109 | search_allowed_fields = ("brtarget__target",) |
diff --git a/bitbake/lib/toaster/toastergui/templates/build.html b/bitbake/lib/toaster/toastergui/templates/build.html index e71e38feb9..684ec65884 100644 --- a/bitbake/lib/toaster/toastergui/templates/build.html +++ b/bitbake/lib/toaster/toastergui/templates/build.html | |||
@@ -40,7 +40,9 @@ | |||
40 | <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work --> | 40 | <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work --> |
41 | {% for build in objects %} | 41 | {% for build in objects %} |
42 | <tr class="data"> | 42 | <tr class="data"> |
43 | <td class="outcome"><a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a></td> | 43 | <td class="outcome"> |
44 | <a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a> | ||
45 | </td> | ||
44 | <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td> | 46 | <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td> |
45 | <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> | 47 | <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> |
46 | <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td> | 48 | <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td> |
@@ -61,11 +63,6 @@ | |||
61 | <td class="errors_no"> | 63 | <td class="errors_no"> |
62 | {% if build.errors_no %} | 64 | {% if build.errors_no %} |
63 | <a class="errors_no error" href="{% url "builddashboard" build.id %}#errors">{{build.errors_no}} error{{build.errors_no|pluralize}}</a> | 65 | <a class="errors_no error" href="{% url "builddashboard" build.id %}#errors">{{build.errors_no}} error{{build.errors_no|pluralize}}</a> |
64 | {% if MANAGED and build.project %} | ||
65 | <a href="{% url 'build_artifact' build.id "cookerlog" build.id %}"> | ||
66 | <i class="icon-download-alt" title="" data-original-title="Download build log"></i> | ||
67 | </a> | ||
68 | {% endif %} | ||
69 | {%endif%} | 66 | {%endif%} |
70 | </td> | 67 | </td> |
71 | <td class="warnings_no">{% if build.warnings_no %}<a class="warnings_no warning" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>{%endif%}</td> | 68 | <td class="warnings_no">{% if build.warnings_no %}<a class="warnings_no warning" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>{%endif%}</td> |
diff --git a/bitbake/lib/toaster/toastergui/templates/builddashboard.html b/bitbake/lib/toaster/toastergui/templates/builddashboard.html index 2458cdb6d1..c0898e291d 100644 --- a/bitbake/lib/toaster/toastergui/templates/builddashboard.html +++ b/bitbake/lib/toaster/toastergui/templates/builddashboard.html | |||
@@ -36,7 +36,11 @@ | |||
36 | {% endif %} | 36 | {% endif %} |
37 | <span > <i class="icon-warning-sign yellow"></i><strong><a href="#warnings" class="warning show-warnings"> {{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a></strong></span> | 37 | <span > <i class="icon-warning-sign yellow"></i><strong><a href="#warnings" class="warning show-warnings"> {{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a></strong></span> |
38 | {% endif %} | 38 | {% endif %} |
39 | <span class="pull-right">Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent|sectohms }}</a></span> | 39 | <span class="pull-right">Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent|sectohms }}</a> |
40 | {% if MANAGED and build.project %} | ||
41 | <a class="btn {%if build.outcome == build.SUCCEEDED%}btn-success{%else%}btn-danger{%endif%} pull-right" href="{% url 'build_artifact' build.id "cookerlog" build.id %}">Download build log</a> | ||
42 | {% endif %} | ||
43 | </span> | ||
40 | {%endif%} | 44 | {%endif%} |
41 | </div> | 45 | </div> |
42 | {% if build.toaster_exceptions.count > 0 %} | 46 | {% if build.toaster_exceptions.count > 0 %} |
@@ -54,10 +58,7 @@ | |||
54 | <div class="accordion span10 pull-right" id="errors"> | 58 | <div class="accordion span10 pull-right" id="errors"> |
55 | <div class="accordion-group"> | 59 | <div class="accordion-group"> |
56 | <div class="accordion-heading"> | 60 | <div class="accordion-heading"> |
57 | {% if MANAGED and build.project %} | 61 | <a class="accordion-toggle error toggle-errors"> |
58 | <a class="btn btn-large pull-right" href="{% url 'build_artifact' build.id "cookerlog" build.id %}" style="margin:15px;">Download build log</a> | ||
59 | {% endif %} | ||
60 | <a class="accordion-toggle error toggle-errors"> | ||
61 | <h2 id="error-toggle"> | 62 | <h2 id="error-toggle"> |
62 | <i class="icon-minus-sign"></i> | 63 | <i class="icon-minus-sign"></i> |
63 | {{build.errors_no}} error{{build.errors_no|pluralize}} | 64 | {{build.errors_no}} error{{build.errors_no|pluralize}} |
diff --git a/bitbake/lib/toaster/toastergui/templates/managed_builds.html b/bitbake/lib/toaster/toastergui/templates/managed_builds.html index 121ad07898..a4db55b967 100644 --- a/bitbake/lib/toaster/toastergui/templates/managed_builds.html +++ b/bitbake/lib/toaster/toastergui/templates/managed_builds.html | |||
@@ -46,7 +46,14 @@ | |||
46 | <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work --> | 46 | <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work --> |
47 | {% for buildrequest in objects %}{% if buildrequest.build %} {% with build=buildrequest.build %} {# if we have a build, just display it #} | 47 | {% for buildrequest in objects %}{% if buildrequest.build %} {% with build=buildrequest.build %} {# if we have a build, just display it #} |
48 | <tr class="data"> | 48 | <tr class="data"> |
49 | <td class="outcome"><a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a></td> | 49 | <td class="outcome"><a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a> |
50 | {% if build.project %} | ||
51 | <a href="{% url 'build_artifact' build.id "cookerlog" build.id %}"> | ||
52 | <i class="icon-download-alt" title="" data-original-title="Download build log"></i> | ||
53 | </a> | ||
54 | {% endif %} | ||
55 | |||
56 | </td> | ||
50 | <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td> | 57 | <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td> |
51 | <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> | 58 | <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> |
52 | <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td> | 59 | <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td> |
diff --git a/bitbake/lib/toaster/toastergui/templates/projectbuilds.html b/bitbake/lib/toaster/toastergui/templates/projectbuilds.html index 02d166341f..afcf5191b5 100644 --- a/bitbake/lib/toaster/toastergui/templates/projectbuilds.html +++ b/bitbake/lib/toaster/toastergui/templates/projectbuilds.html | |||
@@ -49,7 +49,14 @@ | |||
49 | <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work --> | 49 | <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work --> |
50 | {% for br in objects %}{% if br.build %} {% with build=br.build %} {# if we have a build, just display it #} | 50 | {% for br in objects %}{% if br.build %} {% with build=br.build %} {# if we have a build, just display it #} |
51 | <tr class="data"> | 51 | <tr class="data"> |
52 | <td class="outcome"><a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a></td> | 52 | <td class="outcome"><a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a> |
53 | {% if build.project %} | ||
54 | <a href="{% url 'build_artifact' build.id "cookerlog" build.id %}"> | ||
55 | <i class="icon-download-alt" title="" data-original-title="Download build log"></i> | ||
56 | </a> | ||
57 | {% endif %} | ||
58 | </td> | ||
59 | |||
53 | <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td> | 60 | <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td> |
54 | <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> | 61 | <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> |
55 | <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td> | 62 | <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td> |
@@ -70,11 +77,6 @@ | |||
70 | <td class="errors_no"> | 77 | <td class="errors_no"> |
71 | {% if build.errors_no %} | 78 | {% if build.errors_no %} |
72 | <a class="errors_no error" href="{% url "builddashboard" build.id %}#errors">{{build.errors_no}} error{{build.errors_no|pluralize}}</a> | 79 | <a class="errors_no error" href="{% url "builddashboard" build.id %}#errors">{{build.errors_no}} error{{build.errors_no|pluralize}}</a> |
73 | {% if MANAGED and build.project %} | ||
74 | <a href="{% url 'build_artifact' build.id "cookerlog" build.id %}"> | ||
75 | <i class="icon-download-alt" title="" data-original-title="Download build log"></i> | ||
76 | </a> | ||
77 | {% endif %} | ||
78 | {%endif%} | 80 | {%endif%} |
79 | </td> | 81 | </td> |
80 | <td class="warnings_no">{% if build.warnings_no %}<a class="warnings_no warning" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>{%endif%}</td> | 82 | <td class="warnings_no">{% if build.warnings_no %}<a class="warnings_no warning" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>{%endif%}</td> |
diff --git a/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html b/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html new file mode 100644 index 0000000000..c93d4257db --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html | |||
@@ -0,0 +1,17 @@ | |||
1 | {% extends "base.html" %} | ||
2 | {% load projecttags %} | ||
3 | {% load humanize %} | ||
4 | {% load static %} | ||
5 | |||
6 | {% block pagecontent %} | ||
7 | <div class="section"> | ||
8 | </div> | ||
9 | <div class="row-fluid"> | ||
10 | |||
11 | <div class="alert alert-info"> | ||
12 | <p class="lead"> The artifact you are seeking to download is not available. We are sorry. You can <a href="javascript:window.history.back()">go back</a> | ||
13 | </p> | ||
14 | </div> | ||
15 | </div> | ||
16 | {% endblock %} | ||
17 | |||
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py index b5ff9b1d53..eb323ec81d 100755 --- a/bitbake/lib/toaster/toastergui/views.py +++ b/bitbake/lib/toaster/toastergui/views.py | |||
@@ -2914,9 +2914,6 @@ if toastermain.settings.MANAGED: | |||
2914 | if artifact_type == "imagefile": | 2914 | if artifact_type == "imagefile": |
2915 | file_name = Target_Image_File.objects.get(target__build = b, pk = artifact_id).file_name | 2915 | file_name = Target_Image_File.objects.get(target__build = b, pk = artifact_id).file_name |
2916 | 2916 | ||
2917 | elif artifact_type == "cookerlog": | ||
2918 | file_name = b.cooker_log_path | ||
2919 | |||
2920 | elif artifact_type == "buildartifact": | 2917 | elif artifact_type == "buildartifact": |
2921 | file_name = BuildArtifact.objects.get(build = b, pk = artifact_id).file_name | 2918 | file_name = BuildArtifact.objects.get(build = b, pk = artifact_id).file_name |
2922 | 2919 | ||
@@ -2935,26 +2932,87 @@ if toastermain.settings.MANAGED: | |||
2935 | 2932 | ||
2936 | 2933 | ||
2937 | def build_artifact(request, build_id, artifact_type, artifact_id): | 2934 | def build_artifact(request, build_id, artifact_type, artifact_id): |
2938 | b = Build.objects.get(pk=build_id) | 2935 | if artifact_type in ["cookerlog"]: |
2939 | if b.buildrequest is None or b.buildrequest.environment is None: | 2936 | # these artifacts are saved after building, so they are on the server itself |
2940 | raise Exception("Artifact not available for download (missing build request or build environment)") | 2937 | def _mimetype_for_artifact(path): |
2938 | try: | ||
2939 | import magic | ||
2940 | |||
2941 | # fair warning: this is a mess; there are multiple competing and incompatible | ||
2942 | # magic modules floating around, so we try some of the most common combinations | ||
2943 | |||
2944 | try: # we try ubuntu's python-magic 5.4 | ||
2945 | m = magic.open(magic.MAGIC_MIME_TYPE) | ||
2946 | m.load() | ||
2947 | return m.file(path) | ||
2948 | except AttributeError: | ||
2949 | pass | ||
2950 | |||
2951 | try: # we try python-magic 0.4.6 | ||
2952 | m = magic.Magic(magic.MAGIC_MIME) | ||
2953 | return m.from_file(path) | ||
2954 | except AttributeError: | ||
2955 | pass | ||
2956 | |||
2957 | try: # we try pip filemagic 1.6 | ||
2958 | m = magic.Magic(flags=magic.MAGIC_MIME_TYPE) | ||
2959 | return m.id_filename(path) | ||
2960 | except AttributeError: | ||
2961 | pass | ||
2962 | |||
2963 | return "binary/octet-stream" | ||
2964 | except ImportError: | ||
2965 | return "binary/octet-stream" | ||
2966 | try: | ||
2967 | # match code with runbuilds.Command.archive() | ||
2968 | build_artifact_storage_dir = os.path.join(ToasterSetting.objects.get(name="ARTIFACTS_STORAGE_DIR").value, "%d" % int(build_id)) | ||
2969 | file_name = os.path.join(build_artifact_storage_dir, "cooker_log.txt") | ||
2970 | |||
2971 | fsock = open(file_name, "r") | ||
2972 | content_type=_mimetype_for_artifact(file_name) | ||
2941 | 2973 | ||
2942 | file_name = _file_name_for_artifact(b, artifact_type, artifact_id) | 2974 | response = HttpResponse(fsock, content_type = content_type) |
2943 | fsock = None | 2975 | |
2944 | content_type='application/force-download' | 2976 | response['Content-Disposition'] = 'attachment; filename=' + os.path.basename(file_name) |
2977 | return response | ||
2978 | except IOError: | ||
2979 | context = { | ||
2980 | 'build' : Build.objects.get(pk = build_id), | ||
2981 | } | ||
2982 | return render(request, "unavailable_artifact.html", context) | ||
2945 | 2983 | ||
2946 | if file_name is None: | ||
2947 | raise Exception("Could not handle artifact %s id %s" % (artifact_type, artifact_id)) | ||
2948 | else: | 2984 | else: |
2949 | content_type = b.buildrequest.environment.get_artifact_type(file_name) | 2985 | # retrieve the artifact directly from the build environment |
2950 | fsock = b.buildrequest.environment.get_artifact(file_name) | 2986 | return _get_be_artifact(request, build_id, artifact_type, artifact_id) |
2951 | file_name = os.path.basename(file_name) # we assume that the build environment system has the same path conventions as host | ||
2952 | 2987 | ||
2953 | response = HttpResponse(fsock, content_type = content_type) | ||
2954 | 2988 | ||
2955 | # returns a file from the environment | 2989 | def _get_be_artifact(request, build_id, artifact_type, artifact_id): |
2956 | response['Content-Disposition'] = 'attachment; filename=' + file_name | 2990 | try: |
2957 | return response | 2991 | b = Build.objects.get(pk=build_id) |
2992 | if b.buildrequest is None or b.buildrequest.environment is None: | ||
2993 | raise Exception("Artifact not available for download (missing build request or build environment)") | ||
2994 | |||
2995 | file_name = _file_name_for_artifact(b, artifact_type, artifact_id) | ||
2996 | fsock = None | ||
2997 | content_type='application/force-download' | ||
2998 | |||
2999 | if file_name is None: | ||
3000 | raise Exception("Could not handle artifact %s id %s" % (artifact_type, artifact_id)) | ||
3001 | else: | ||
3002 | content_type = b.buildrequest.environment.get_artifact_type(file_name) | ||
3003 | fsock = b.buildrequest.environment.get_artifact(file_name) | ||
3004 | file_name = os.path.basename(file_name) # we assume that the build environment system has the same path conventions as host | ||
3005 | |||
3006 | response = HttpResponse(fsock, content_type = content_type) | ||
3007 | |||
3008 | # returns a file from the environment | ||
3009 | response['Content-Disposition'] = 'attachment; filename=' + file_name | ||
3010 | return response | ||
3011 | except IOError: | ||
3012 | context = { | ||
3013 | 'build' : Build.objects.get(pk = build_id), | ||
3014 | } | ||
3015 | return render(request, "unavailable_artifact.html", context) | ||
2958 | 3016 | ||
2959 | 3017 | ||
2960 | 3018 | ||
diff --git a/bitbake/lib/toaster/toastermain/urls.py b/bitbake/lib/toaster/toastermain/urls.py index f66f11dcde..395c4e8c34 100644 --- a/bitbake/lib/toaster/toastermain/urls.py +++ b/bitbake/lib/toaster/toastermain/urls.py | |||
@@ -50,12 +50,12 @@ import toastermain.settings | |||
50 | 50 | ||
51 | if toastermain.settings.FRESH_ENABLED: | 51 | if toastermain.settings.FRESH_ENABLED: |
52 | urlpatterns.insert(1, url(r'', include('fresh.urls'))) | 52 | urlpatterns.insert(1, url(r'', include('fresh.urls'))) |
53 | logger.info("Enabled django-fresh extension") | 53 | #logger.info("Enabled django-fresh extension") |
54 | 54 | ||
55 | if toastermain.settings.DEBUG_PANEL_ENABLED: | 55 | if toastermain.settings.DEBUG_PANEL_ENABLED: |
56 | import debug_toolbar | 56 | import debug_toolbar |
57 | urlpatterns.insert(1, url(r'', include(debug_toolbar.urls))) | 57 | urlpatterns.insert(1, url(r'', include(debug_toolbar.urls))) |
58 | logger.info("Enabled django_toolbar extension") | 58 | #logger.info("Enabled django_toolbar extension") |
59 | 59 | ||
60 | 60 | ||
61 | if toastermain.settings.MANAGED: | 61 | if toastermain.settings.MANAGED: |
@@ -86,4 +86,4 @@ for t in os.walk(os.path.dirname(currentdir)): | |||
86 | logger.warn("Module \'%s\' has a regexp conflict, was not added to the urlpatterns" % modulename) | 86 | logger.warn("Module \'%s\' has a regexp conflict, was not added to the urlpatterns" % modulename) |
87 | 87 | ||
88 | from pprint import pformat | 88 | from pprint import pformat |
89 | logger.debug("urlpatterns list %s", pformat(urlpatterns)) | 89 | #logger.debug("urlpatterns list %s", pformat(urlpatterns)) |