summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Callaghan <dcallagh@redhat.com>2014-02-18 17:48:47 +1000
committerGerrit Code Review <gerrit@beaker-project.org>2014-02-21 07:04:47 +0000
commit07869a2c0e33c56ac0f22c9ae5c7402d481cfceb (patch)
tree98be749174bbd21bcdd20402cf8db9b0730ae390
parent04641b426c70048424ae20ec8a1c8f06d03dbfd3 (diff)
external tasks
A recipe task no longer needs to reference the task library, instead the job XML can specify a "fetch URL". Based on a patch contributed by Bill Peck <bpeck@redhat.com>. Bug: 1057459 Change-Id: Ib3b5415c623c01343228447d3cf4a0298a9bcd70
-rw-r--r--Common/bkr/common/schema/beaker-job.rng49
-rw-r--r--IntegrationTests/src/bkr/inttest/client/test_machine_test.py4
-rw-r--r--IntegrationTests/src/bkr/inttest/complete-job.xml4
-rw-r--r--IntegrationTests/src/bkr/inttest/data_setup.py4
-rw-r--r--IntegrationTests/src/bkr/inttest/server/selenium/test_jobs_xmlrpc.py60
-rw-r--r--IntegrationTests/src/bkr/inttest/server/test_jobs.py6
-rw-r--r--IntegrationTests/src/bkr/inttest/server/test_model.py2
-rw-r--r--IntegrationTests/src/bkr/inttest/server/test_reporting_queries.py3
-rw-r--r--Server/bkr/server/job_matrix.py15
-rw-r--r--Server/bkr/server/jobs.py18
-rw-r--r--Server/bkr/server/jobxml.py12
-rw-r--r--Server/bkr/server/mail.py2
-rw-r--r--Server/bkr/server/model/scheduler.py111
-rw-r--r--Server/bkr/server/model/tasklibrary.py7
-rw-r--r--Server/bkr/server/tasks.py2
-rw-r--r--Server/bkr/server/templates/tasks_widget.kid2
-rw-r--r--Server/bkr/server/watchdog.py2
-rw-r--r--documentation/whats-new/next/external-tasks.rst36
18 files changed, 269 insertions, 70 deletions
diff --git a/Common/bkr/common/schema/beaker-job.rng b/Common/bkr/common/schema/beaker-job.rng
index 9a393e6..75e2825 100644
--- a/Common/bkr/common/schema/beaker-job.rng
+++ b/Common/bkr/common/schema/beaker-job.rng
@@ -348,11 +348,50 @@
</define>
<define name="task">
<element name="task">
- <attribute name="name">
- <a:documentation xml:lang="en">
- Name of the task. Should be a valid name from the Task Library.
- </a:documentation>
- </attribute>
+ <choice>
+ <attribute name="name">
+ <a:documentation xml:lang="en">
+ Name of the task. When no fetch element is specified, the named
+ task must exist in Beaker's task library.
+ </a:documentation>
+ </attribute>
+ <group>
+ <element name="fetch">
+ <attribute name="url">
+ <a:documentation xml:lang="en">
+ URL from which the harness should fetch the task. Refer to the
+ harness documentation for supported URL schemes and task
+ formats.
+ </a:documentation>
+ <data type="anyURI"/>
+ </attribute>
+ <optional>
+ <attribute name="subdir">
+ <a:documentation xml:lang="en">
+ If the fetch URL points at an archive or repository
+ containing multiple tasks, this attribute identifies which
+ subtree the harness should use to find the task.
+ The default value is the empty string ("") which means that
+ the task is at the root of the archive.
+ </a:documentation>
+ </attribute>
+ </optional>
+ </element>
+ <optional>
+ <attribute name="name">
+ <a:documentation xml:lang="en">
+ Name of the task. When the fetch element is specified, the task
+ name is only used to report results (the task need not exist in
+ Beaker's task library).
+
+ If the task name is not given, it defaults to the fetch URL
+ combined with the subdirectory (if any). The task name can also
+ be updated by the harness when the recipe executes.
+ </a:documentation>
+ </attribute>
+ </optional>
+ </group>
+ </choice>
<optional>
<attribute name="role">
<a:documentation xml:lang="en">
diff --git a/IntegrationTests/src/bkr/inttest/client/test_machine_test.py b/IntegrationTests/src/bkr/inttest/client/test_machine_test.py
index 37613e3..98985a3 100644
--- a/IntegrationTests/src/bkr/inttest/client/test_machine_test.py
+++ b/IntegrationTests/src/bkr/inttest/client/test_machine_test.py
@@ -24,8 +24,8 @@ class MachineTestTest(unittest.TestCase):
self.assertEqual(new_job.whiteboard, u'Test '+ self.system.fqdn)
tasks = new_job.recipesets[0].recipes[0].tasks
self.assertEqual(len(tasks), 2)
- self.assertEqual(tasks[0].task.name, u'/distribution/install')
- self.assertEqual(tasks[1].task.name, u'/distribution/inventory')
+ self.assertEqual(tasks[0].name, u'/distribution/install')
+ self.assertEqual(tasks[1].name, u'/distribution/inventory')
#https://bugzilla.redhat.com/show_bug.cgi?id=893878
try:
diff --git a/IntegrationTests/src/bkr/inttest/complete-job.xml b/IntegrationTests/src/bkr/inttest/complete-job.xml
index 9cd81e7..2468dda 100644
--- a/IntegrationTests/src/bkr/inttest/complete-job.xml
+++ b/IntegrationTests/src/bkr/inttest/complete-job.xml
@@ -33,7 +33,9 @@
<partition fs="btrfs" name="/mnt/testarea" size="18" type="part"/>
<partition fs="btrfs" name="/usr/local" size="8" type="part"/>
</partitions>
- <task name="/distribution/install" role="STANDALONE"/>
+ <task name="/distribution/install" role="STANDALONE">
+ <fetch subdir="install" url="git://example.com/externaltasks#master"/>
+ </task>
</guestrecipe>
<autopick random="true"/>
<watchdog panic="None"/>
diff --git a/IntegrationTests/src/bkr/inttest/data_setup.py b/IntegrationTests/src/bkr/inttest/data_setup.py
index e2d214f..013fb5a 100644
--- a/IntegrationTests/src/bkr/inttest/data_setup.py
+++ b/IntegrationTests/src/bkr/inttest/data_setup.py
@@ -364,12 +364,12 @@ def create_recipe(distro_tree=None, task_list=None,
if task_list: #don't specify a task_list and a task_name...
for t in task_list:
- rt = RecipeTask(task=t)
+ rt = RecipeTask.from_task(t)
rt.role = u'STANDALONE'
recipe.tasks.append(rt)
recipe.ttasks = len(task_list)
else:
- rt = RecipeTask(task=create_task(name=task_name))
+ rt = RecipeTask.from_task(create_task(name=task_name))
rt.role = u'STANDALONE'
recipe.tasks.append(rt)
return recipe
diff --git a/IntegrationTests/src/bkr/inttest/server/selenium/test_jobs_xmlrpc.py b/IntegrationTests/src/bkr/inttest/server/selenium/test_jobs_xmlrpc.py
index 7e3bedd..9e6f555 100644
--- a/IntegrationTests/src/bkr/inttest/server/selenium/test_jobs_xmlrpc.py
+++ b/IntegrationTests/src/bkr/inttest/server/selenium/test_jobs_xmlrpc.py
@@ -26,7 +26,7 @@ import datetime
from turbogears.database import session
from bkr.inttest.server.selenium import XmlRpcTestCase
from bkr.inttest import data_setup
-from bkr.server.model import Job, Distro, ConfigItem, User
+from bkr.server.model import Job, Distro, ConfigItem, User, TaskBase
class JobUploadTest(XmlRpcTestCase):
@@ -140,3 +140,61 @@ class JobUploadTest(XmlRpcTestCase):
</job>
'''.encode('utf8'))
self.assert_(job_tid.startswith('J:'))
+
+ def test_external_tasks(self):
+ job_tid = self.server.jobs.upload('''
+ <job>
+ <whiteboard>job with external task</whiteboard>
+ <recipeSet>
+ <recipe>
+ <distroRequires>
+ <distro_name op="=" value="BlueShoeLinux5-5" />
+ </distroRequires>
+ <hostRequires/>
+ <task name="/distribution/install" />
+ <task name="/distribution/example">
+ <fetch url="git://example.com/externaltasks/example#master"/>
+ </task>
+ <task>
+ <fetch url="git://example.com/externaltasks/example2#master"/>
+ </task>
+ <task name="/distribution/example3">
+ <fetch url="git://example.com/externaltasks#master"
+ subdir="examples/3" />
+ </task>
+ <task>
+ <fetch url="git://example.com/externaltasks#master"
+ subdir="examples/4" />
+ </task>
+ </recipe>
+ </recipeSet>
+ </job>
+ ''')
+ self.assert_(job_tid.startswith('J:'))
+ with session.begin():
+ job = TaskBase.get_by_t_id(job_tid)
+ recipe = job.recipesets[0].recipes[0]
+ self.assertEquals(len(recipe.tasks), 5)
+ self.assertEquals(recipe.tasks[0].name, u'/distribution/install')
+ self.assertEquals(recipe.tasks[0].task.name, u'/distribution/install')
+ self.assertEquals(recipe.tasks[0].fetch_url, None)
+ self.assertEquals(recipe.tasks[1].name, u'/distribution/example')
+ self.assertEquals(recipe.tasks[1].task, None)
+ self.assertEquals(recipe.tasks[1].fetch_url,
+ 'git://example.com/externaltasks/example#master')
+ self.assertEquals(recipe.tasks[2].name,
+ u'git://example.com/externaltasks/example2#master')
+ self.assertEquals(recipe.tasks[2].task, None)
+ self.assertEquals(recipe.tasks[2].fetch_url,
+ u'git://example.com/externaltasks/example2#master')
+ self.assertEquals(recipe.tasks[3].name, u'/distribution/example3')
+ self.assertEquals(recipe.tasks[3].task, None)
+ self.assertEquals(recipe.tasks[3].fetch_url,
+ u'git://example.com/externaltasks#master')
+ self.assertEquals(recipe.tasks[3].fetch_subdir, u'examples/3')
+ self.assertEquals(recipe.tasks[4].name,
+ u'git://example.com/externaltasks#master examples/4')
+ self.assertEquals(recipe.tasks[4].task, None)
+ self.assertEquals(recipe.tasks[4].fetch_url,
+ u'git://example.com/externaltasks#master')
+ self.assertEquals(recipe.tasks[4].fetch_subdir, u'examples/4')
diff --git a/IntegrationTests/src/bkr/inttest/server/test_jobs.py b/IntegrationTests/src/bkr/inttest/server/test_jobs.py
index 18761f1..34fcce5 100644
--- a/IntegrationTests/src/bkr/inttest/server/test_jobs.py
+++ b/IntegrationTests/src/bkr/inttest/server/test_jobs.py
@@ -1,5 +1,5 @@
-import unittest
+import unittest2 as unittest
import xmltramp
import pkg_resources
from turbogears import testutil
@@ -11,6 +11,8 @@ from bkr.server.model import Distro
class TestJobsController(unittest.TestCase):
+ maxDiff = None
+
@with_transaction
def setUp(self):
from bkr.server.jobs import Jobs
@@ -69,4 +71,4 @@ class TestJobsController(unittest.TestCase):
with session.begin():
job = testutil.call(self.controller.process_xmljob, xmljob, self.user)
roundtripped_xml = job.to_xml(clone=True).toprettyxml(indent=' ')
- self.assertEquals(roundtripped_xml, complete_job_xml)
+ self.assertMultiLineEqual(roundtripped_xml, complete_job_xml)
diff --git a/IntegrationTests/src/bkr/inttest/server/test_model.py b/IntegrationTests/src/bkr/inttest/server/test_model.py
index 14164d9..ecbb67c 100644
--- a/IntegrationTests/src/bkr/inttest/server/test_model.py
+++ b/IntegrationTests/src/bkr/inttest/server/test_model.py
@@ -2725,7 +2725,7 @@ class RecipeTaskResultTest(unittest.TestCase):
def test_short_path(self):
task = data_setup.create_task(name=u'/distribution/install')
- rt = RecipeTask(task=task)
+ rt = RecipeTask.from_task(task)
rtr = RecipeTaskResult(recipetask=rt, path=u'/distribution/install/Sysinfo')
self.assertEquals(rtr.short_path, u'Sysinfo')
rtr = RecipeTaskResult(recipetask=rt, path=u'/start')
diff --git a/IntegrationTests/src/bkr/inttest/server/test_reporting_queries.py b/IntegrationTests/src/bkr/inttest/server/test_reporting_queries.py
index bc229f9..20dd3d5 100644
--- a/IntegrationTests/src/bkr/inttest/server/test_reporting_queries.py
+++ b/IntegrationTests/src/bkr/inttest/server/test_reporting_queries.py
@@ -225,8 +225,7 @@ class ReportingQueryTest(unittest.TestCase):
def test_task_durations(self):
short_task = data_setup.create_task()
long_task = data_setup.create_task()
- r = data_setup.create_recipe()
- r.tasks[:] = [RecipeTask(task=short_task), RecipeTask(task=long_task)]
+ r = data_setup.create_recipe(task_list=[short_task, long_task])
data_setup.mark_job_complete(
data_setup.create_job_for_recipes([r]))
r.tasks[0].start_time = datetime.datetime(2012, 10, 15, 10, 54, 0)
diff --git a/Server/bkr/server/job_matrix.py b/Server/bkr/server/job_matrix.py
index 9d37c13..7069d5e 100644
--- a/Server/bkr/server/job_matrix.py
+++ b/Server/bkr/server/job_matrix.py
@@ -194,7 +194,7 @@ class JobMatrix:
# Let's get all the tasks that will be run, and the arch/whiteboard
the_tasks = {}
for recipe,arch in recipes:
- the_tasks.update(dict([(rt.task.name,{}) for rt in recipe.tasks]))
+ the_tasks.update(dict([(rt.name,{}) for rt in recipe.tasks]))
if arch in whiteboard_data:
if recipe.whiteboard not in whiteboard_data[arch]:
whiteboard_data[arch].append(recipe.whiteboard)
@@ -209,7 +209,7 @@ class JobMatrix:
arch_alias = model.Arch.__table__.alias()
recipe_table_alias = model.Recipe.__table__.alias()
- my_select = [model.Task.id.label('task_id'),
+ my_select = [model.RecipeTask.name,
model.RecipeTask.result,
recipe_table_alias.c.whiteboard,
arch_alias.c.arch,
@@ -224,9 +224,8 @@ class JobMatrix:
my_from = [model.RecipeSet.__table__.join(recipe_table_alias).
join(model.DistroTree.__table__, model.DistroTree.id == recipe_table_alias.c.distro_tree_id).
join(arch_alias, arch_alias.c.id == model.DistroTree.arch_id).
- join(model.RecipeTask.__table__, model.RecipeTask.recipe_id == recipe_table_alias.c.id).
- join(model.Task.__table__, model.Task.id == model.RecipeTask.task_id)]
-
+ join(model.RecipeTask.__table__, model.RecipeTask.recipe_id == recipe_table_alias.c.id)]
+
#If this query starts to bog down and slow up, we could create a view for the inner select (s2)
#SQLAlchemy Select object does not really support this,I think you would have to use SQLAlchemy text for s2, and then
#build a specific table for it
@@ -265,10 +264,8 @@ class JobMatrix:
s2.c.whiteboard,
s2.c.arch,
s2.c.arch_id,
- model.Task.name.label('task_name'),
- s2.c.task_id.label('task_id_pk')],
- s2.c.task_id == model.Task.id,
- from_obj=[model.Task.__table__, s2]).group_by(model.Task.name).order_by(model.Task.name).alias()
+ s2.c.name.label('task_name')],
+ from_obj=[s2]).group_by(s2.c.name).order_by(s2.c.name).alias()
results = session.connection(model.Recipe).execute(s1)
for task_details in results:
if task_details.arch in the_tasks[task_details.task_name]:
diff --git a/Server/bkr/server/jobs.py b/Server/bkr/server/jobs.py
index 018261a..f2f5c44 100644
--- a/Server/bkr/server/jobs.py
+++ b/Server/bkr/server/jobs.py
@@ -618,17 +618,21 @@ class Jobs(RPCRoot):
xmltasks = []
invalid_tasks = []
for xmltask in xmlrecipe.iter_tasks():
- try:
- Task.by_name(xmltask.name, valid=1)
- except InvalidRequestError, e:
- invalid_tasks.append(xmltask.name)
- else:
+ if hasattr(xmltask, 'fetch'):
+ # If fetch URL is given, the task doesn't need to exist.
+ xmltasks.append(xmltask)
+ elif Task.exists_by_name(xmltask.name, valid=True):
xmltasks.append(xmltask)
+ else:
+ invalid_tasks.append(xmltask.name)
if invalid_tasks and not ignore_missing_tasks:
raise BX(_('Invalid task(s): %s') % ', '.join(invalid_tasks))
for xmltask in xmltasks:
- task = Task.by_name(xmltask.name)
- recipetask = RecipeTask(task=task)
+ if hasattr(xmltask, 'fetch'):
+ recipetask = RecipeTask.from_fetch_url(xmltask.fetch.url,
+ subdir=xmltask.fetch.subdir, name=xmltask.name)
+ else:
+ recipetask = RecipeTask.from_task(Task.by_name(xmltask.name))
recipetask.role = xmltask.role
for xmlparam in xmltask.iter_params():
param = RecipeTaskParam( name=xmlparam.name,
diff --git a/Server/bkr/server/jobxml.py b/Server/bkr/server/jobxml.py
index c3dc3a1..696d007 100644
--- a/Server/bkr/server/jobxml.py
+++ b/Server/bkr/server/jobxml.py
@@ -222,7 +222,7 @@ class XmlTask(ElementWrapper):
elif attrname == 'id':
return self.get_xml_attr('id', int, 0)
elif attrname == 'name':
- return self.get_xml_attr('name', unicode, u'None')
+ return self.get_xml_attr('name', unicode, None)
elif attrname == 'avg_time':
return self.get_xml_attr('avg_time', int, 0)
elif attrname == 'status':
@@ -231,6 +231,8 @@ class XmlTask(ElementWrapper):
return self.get_xml_attr('result', unicode, u'None')
elif attrname == 'rpm':
return XmlRpm(self.wrappedEl['rpm'])
+ elif attrname == 'fetch':
+ return XmlFetch(self.wrappedEl['fetch'])
else: raise AttributeError, attrname
@@ -285,6 +287,14 @@ class XmlRpm(ElementWrapper):
return self.get_xml_attr('name', unicode, u'None')
else: raise AttributeError, attrname
+class XmlFetch(ElementWrapper):
+ def __getattr__(self, attrname):
+ if attrname == 'url':
+ return self.get_xml_attr('url', unicode, None)
+ elif attrname == 'subdir':
+ return self.get_xml_attr('subdir', unicode, u'')
+ else: raise AttributeError, attrname
+
subclassDict = {
'job' : XmlJob,
'recipeSet' : XmlRecipeSet,
diff --git a/Server/bkr/server/mail.py b/Server/bkr/server/mail.py
index c55e638..36402a3 100644
--- a/Server/bkr/server/mail.py
+++ b/Server/bkr/server/mail.py
@@ -53,7 +53,7 @@ def failed_recipes(job):
for task in recipe.tasks:
if task.is_failed():
msg = "%s\t\t\tTaskID: %s TaskName: %s StartTime: %s Duration: %s Status: %s Result: %s\n" \
- % (msg, task.id, task.task.name, task.start_time, task.duration,
+ % (msg, task.id, task.name, task.start_time, task.duration,
task.status, task.result)
return msg
diff --git a/Server/bkr/server/model/scheduler.py b/Server/bkr/server/model/scheduler.py
index 31c7941..5feeb11 100644
--- a/Server/bkr/server/model/scheduler.py
+++ b/Server/bkr/server/model/scheduler.py
@@ -18,7 +18,8 @@ from sqlalchemy import (Table, Column, ForeignKey, UniqueConstraint, Index,
Integer, Unicode, DateTime, Boolean, UnicodeText, String, Numeric)
from sqlalchemy.sql import select, union, and_, or_, not_, func, literal
from sqlalchemy.exc import InvalidRequestError
-from sqlalchemy.orm import mapper, relationship, backref, object_mapper, dynamic_loader
+from sqlalchemy.orm import (mapper, relationship, backref, object_mapper,
+ dynamic_loader, validates)
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.ext.associationproxy import association_proxy
from turbogears import url
@@ -760,9 +761,11 @@ class Job(TaskBase, DeclarativeMappedObject):
recipe.kernel_options_post = kw.get('koptions_post')
# Eventually we will want the option to add more tasks.
# Add Install task
- recipe.tasks.append(RecipeTask(task = Task.by_name(u'/distribution/install')))
+ recipe.tasks.append(RecipeTask.from_task(
+ Task.by_name(u'/distribution/install')))
# Add Reserve task
- reserveTask = RecipeTask(task = Task.by_name(u'/distribution/reservesys'))
+ reserveTask = RecipeTask.from_task(
+ Task.by_name(u'/distribution/reservesys'))
if kw.get('reservetime'):
#FIXME add DateTimePicker to ReserveSystem Form
reserveTask.params.append(RecipeTaskParam( name = 'RESERVETIME',
@@ -2537,8 +2540,12 @@ class RecipeTask(TaskBase, DeclarativeMappedObject):
__table_args__ = {'mysql_engine': 'InnoDB'}
id = Column(Integer, primary_key=True)
recipe_id = Column(Integer, ForeignKey('recipe.id'), nullable=False)
- task_id = Column(Integer, ForeignKey('task.id'), nullable=False)
- task = relationship(Task, uselist=False)
+ name = Column(Unicode(255), nullable=False, index=True)
+ # each RecipeTask must have either a fetch_url or a task reference
+ fetch_url = Column(Unicode(2048))
+ fetch_subdir = Column(Unicode(2048), nullable=False, default=u'')
+ task_id = Column(Integer, ForeignKey('task.id'))
+ task = relationship(Task)
start_time = Column(DateTime)
finish_time = Column(DateTime)
result = Column(TaskResult.db_type(), nullable=False, default=TaskResult.new)
@@ -2556,9 +2563,37 @@ class RecipeTask(TaskBase, DeclarativeMappedObject):
result_types = ['pass_','warn','fail','panic', 'result_none']
stop_types = ['stop','abort','cancel']
- def __init__(self, task):
- super(RecipeTask, self).__init__()
- self.task = task
+ @classmethod
+ def from_task(cls, task):
+ """
+ Constructs a RecipeTask for the given Task from the task library.
+ """
+ return cls(name=task.name, task=task)
+
+ @classmethod
+ def from_fetch_url(cls, url, subdir=None, name=None):
+ """
+ Constructs an external RecipeTask for the given fetch URL. If name is
+ not given it defaults to the fetch URL combined with the subdir (if any).
+ """
+ if name is None:
+ if subdir:
+ name = u'%s %s' % (url, subdir)
+ else:
+ name = url
+ return cls(name=name, fetch_url=url, fetch_subdir=subdir)
+
+ @validates('task')
+ def validate_task(self, key, value):
+ if value is not None and self.fetch_url is not None:
+ raise ValueError('RecipeTask cannot have both task and fetch_url')
+ return value
+
+ @validates('fetch_url')
+ def validate_fetch_url(self, key, value):
+ if value is not None and self.task is not None:
+ raise ValueError('RecipeTask cannot have both fetch_url and task')
+ return value
def delete(self):
self.logs = []
@@ -2590,20 +2625,27 @@ class RecipeTask(TaskBase, DeclarativeMappedObject):
def to_xml(self, clone=False, *args, **kw):
task = xmldoc.createElement("task")
- task.setAttribute("name", "%s" % self.task.name)
+ task.setAttribute("name", "%s" % self.name)
task.setAttribute("role", "%s" % self.role and self.role or 'STANDALONE')
if not clone:
task.setAttribute("id", "%s" % self.id)
- task.setAttribute("avg_time", "%s" % self.task.avg_time)
task.setAttribute("result", "%s" % self.result)
task.setAttribute("status", "%s" % self.status)
- rpm = xmldoc.createElement("rpm")
- name = self.task.rpm[:self.task.rpm.find('-%s' % self.task.version)]
- rpm.setAttribute("name", name)
- rpm.setAttribute("path", "%s" % self.task.path)
- task.appendChild(rpm)
- if self.duration and not clone:
- task.setAttribute("duration", "%s" % self.duration)
+ if self.task:
+ task.setAttribute("avg_time", "%s" % self.task.avg_time)
+ rpm = xmldoc.createElement("rpm")
+ name = self.task.rpm[:self.task.rpm.find('-%s' % self.task.version)]
+ rpm.setAttribute("name", name)
+ rpm.setAttribute("path", "%s" % self.task.path)
+ task.appendChild(rpm)
+ if self.duration:
+ task.setAttribute("duration", "%s" % self.duration)
+ if self.fetch_url:
+ fetch = xmldoc.createElement('fetch')
+ fetch.setAttribute('url', self.fetch_url)
+ if self.fetch_subdir:
+ fetch.setAttribute('subdir', self.fetch_subdir)
+ task.appendChild(fetch)
if not self.is_queued() and not clone:
roles = xmldoc.createElement("roles")
for role in self.roles_to_xml():
@@ -2630,10 +2672,6 @@ class RecipeTask(TaskBase, DeclarativeMappedObject):
return duration
duration = property(_get_duration)
- def path(self):
- return self.task.name
- path = property(path)
-
def link_id(self):
""" Return a link to this Executed Recipe->Task
"""
@@ -2642,13 +2680,19 @@ class RecipeTask(TaskBase, DeclarativeMappedObject):
link_id = property(link_id)
- def link(self):
- """ Return a link to this Task
+ @property
+ def name_markup(self):
"""
- return make_link(url = '/tasks/%s' % self.task.id,
- text = self.task.name)
-
- link = property(link)
+ Returns HTML markup (in the form of a kid.Element) displaying the name.
+ The name is linked to the task library when applicable.
+ """
+ if self.task:
+ return make_link(url = '/tasks/%s' % self.task.id,
+ text = self.name)
+ else:
+ span = Element('span')
+ span.text = self.name
+ return span
@property
def all_logs(self):
@@ -2690,9 +2734,10 @@ class RecipeTask(TaskBase, DeclarativeMappedObject):
if watchdog_override:
self.recipe.watchdog.kill_time = watchdog_override
else:
+ task_time = self.task.avg_time if self.task else 0
# add in 30 minutes at a minimum
- self.recipe.watchdog.kill_time = datetime.utcnow() + timedelta(
- seconds=self.task.avg_time + 1800)
+ self.recipe.watchdog.kill_time = (datetime.utcnow() +
+ timedelta(task_time + 1800))
self.recipe.recipeset.job._mark_dirty()
return True
@@ -2810,7 +2855,7 @@ class RecipeTask(TaskBase, DeclarativeMappedObject):
worker = worker,
state_label = "%s" % self.status,
state = self.status.value,
- method = "%s" % self.task.name,
+ method = "%s" % self.name,
result = "%s" % self.result,
is_finished = self.is_finished(),
is_failed = self.is_failed(),
@@ -3079,10 +3124,10 @@ class RecipeTaskResult(TaskBase, DeclarativeMappedObject):
"""
if not self.path or self.path == '/':
short_path = self.log or './'
- elif self.path.rstrip('/') == self.recipetask.task.name:
+ elif self.path.rstrip('/') == self.recipetask.name:
short_path = './'
- elif self.path.startswith(self.recipetask.task.name + '/'):
- short_path = self.path.replace(self.recipetask.task.name + '/', '', 1)
+ elif self.path.startswith(self.recipetask.name + '/'):
+ short_path = self.path.replace(self.recipetask.name + '/', '', 1)
else:
short_path = self.path
return short_path
diff --git a/Server/bkr/server/model/tasklibrary.py b/Server/bkr/server/model/tasklibrary.py
index 8160d87..12c9072 100644
--- a/Server/bkr/server/model/tasklibrary.py
+++ b/Server/bkr/server/model/tasklibrary.py
@@ -317,6 +317,13 @@ class Task(DeclarativeMappedObject):
library = TaskLibrary()
@classmethod
+ def exists_by_name(cls, name, valid=None):
+ query = cls.query.filter(Task.name == name)
+ if valid is not None:
+ query = query.filter(Task.valid == bool(valid))
+ return query.count() > 0
+
+ @classmethod
def by_name(cls, name, valid=None):
query = cls.query.filter(Task.name==name)
if valid is not None:
diff --git a/Server/bkr/server/tasks.py b/Server/bkr/server/tasks.py
index 22a07f0..a587846 100644
--- a/Server/bkr/server/tasks.py
+++ b/Server/bkr/server/tasks.py
@@ -296,7 +296,7 @@ class Tasks(RPCRoot):
if kw.get('task'):
# Shouldn't have to do this. This only happens on the LinkRemoteFunction calls
kw['task'] = kw.get('task').replace('%2F','/')
- tasks = tasks.join('task').filter(Task.name.like('%s' % kw.get('task').replace('*','%%')))
+ tasks = tasks.filter(RecipeTask.name.like('%s' % kw.get('task').replace('*','%%')))
if kw.get('distro'):
tasks = tasks.join(RecipeTask.recipe, Recipe.distro_tree, DistroTree.distro)\
.filter(Distro.name.like('%%%s%%' % kw.get('distro')))
diff --git a/Server/bkr/server/templates/tasks_widget.kid b/Server/bkr/server/templates/tasks_widget.kid
index a49c5bc..339b30d 100644
--- a/Server/bkr/server/templates/tasks_widget.kid
+++ b/Server/bkr/server/templates/tasks_widget.kid
@@ -46,7 +46,7 @@
${task.link_id}
</td>
<td class="task" py:if="not hidden.has_key('task')">
- ${task.link}
+ ${task.name_markup}
</td>
<td class="task" py:if="hidden.has_key('task')">&nbsp;</td>
<td class="task" py:if="not hidden.has_key('distro_tree')">
diff --git a/Server/bkr/server/watchdog.py b/Server/bkr/server/watchdog.py
index b7157ec..666c5e6 100644
--- a/Server/bkr/server/watchdog.py
+++ b/Server/bkr/server/watchdog.py
@@ -28,7 +28,7 @@ class Watchdogs(RPCRoot):
fields = [col(name='job_id', getter=lambda x: x.recipe.recipeset.job.link, title="Job ID"),
col(name='system_name', getter=lambda x: x.recipe.resource.link, title="System"),
col(name='lab_controller', getter=lambda x: x.recipe.recipeset.lab_controller, title="Lab Controller"),
- col(name='task_name', getter=lambda x: x.recipetask.link
+ col(name='task_name', getter=lambda x: x.recipetask.name_markup
if x.recipetask is not None else None, title="Task Name"),
col(name='kill_time', getter=lambda x: x.kill_time,
title="Kill Time", options=dict(datetime=True))]
diff --git a/documentation/whats-new/next/external-tasks.rst b/documentation/whats-new/next/external-tasks.rst
new file mode 100644
index 0000000..7002377
--- /dev/null
+++ b/documentation/whats-new/next/external-tasks.rst
@@ -0,0 +1,36 @@
+External tasks
+==============
+
+Database changes
+----------------
+
+Run the following SQL to upgrade.
+
+.. note:: In established Beaker instances the ``recipe_task`` table may be very
+ large, and therefore these upgrade steps may take a long time. Allow
+ approximately 1 minute per 600 000 rows.
+
+::
+
+ ALTER TABLE recipe_task
+ ADD name VARCHAR(255) NOT NULL AFTER recipe_id,
+ ADD fetch_url VARCHAR(2048) AFTER version,
+ ADD fetch_subdir VARCHAR(2048) NOT NULL DEFAULT '' AFTER fetch_url,
+ MODIFY task_id INT,
+ ADD INDEX (name);
+
+ UPDATE recipe_task
+ SET name = (SELECT name FROM task WHERE id = recipe_task.task_id);
+
+To roll back, run the following SQL. Note that rollback is not possible if the
+external tasks feature has been used in your Beaker installation, since it is
+not possible to satisfy the ``recipe_task.task_id`` foreign key constraint in
+that case.
+
+::
+
+ ALTER TABLE recipe_task
+ DROP name,
+ DROP fetch_url,
+ DROP fetch_subdir,
+ MODIFY task_id INT NOT NULL;