diff options
4 files changed, 226 insertions, 25 deletions
diff --git a/bitbake/lib/toaster/bldcontrol/bbcontroller.py b/bitbake/lib/toaster/bldcontrol/bbcontroller.py index c3beba96f0..1e58c67fcd 100644 --- a/bitbake/lib/toaster/bldcontrol/bbcontroller.py +++ b/bitbake/lib/toaster/bldcontrol/bbcontroller.py | |||
| @@ -126,6 +126,8 @@ class BuildEnvironmentController(object): | |||
| 126 | def setLayers(self,ls): | 126 | def setLayers(self,ls): |
| 127 | """ Sets the layer variables in the config file, after validating local layer paths. | 127 | """ Sets the layer variables in the config file, after validating local layer paths. |
| 128 | The layer paths must be in a list of BRLayer object | 128 | The layer paths must be in a list of BRLayer object |
| 129 | |||
| 130 | a word of attention: by convention, the first layer for any build will be poky! | ||
| 129 | """ | 131 | """ |
| 130 | raise Exception("Must override setLayers") | 132 | raise Exception("Must override setLayers") |
| 131 | 133 | ||
| @@ -165,25 +167,31 @@ class BuildEnvironmentController(object): | |||
| 165 | class ShellCmdException(Exception): | 167 | class ShellCmdException(Exception): |
| 166 | pass | 168 | pass |
| 167 | 169 | ||
| 170 | |||
| 171 | class BuildSetupException(Exception): | ||
| 172 | pass | ||
| 173 | |||
| 168 | class LocalhostBEController(BuildEnvironmentController): | 174 | class LocalhostBEController(BuildEnvironmentController): |
| 169 | """ Implementation of the BuildEnvironmentController for the localhost; | 175 | """ Implementation of the BuildEnvironmentController for the localhost; |
| 170 | this controller manages the default build directory, | 176 | this controller manages the default build directory, |
| 171 | the server setup and system start and stop for the localhost-type build environment | 177 | the server setup and system start and stop for the localhost-type build environment |
| 172 | 178 | ||
| 173 | """ | 179 | """ |
| 174 | from os.path import dirname as DN | ||
| 175 | 180 | ||
| 176 | def __init__(self, be): | 181 | def __init__(self, be): |
| 177 | super(LocalhostBEController, self).__init__(be) | 182 | super(LocalhostBEController, self).__init__(be) |
| 178 | from os.path import dirname as DN | ||
| 179 | self.dburl = settings.getDATABASE_URL() | 183 | self.dburl = settings.getDATABASE_URL() |
| 184 | self.pokydirname = None | ||
| 185 | |||
| 186 | def _shellcmd(self, command, cwd = None): | ||
| 187 | if cwd is None: | ||
| 188 | cwd = self.be.sourcedir | ||
| 180 | 189 | ||
| 181 | def _shellcmd(self, command): | 190 | p = subprocess.Popen(command, cwd = cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 182 | p = subprocess.Popen(command, cwd=self.be.sourcedir, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
| 183 | (out,err) = p.communicate() | 191 | (out,err) = p.communicate() |
| 184 | if p.returncode: | 192 | if p.returncode: |
| 185 | if len(err) == 0: | 193 | if len(err) == 0: |
| 186 | err = "command: %s" % command | 194 | err = "command: %s \n%s" % (command, out) |
| 187 | else: | 195 | else: |
| 188 | err = "command: %s \n%s" % (command, err) | 196 | err = "command: %s \n%s" % (command, err) |
| 189 | raise ShellCmdException(err) | 197 | raise ShellCmdException(err) |
| @@ -191,22 +199,20 @@ class LocalhostBEController(BuildEnvironmentController): | |||
| 191 | return out | 199 | return out |
| 192 | 200 | ||
| 193 | def _createdirpath(self, path): | 201 | def _createdirpath(self, path): |
| 202 | from os.path import dirname as DN | ||
| 194 | if not os.path.exists(DN(path)): | 203 | if not os.path.exists(DN(path)): |
| 195 | self._createdirpath(DN(path)) | 204 | self._createdirpath(DN(path)) |
| 196 | if not os.path.exists(path): | 205 | if not os.path.exists(path): |
| 197 | os.mkdir(path, 0755) | 206 | os.mkdir(path, 0755) |
| 198 | 207 | ||
| 199 | def _startBE(self): | 208 | def _startBE(self): |
| 200 | assert self.be.sourcedir and os.path.exists(self.be.sourcedir) | 209 | assert self.pokydirname and os.path.exists(self.pokydirname) |
| 201 | self._createdirpath(self.be.builddir) | 210 | self._createdirpath(self.be.builddir) |
| 202 | self._shellcmd("bash -c \"source %s/oe-init-build-env %s\"" % (self.be.sourcedir, self.be.builddir)) | 211 | self._shellcmd("bash -c \"source %s/oe-init-build-env %s\"" % (self.pokydirname, self.be.builddir)) |
| 203 | 212 | ||
| 204 | def startBBServer(self): | 213 | def startBBServer(self): |
| 205 | assert self.be.sourcedir and os.path.exists(self.be.sourcedir) | 214 | assert self.pokydirname and os.path.exists(self.pokydirname) |
| 206 | 215 | print self._shellcmd("bash -c \"source %s/oe-init-build-env %s && DATABASE_URL=%s source toaster start noweb && sleep 1\"" % (self.pokydirname, self.be.builddir, self.dburl)) | |
| 207 | self._startBE() | ||
| 208 | |||
| 209 | print self._shellcmd("bash -c \"source %s/oe-init-build-env %s && DATABASE_URL=%s source toaster start noweb && sleep 1\"" % (self.be.sourcedir, self.be.builddir, self.dburl)) | ||
| 210 | # FIXME unfortunate sleep 1 - we need to make sure that bbserver is started and the toaster ui is connected | 216 | # FIXME unfortunate sleep 1 - we need to make sure that bbserver is started and the toaster ui is connected |
| 211 | # but since they start async without any return, we just wait a bit | 217 | # but since they start async without any return, we just wait a bit |
| 212 | print "Started server" | 218 | print "Started server" |
| @@ -225,10 +231,82 @@ class LocalhostBEController(BuildEnvironmentController): | |||
| 225 | print "Stopped server" | 231 | print "Stopped server" |
| 226 | 232 | ||
| 227 | def setLayers(self, layers): | 233 | def setLayers(self, layers): |
| 234 | """ a word of attention: by convention, the first layer for any build will be poky! """ | ||
| 235 | |||
| 228 | assert self.be.sourcedir is not None | 236 | assert self.be.sourcedir is not None |
| 229 | layerconf = os.path.join(self.be.builddir, "conf/bblayers.conf") | 237 | # set layers in the layersource |
| 230 | if not os.path.exists(layerconf): | 238 | |
| 231 | raise Exception("BE is not consistent: bblayers.conf file missing at ", layerconf) | 239 | # 1. get a list of repos, and map dirpaths for each layer |
| 240 | gitrepos = {} | ||
| 241 | for layer in layers: | ||
| 242 | if not layer.giturl in gitrepos: | ||
| 243 | gitrepos[layer.giturl] = [] | ||
| 244 | gitrepos[layer.giturl].append( (layer.name, layer.dirpath, layer.commit)) | ||
| 245 | for giturl in gitrepos.keys(): | ||
| 246 | commitid = gitrepos[giturl][0][2] | ||
| 247 | for e in gitrepos[giturl]: | ||
| 248 | if commitid != e[2]: | ||
| 249 | raise BuildSetupException("More than one commit per git url, unsupported configuration") | ||
| 250 | |||
| 251 | def _getgitdirectoryname(url): | ||
| 252 | import re | ||
| 253 | components = re.split(r'[\.\/]', url) | ||
| 254 | return components[-2] if components[-1] == "git" else components[-1] | ||
| 255 | |||
| 256 | layerlist = [] | ||
| 257 | |||
| 258 | # 2. checkout the repositories | ||
| 259 | for giturl in gitrepos.keys(): | ||
| 260 | localdirname = os.path.join(self.be.sourcedir, _getgitdirectoryname(giturl)) | ||
| 261 | print "DEBUG: giturl checking out in current directory", localdirname | ||
| 262 | |||
| 263 | # make sure our directory is a git repository | ||
| 264 | if os.path.exists(localdirname): | ||
| 265 | if not giturl in self._shellcmd("git remote -v", localdirname): | ||
| 266 | raise BuildSetupException("Existing git repository at %s, but with different remotes (not '%s'). Aborting." % (localdirname, giturl)) | ||
| 267 | else: | ||
| 268 | self._shellcmd("git clone \"%s\" \"%s\"" % (giturl, localdirname)) | ||
| 269 | # checkout the needed commit | ||
| 270 | commit = gitrepos[giturl][0][2] | ||
| 271 | self._shellcmd("git fetch --all && git checkout \"%s\"" % commit , localdirname) | ||
| 272 | print "DEBUG: checked out commit ", commit, "to", localdirname | ||
| 273 | |||
| 274 | # if this is the first checkout, take the localdirname as poky dir | ||
| 275 | if self.pokydirname is None: | ||
| 276 | print "DEBUG: selected poky dir name", localdirname | ||
| 277 | self.pokydirname = localdirname | ||
| 278 | |||
| 279 | # verify our repositories | ||
| 280 | for name, dirpath, commit in gitrepos[giturl]: | ||
| 281 | localdirpath = os.path.join(localdirname, dirpath) | ||
| 282 | if not os.path.exists(localdirpath): | ||
| 283 | raise BuildSetupException("Cannot find layer git path '%s' in checked out repository '%s:%s'. Aborting." % (localdirpath, giturl, commit)) | ||
| 284 | |||
| 285 | layerlist.append(localdirpath) | ||
| 286 | |||
| 287 | print "DEBUG: current layer list ", layerlist | ||
| 288 | |||
| 289 | # 3. configure the build environment, so we have a conf/bblayers.conf | ||
| 290 | assert self.pokydirname is not None | ||
| 291 | self._startBE() | ||
| 292 | |||
| 293 | # 4. update the bblayers.conf | ||
| 294 | bblayerconf = os.path.join(self.be.builddir, "conf/bblayers.conf") | ||
| 295 | if not os.path.exists(bblayerconf): | ||
| 296 | raise BuildSetupException("BE is not consistent: bblayers.conf file missing at %s" % bblayerconf) | ||
| 297 | |||
| 298 | conflines = open(bblayerconf, "r").readlines() | ||
| 299 | |||
| 300 | bblayerconffile = open(bblayerconf, "w") | ||
| 301 | for i in xrange(len(conflines)): | ||
| 302 | if conflines[i].startswith("# line added by toaster"): | ||
| 303 | i += 2 | ||
| 304 | else: | ||
| 305 | bblayerconffile.write(conflines[i]) | ||
| 306 | |||
| 307 | bblayerconffile.write("\n# line added by toaster build control\nBBLAYERS = \"" + " ".join(layerlist) + "\"") | ||
| 308 | bblayerconffile.close() | ||
| 309 | |||
| 232 | return True | 310 | return True |
| 233 | 311 | ||
| 234 | def release(self): | 312 | def release(self): |
diff --git a/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py b/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py index dd8f84b07a..fa8c1a9906 100644 --- a/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py +++ b/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py | |||
| @@ -1,8 +1,8 @@ | |||
| 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 |
| 4 | from bldcontrol.bbcontroller import getBuildEnvironmentController, ShellCmdException | 4 | from bldcontrol.bbcontroller import getBuildEnvironmentController, ShellCmdException, BuildSetupException |
| 5 | from bldcontrol.models import BuildRequest, BuildEnvironment | 5 | from bldcontrol.models import BuildRequest, BuildEnvironment, BRError |
| 6 | import os | 6 | import os |
| 7 | 7 | ||
| 8 | class Command(NoArgsCommand): | 8 | class Command(NoArgsCommand): |
| @@ -25,6 +25,7 @@ class Command(NoArgsCommand): | |||
| 25 | return br | 25 | return br |
| 26 | 26 | ||
| 27 | def schedule(self): | 27 | def schedule(self): |
| 28 | import traceback | ||
| 28 | try: | 29 | try: |
| 29 | br = None | 30 | br = None |
| 30 | try: | 31 | try: |
| @@ -63,15 +64,19 @@ class Command(NoArgsCommand): | |||
| 63 | 64 | ||
| 64 | # cleanup to be performed by toaster when the deed is done | 65 | # cleanup to be performed by toaster when the deed is done |
| 65 | 66 | ||
| 66 | except ShellCmdException as e: | ||
| 67 | import traceback | ||
| 68 | print " EE Error executing shell command\n", e | ||
| 69 | traceback.format_exc(e) | ||
| 70 | 67 | ||
| 71 | except Exception as e: | 68 | except Exception as e: |
| 72 | import traceback | 69 | print " EE Error executing shell command\n", e |
| 73 | traceback.print_exc() | 70 | traceback.print_exc(e) |
| 74 | raise e | 71 | BRError.objects.create(req = br, |
| 72 | errtype = str(type(e)), | ||
| 73 | errmsg = str(e), | ||
| 74 | traceback = traceback.format_exc(e)) | ||
| 75 | br.state = BuildRequest.REQ_FAILED | ||
| 76 | br.save() | ||
| 77 | bec.be.lock = BuildEnvironment.LOCK_FREE | ||
| 78 | bec.be.save() | ||
| 79 | |||
| 75 | 80 | ||
| 76 | def cleanup(self): | 81 | def cleanup(self): |
| 77 | from django.utils import timezone | 82 | from django.utils import timezone |
diff --git a/bitbake/lib/toaster/bldcontrol/migrations/0005_auto__add_brerror.py b/bitbake/lib/toaster/bldcontrol/migrations/0005_auto__add_brerror.py new file mode 100644 index 0000000000..98aeb41ceb --- /dev/null +++ b/bitbake/lib/toaster/bldcontrol/migrations/0005_auto__add_brerror.py | |||
| @@ -0,0 +1,112 @@ | |||
| 1 | # -*- coding: utf-8 -*- | ||
| 2 | from south.utils import datetime_utils as datetime | ||
| 3 | from south.db import db | ||
| 4 | from south.v2 import SchemaMigration | ||
| 5 | from django.db import models | ||
| 6 | |||
| 7 | |||
| 8 | class Migration(SchemaMigration): | ||
| 9 | |||
| 10 | def forwards(self, orm): | ||
| 11 | # Adding model 'BRError' | ||
| 12 | db.create_table(u'bldcontrol_brerror', ( | ||
| 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), | ||
| 14 | ('req', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['bldcontrol.BuildRequest'])), | ||
| 15 | ('errtype', self.gf('django.db.models.fields.CharField')(max_length=100)), | ||
| 16 | ('errmsg', self.gf('django.db.models.fields.TextField')()), | ||
| 17 | ('traceback', self.gf('django.db.models.fields.TextField')()), | ||
| 18 | )) | ||
| 19 | db.send_create_signal(u'bldcontrol', ['BRError']) | ||
| 20 | |||
| 21 | |||
| 22 | def backwards(self, orm): | ||
| 23 | # Deleting model 'BRError' | ||
| 24 | db.delete_table(u'bldcontrol_brerror') | ||
| 25 | |||
| 26 | |||
| 27 | models = { | ||
| 28 | u'bldcontrol.brerror': { | ||
| 29 | 'Meta': {'object_name': 'BRError'}, | ||
| 30 | 'errmsg': ('django.db.models.fields.TextField', [], {}), | ||
| 31 | 'errtype': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
| 32 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
| 33 | 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}), | ||
| 34 | 'traceback': ('django.db.models.fields.TextField', [], {}) | ||
| 35 | }, | ||
| 36 | u'bldcontrol.brlayer': { | ||
| 37 | 'Meta': {'object_name': 'BRLayer'}, | ||
| 38 | 'commit': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
| 39 | 'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
| 40 | 'giturl': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
| 41 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
| 42 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
| 43 | 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}) | ||
| 44 | }, | ||
| 45 | u'bldcontrol.brtarget': { | ||
| 46 | 'Meta': {'object_name': 'BRTarget'}, | ||
| 47 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
| 48 | 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}), | ||
| 49 | 'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
| 50 | 'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}) | ||
| 51 | }, | ||
| 52 | u'bldcontrol.brvariable': { | ||
| 53 | 'Meta': {'object_name': 'BRVariable'}, | ||
| 54 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
| 55 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
| 56 | 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}), | ||
| 57 | 'value': ('django.db.models.fields.TextField', [], {'blank': 'True'}) | ||
| 58 | }, | ||
| 59 | u'bldcontrol.buildenvironment': { | ||
| 60 | 'Meta': {'object_name': 'BuildEnvironment'}, | ||
| 61 | 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), | ||
| 62 | 'bbaddress': ('django.db.models.fields.CharField', [], {'max_length': '254', 'blank': 'True'}), | ||
| 63 | 'bbport': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), | ||
| 64 | 'bbstate': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
| 65 | 'bbtoken': ('django.db.models.fields.CharField', [], {'max_length': '126', 'blank': 'True'}), | ||
| 66 | 'betype': ('django.db.models.fields.IntegerField', [], {}), | ||
| 67 | 'builddir': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), | ||
| 68 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
| 69 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
| 70 | 'lock': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
| 71 | 'sourcedir': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), | ||
| 72 | 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) | ||
| 73 | }, | ||
| 74 | u'bldcontrol.buildrequest': { | ||
| 75 | 'Meta': {'object_name': 'BuildRequest'}, | ||
| 76 | 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']", 'null': 'True'}), | ||
| 77 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
| 78 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
| 79 | 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), | ||
| 80 | 'state': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
| 81 | 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) | ||
| 82 | }, | ||
| 83 | u'orm.build': { | ||
| 84 | 'Meta': {'object_name': 'Build'}, | ||
| 85 | 'bitbake_version': ('django.db.models.fields.CharField', [], {'max_length': '50'}), | ||
| 86 | 'build_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
| 87 | 'completed_on': ('django.db.models.fields.DateTimeField', [], {}), | ||
| 88 | 'cooker_log_path': ('django.db.models.fields.CharField', [], {'max_length': '500'}), | ||
| 89 | 'distro': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
| 90 | 'distro_version': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
| 91 | 'errors_no': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
| 92 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
| 93 | 'machine': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
| 94 | 'outcome': ('django.db.models.fields.IntegerField', [], {'default': '2'}), | ||
| 95 | 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']", 'null': 'True'}), | ||
| 96 | 'started_on': ('django.db.models.fields.DateTimeField', [], {}), | ||
| 97 | 'timespent': ('django.db.models.fields.IntegerField', [], {'default': '0'}), | ||
| 98 | 'warnings_no': ('django.db.models.fields.IntegerField', [], {'default': '0'}) | ||
| 99 | }, | ||
| 100 | u'orm.project': { | ||
| 101 | 'Meta': {'object_name': 'Project'}, | ||
| 102 | 'branch': ('django.db.models.fields.CharField', [], {'max_length': '50'}), | ||
| 103 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
| 104 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
| 105 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
| 106 | 'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), | ||
| 107 | 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), | ||
| 108 | 'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}) | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | complete_apps = ['bldcontrol'] \ No newline at end of file | ||
diff --git a/bitbake/lib/toaster/bldcontrol/models.py b/bitbake/lib/toaster/bldcontrol/models.py index 1f253ffb22..8c271ffa94 100644 --- a/bitbake/lib/toaster/bldcontrol/models.py +++ b/bitbake/lib/toaster/bldcontrol/models.py | |||
| @@ -48,12 +48,14 @@ class BuildRequest(models.Model): | |||
| 48 | REQ_QUEUED = 1 | 48 | REQ_QUEUED = 1 |
| 49 | REQ_INPROGRESS = 2 | 49 | REQ_INPROGRESS = 2 |
| 50 | REQ_COMPLETED = 3 | 50 | REQ_COMPLETED = 3 |
| 51 | REQ_FAILED = 4 | ||
| 51 | 52 | ||
| 52 | REQUEST_STATE = ( | 53 | REQUEST_STATE = ( |
| 53 | (REQ_CREATED, "created"), | 54 | (REQ_CREATED, "created"), |
| 54 | (REQ_QUEUED, "queued"), | 55 | (REQ_QUEUED, "queued"), |
| 55 | (REQ_INPROGRESS, "in progress"), | 56 | (REQ_INPROGRESS, "in progress"), |
| 56 | (REQ_COMPLETED, "completed"), | 57 | (REQ_COMPLETED, "completed"), |
| 58 | (REQ_FAILED, "failed"), | ||
| 57 | ) | 59 | ) |
| 58 | 60 | ||
| 59 | project = models.ForeignKey(Project) | 61 | project = models.ForeignKey(Project) |
| @@ -84,4 +86,8 @@ class BRTarget(models.Model): | |||
| 84 | target = models.CharField(max_length=100) | 86 | target = models.CharField(max_length=100) |
| 85 | task = models.CharField(max_length=100, null=True) | 87 | task = models.CharField(max_length=100, null=True) |
| 86 | 88 | ||
| 87 | 89 | class BRError(models.Model): | |
| 90 | req = models.ForeignKey(BuildRequest) | ||
| 91 | errtype = models.CharField(max_length=100) | ||
| 92 | errmsg = models.TextField() | ||
| 93 | traceback = models.TextField() | ||
