Merge "Add gerrit/smtp port config options to the doc"
diff --git a/doc/source/launchers.rst b/doc/source/launchers.rst
index f3e45db..accd80b 100644
--- a/doc/source/launchers.rst
+++ b/doc/source/launchers.rst
@@ -11,6 +11,8 @@
.. _`Turbo-Hipster Documentation`:
http://turbo-hipster.rtfd.org/
+.. _FormPost: http://docs.openstack.org/developer/swift/misc.html#module-swift.common.middleware.formpost
+
.. _launchers:
Launchers
@@ -117,6 +119,34 @@
Your jobs can check whether the parameters are ``000000`` to act
differently on each kind of event.
+Swift parameters
+~~~~~~~~~~~~~~~~
+
+If swift information has been configured for the job zuul will also
+provide signed credentials for the builder to upload results and
+assets into containers using the `FormPost`_ middleware.
+
+Each zuul container/instruction set will contain each of the following
+parameters where $NAME is the ``name`` defined in the layout.
+
+*SWIFT_$NAME_URL*
+ The swift destination URL. This will be the entire URL including
+ the AUTH, container and path prefix (folder).
+*SWIFT_$NAME_HMAC_BODY*
+ The information signed in the HMAC body. The body is as follows::
+
+ PATH TO OBJECT PREFIX (excluding domain)
+ BLANK LINE (zuul implements no form redirect)
+ MAX FILE SIZE
+ MAX FILE COUNT
+ SIGNATURE EXPIRY
+
+*SWIFT_$NAME_SIGNATURE*
+ The HMAC body signed with the configured key.
+*SWIFT_$NAME_LOGSERVER_PREFIX*
+ The URL to prepend to the object path when returning the results
+ from a build.
+
Gearman
-------
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 56a8992..408b0ac 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -176,6 +176,59 @@
This can be overridden by individual pipelines.
``default_to=you@example.com``
+.. _swift:
+
+swift
+"""""
+
+To send (optional) swift upload instructions this section must be
+present. Multiple destinations can be defined in the :ref:`jobs`
+section of the layout.
+
+**authurl**
+ The (keystone) Auth URL for swift
+ ``For example, https://identity.api.rackspacecloud.com/v2.0/``
+
+Any of the `swiftclient connection parameters`_ can also be defined
+here by the same name. Including the os_options by their key name (
+``for example tenant_id``)
+
+.. _swiftclient connection parameters: http://docs.openstack.org/developer/python-swiftclient/swiftclient.html#module-swiftclient.client
+
+**X-Account-Meta-Temp-Url-Key** (optional)
+ This is the key used to sign the HMAC message. zuul will send the
+ key to swift for you so you only need to define it here. If you do
+ not set a key zuul will generate one automatically.
+
+**region_name** (optional)
+ The region name holding the swift container
+ ``For example, SYD``
+
+Each destination defined by the :ref:`jobs` will have the following
+default values that it may overwrite.
+
+**default_container** (optional)
+ Container name to place the log into
+ ``For example, logs``
+
+**default_expiry** (optional)
+ How long the signed destination should be available for
+ ``default: 7200 (2hrs)``
+
+**default_max_file_size** (optional)
+ The maximum size of an individual file
+ ``default: 104857600 (100MB)``
+
+**default_max_file_count** (optional)
+ The maximum number of separate files to allow
+ ``default: 10``
+
+**default_logserver_prefix**
+ Provide a URL to the CDN or logserver app so that a worker knows
+ what URL to return. The worker should return the logserver_prefix
+ url and the object path.
+ ``For example: http://logs.example.org/server.app?obj=``
+
layout.yaml
~~~~~~~~~~~
@@ -246,6 +299,12 @@
reported back to Gerrit when at least one voting build fails.
Defaults to "Build failed."
+**merge-failure-message**
+ An optional field that supplies the introductory text in message
+ reported back to Gerrit when a change fails to merge with the
+ current state of the repository.
+ Defaults to "Merge failed."
+
**footer-message**
An optional field to supply additional information after test results.
Useful for adding information about the CI system such as debugging
@@ -421,6 +480,12 @@
Uses the same syntax as **success**, but describes what Zuul should
do if at least one job fails.
+**merge-failure**
+ Uses the same syntax as **success**, but describes what Zuul should
+ do if it is unable to merge in the patchset. If no merge-failure
+ reporters are listed then the ``failure`` reporters will be used to
+ notify of unsuccessful merges.
+
**start**
Uses the same syntax as **success**, but describes what Zuul should
do when a change is added to the pipeline manager. This can be used,
@@ -563,6 +628,9 @@
the ``ref-updated`` event which does include the commit sha1 (but lacks the
Gerrit change number).
+
+.. _jobs:
+
Jobs
""""
@@ -645,6 +713,39 @@
be used to specify on what node (or class of node) the job should be
run.
+**swift**
+ If :ref:`swift` is configured then each job can define a destination
+ container for the builder to place logs and/or assets into. Multiple
+ containers can be listed for each job by providing a unique ``name``.
+
+ *name*
+ Set an identifying name for the container. This is used in the
+ parameter key sent to the builder. For example if it ``logs`` then
+ one of the parameters sent will be ``SWIFT_logs_CONTAINER``
+ (case-sensitive).
+
+ Each of the defaults defined in :ref:`swift` can be overwritten as:
+
+ *container* (optional)
+ Container name to place the log into
+ ``For example, logs``
+
+ *expiry* (optional)
+ How long the signed destination should be available for
+
+ *max_file_size** (optional)
+ The maximum size of an individual file
+
+ *max_file_count* (optional)
+ The maximum number of separate files to allow
+
+ *logserver_prefix*
+ Provide a URL to the CDN or logserver app so that a worker knows
+ what URL to return.
+ ``For example: http://logs.example.org/server.app?obj=``
+ The worker should return the logserver_prefix url and the object
+ path as the URL in the results data packet.
+
Here is an example of setting the failure message for jobs that check
whether a change merges cleanly::
@@ -768,8 +869,8 @@
Note that if multiple templates are used for a project and one
template specifies a job that is also specified in another template,
-or specified in the project itself, those jobs will be duplicated in
-the resulting project configuration.
+or specified in the project itself, the configuration defined by
+either the last template or the project itself will take priority.
logging.conf
~~~~~~~~~~~~
diff --git a/etc/status/public_html/app.js b/etc/status/public_html/app.js
index 2f9d3b7..b4c82f8 100644
--- a/etc/status/public_html/app.js
+++ b/etc/status/public_html/app.js
@@ -17,8 +17,9 @@
// under the License.
(function ($) {
- var $container, $msg, $msgWrap, $indicator, $queueInfo, $queueEventsNum, $queueResultsNum, $pipelines,
- prevHtml, xhr, zuul, $jq,
+ var $container, $msg, $msgWrap, $indicator, $queueInfo, $queueEventsNum,
+ $queueResultsNum, $pipelines, $jq;
+ var xhr, prevHtml, zuul,
demo = location.search.match(/[?&]demo=([^?&]*)/),
source = demo ?
'./status-' + (demo[1] || 'basic') + '.json-sample' :
@@ -67,8 +68,10 @@
$('#zuul-version-span').text(data['zuul_version']);
}
if ('last_reconfigured' in data) {
- var last_reconfigured = new Date(data['last_reconfigured']);
- $('#last-reconfigured-span').text(last_reconfigured.toString());
+ var last_reconfigured =
+ new Date(data['last_reconfigured']);
+ $('#last-reconfigured-span').text(
+ last_reconfigured.toString());
}
$.each(data.pipelines, function (i, pipeline) {
@@ -82,10 +85,12 @@
}
$queueEventsNum.text(
- data.trigger_event_queue ? data.trigger_event_queue.length : '0'
+ data.trigger_event_queue ?
+ data.trigger_event_queue.length : '0'
);
$queueResultsNum.text(
- data.result_event_queue ? data.result_event_queue.length : '0'
+ data.result_event_queue ?
+ data.result_event_queue.length : '0'
);
})
.fail(function (err, jqXHR, errMsg) {
@@ -102,7 +107,8 @@
format: {
change: function (change) {
- var html = '<div class="well well-small zuul-change"><ul class="nav nav-list">',
+ var html = '<div class="well well-small zuul-change">' +
+ '<ul class="nav nav-list">',
id = change.id,
url = change.url;
@@ -140,10 +146,12 @@
}
html += '<li class="zuul-change-job">';
html += job.url !== null ?
- '<a href="' + job.url + '" class="zuul-change-job-link">' :
+ '<a href="' + job.url + '" ' +
+ 'class="zuul-change-job-link">' :
'<span class="zuul-change-job-link">';
html += job.name;
- html += ' <span class="' + resultClass + '">' + result + '</span>';
+ html += ' <span class="' + resultClass + '">' + result +
+ '</span>';
if (job.voting === false) {
html += ' <span class="muted">(non-voting)</span>';
}
@@ -159,12 +167,15 @@
var html = '<div class="zuul-pipeline span4"><h3>' +
pipeline.name + '</h3>';
if (typeof pipeline.description === 'string') {
- html += '<p><small>' + pipeline.description + '</small></p>';
+ html += '<p><small>' + pipeline.description +
+ '</small></p>';
}
- $.each(pipeline.change_queues, function (queueNum, changeQueue) {
+ $.each(pipeline.change_queues,
+ function (queueNum, changeQueue) {
$.each(changeQueue.heads, function (headNum, changes) {
- if (pipeline.change_queues.length > 1 && headNum === 0) {
+ if (pipeline.change_queues.length > 1 &&
+ headNum === 0) {
var name = changeQueue.name;
html += '<p>Queue: <abbr title="' + name + '">';
if (name.length > 32) {
@@ -173,9 +184,11 @@
html += name + '</abbr></p>';
}
$.each(changes, function (changeNum, change) {
- // If there are multiple changes in the same head it means they're connected
+ // If there are multiple changes in the same head
+ // it means they're connected
if (changeNum > 0) {
- html += '<div class="zuul-change-arrow">↑</div>';
+ html += '<div class="zuul-change-arrow">' +
+ '↑</div>';
}
html += zuul.format.change(change);
});
@@ -216,25 +229,34 @@
});
$jq.one('update-end', function () {
- // Do this asynchronous so that if the first update adds a message, it will not animate
- // while we fade in the content. Instead it simply appears with the rest of the content.
+ // Do this asynchronous so that if the first update adds a message, it
+ // will not animate while we fade in the content. Instead it simply
+ // appears with the rest of the content.
setTimeout(function () {
- $container.addClass('zuul-container-ready'); // Fades in the content
+ // Fade in the content
+ $container.addClass('zuul-container-ready');
});
});
$(function ($) {
$msg = $('<div class="zuul-msg alert alert-error"></div>');
- $msgWrap = $msg.wrap('<div class="zuul-msg-wrap zuul-msg-wrap-off"></div>').parent();
- $indicator = $('<span class="btn pull-right zuul-spinner">updating <i class="icon-refresh"></i></span>');
- $queueInfo = $('<p>Queue lengths: <span>0</span> events, <span>0</span> results.</p>');
+ $msgWrap = $msg.wrap('<div class="zuul-msg-wrap zuul-msg-wrap-off">' +
+ '</div>').parent();
+ $indicator = $('<span class="btn pull-right zuul-spinner">updating ' +
+ '<i class="icon-refresh"></i></span>');
+ $queueInfo = $('<p>Queue lengths: <span>0</span> events, ' +
+ '<span>0</span> results.</p>');
$queueEventsNum = $queueInfo.find('span').eq(0);
$queueResultsNum = $queueEventsNum.next();
$pipelines = $('<div class="row"></div>');
- $zuulVersion = $('<p>Zuul version: <span id="zuul-version-span"></span></p>');
- $lastReconf = $('<p>Last reconfigured: <span id="last-reconfigured-span"></span></p>');
+ $zuulVersion = $('<p>Zuul version: <span id="zuul-version-span">' +
+ '</span></p>');
+ $lastReconf = $('<p>Last reconfigured: ' +
+ '<span id="last-reconfigured-span"></span></p>');
- $container = $('#zuul-container').append($msgWrap, $indicator, $queueInfo, $pipelines, $zuulVersion, $lastReconf);
+ $container = $('#zuul-container').append($msgWrap, $indicator,
+ $queueInfo, $pipelines,
+ $zuulVersion, $lastReconf);
zuul.schedule();
diff --git a/etc/zuul.conf-sample b/etc/zuul.conf-sample
index 75c84e4..ac8021b 100644
--- a/etc/zuul.conf-sample
+++ b/etc/zuul.conf-sample
@@ -23,6 +23,15 @@
;git_user_name=zuul
zuul_url=http://zuul.example.com/p
+[swift]
+authurl=https://identity.api.example.org/v2.0/
+user=username
+key=password
+
+default_container=logs
+region_name=EXP
+logserver_prefix=http://logs.example.org/server.app/
+
[smtp]
server=localhost
port=25
diff --git a/requirements.txt b/requirements.txt
index f14441b..bb48290 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,3 +14,5 @@
voluptuous>=0.7
gear>=0.5.4,<1.0.0
apscheduler>=2.1.1,<3.0
+python-swiftclient>=1.6
+python-keystoneclient>=0.4.2
diff --git a/tests/fixtures/layout-merge-failure.yaml b/tests/fixtures/layout-merge-failure.yaml
new file mode 100644
index 0000000..72bc9c9
--- /dev/null
+++ b/tests/fixtures/layout-merge-failure.yaml
@@ -0,0 +1,56 @@
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+ - name: post
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: ref-updated
+ ref: ^(?!refs/).*$
+
+ - name: gate
+ manager: DependentPipelineManager
+ failure-message: Build failed. For information on how to proceed, see http://wiki.example.org/Test_Failures
+ merge-failure-message: "The merge failed! For more information..."
+ trigger:
+ gerrit:
+ - event: comment-added
+ approval:
+ - approved: 1
+ success:
+ gerrit:
+ verified: 2
+ submit: true
+ failure:
+ gerrit:
+ verified: -2
+ merge-failure:
+ gerrit:
+ verified: -1
+ smtp:
+ to: you@example.com
+ start:
+ gerrit:
+ verified: 0
+ precedence: high
+
+projects:
+ - name: org/project
+ check:
+ - project-merge:
+ - project-test1
+ - project-test2
+ gate:
+ - project-merge:
+ - project-test1
+ - project-test2
diff --git a/tests/fixtures/layout-swift.yaml b/tests/fixtures/layout-swift.yaml
new file mode 100644
index 0000000..acaaad8
--- /dev/null
+++ b/tests/fixtures/layout-swift.yaml
@@ -0,0 +1,59 @@
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+ - name: post
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: ref-updated
+ ref: ^(?!refs/).*$
+
+ - name: gate
+ manager: DependentPipelineManager
+ failure-message: Build failed. For information on how to proceed, see http://wiki.example.org/Test_Failures
+ trigger:
+ gerrit:
+ - event: comment-added
+ approval:
+ - approved: 1
+ success:
+ gerrit:
+ verified: 2
+ submit: true
+ failure:
+ gerrit:
+ verified: -2
+ start:
+ gerrit:
+ verified: 0
+ precedence: high
+
+jobs:
+ - name: ^.*$
+ swift:
+ - name: logs
+ - name: ^.*-merge$
+ swift:
+ - name: logs
+ container: merge_logs
+ failure-message: Unable to merge change
+ - name: test-test
+ swift:
+ - name: MOSTLY
+ container: stash
+
+projects:
+ - name: org/project
+ gate:
+ - test-merge
+ - test-test
diff --git a/tests/fixtures/layout.yaml b/tests/fixtures/layout.yaml
index b1c94de..b02c782 100644
--- a/tests/fixtures/layout.yaml
+++ b/tests/fixtures/layout.yaml
@@ -117,9 +117,6 @@
- name: test-five
check:
- '{name}-{something}-test5'
- - name: test-five-also
- check:
- - '{name}-{something}-test5'
projects:
- name: org/project
@@ -215,8 +212,6 @@
- name: test-three-and-four
- name: test-five
something: foo
- - name: test-five-also
- something: foo
check:
- project-test6
diff --git a/tests/fixtures/layouts/bad_merge_failure.yaml b/tests/fixtures/layouts/bad_merge_failure.yaml
new file mode 100644
index 0000000..313d23b
--- /dev/null
+++ b/tests/fixtures/layouts/bad_merge_failure.yaml
@@ -0,0 +1,39 @@
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+ merge-failure-message:
+
+ - name: gate
+ manager: DependentPipelineManager
+ failure-message: Build failed. For information on how to proceed, see http://wiki.example.org/Test_Failures
+ trigger:
+ gerrit:
+ - event: comment-added
+ approval:
+ - approved: 1
+ success:
+ gerrit:
+ verified: 2
+ submit: true
+ failure:
+ gerrit:
+ verified: -2
+ merge-failure:
+ start:
+ gerrit:
+ verified: 0
+ precedence: high
+
+projects:
+ - name: org/project
+ check:
+ - project-check
diff --git a/tests/fixtures/layouts/bad_swift.yaml b/tests/fixtures/layouts/bad_swift.yaml
new file mode 100644
index 0000000..d8a8c3f
--- /dev/null
+++ b/tests/fixtures/layouts/bad_swift.yaml
@@ -0,0 +1,29 @@
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+jobs:
+ - name: ^.*$
+ swift:
+ - name: logs
+ - name: ^.*-merge$
+ swift:
+ container: merge_assets
+ failure-message: Unable to merge change
+ - name: test-test
+ swift:
+
+projects:
+ - name: test-org/test
+ check:
+ - test-merge
+ - test-test
diff --git a/tests/fixtures/layouts/good_merge_failure.yaml b/tests/fixtures/layouts/good_merge_failure.yaml
new file mode 100644
index 0000000..f69b764
--- /dev/null
+++ b/tests/fixtures/layouts/good_merge_failure.yaml
@@ -0,0 +1,53 @@
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ merge-failure-message: "Could not merge the change. Please rebase..."
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+ - name: post
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: ref-updated
+ ref: ^(?!refs/).*$
+ merge-failure:
+ gerrit:
+ verified: -1
+
+ - name: gate
+ manager: DependentPipelineManager
+ failure-message: Build failed. For information on how to proceed, see http://wiki.example.org/Test_Failures
+ trigger:
+ gerrit:
+ - event: comment-added
+ approval:
+ - approved: 1
+ success:
+ gerrit:
+ verified: 2
+ submit: true
+ failure:
+ gerrit:
+ verified: -2
+ merge-failure:
+ gerrit:
+ verified: -1
+ smtp:
+ to: you@example.com
+ start:
+ gerrit:
+ verified: 0
+ precedence: high
+
+projects:
+ - name: org/project
+ check:
+ - project-check
diff --git a/tests/fixtures/layouts/good_swift.yaml b/tests/fixtures/layouts/good_swift.yaml
new file mode 100644
index 0000000..913c268
--- /dev/null
+++ b/tests/fixtures/layouts/good_swift.yaml
@@ -0,0 +1,32 @@
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+jobs:
+ - name: ^.*$
+ swift:
+ - name: logs
+ - name: ^.*-merge$
+ swift:
+ - name: assets
+ container: merge_assets
+ failure-message: Unable to merge change
+ - name: test-test
+ swift:
+ - name: mostly
+ container: stash
+
+projects:
+ - name: test-org/test
+ check:
+ - test-merge
+ - test-test
diff --git a/tests/fixtures/zuul.conf b/tests/fixtures/zuul.conf
index bee06e4..ec76cd0 100644
--- a/tests/fixtures/zuul.conf
+++ b/tests/fixtures/zuul.conf
@@ -21,4 +21,14 @@
server=localhost
port=25
default_from=zuul@example.com
-default_to=you@example.com
\ No newline at end of file
+default_to=you@example.com
+
+[swift]
+authurl=https://identity.api.example.org/v2.0/
+user=username
+key=password
+tenant_name=" "
+
+default_container=logs
+region_name=EXP
+logserver_prefix=http://logs.example.org/server.app/
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 2d8d6bb..7e1416f 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -30,6 +30,7 @@
import socket
import string
import subprocess
+import swiftclient
import threading
import time
import urllib
@@ -47,6 +48,7 @@
import zuul.rpclistener
import zuul.rpcclient
import zuul.launcher.gearman
+import zuul.lib.swift
import zuul.merger.server
import zuul.merger.client
import zuul.reporter.gerrit
@@ -743,6 +745,18 @@
return True
+class FakeSwiftClientConnection(swiftclient.client.Connection):
+ def post_account(self, headers):
+ # Do nothing
+ pass
+
+ def get_auth(self):
+ # Returns endpoint and (unused) auth token
+ endpoint = os.path.join('https://storage.example.org', 'V1',
+ 'AUTH_account')
+ return endpoint, ''
+
+
class TestScheduler(testtools.TestCase):
log = logging.getLogger("zuul.test")
@@ -823,12 +837,18 @@
self.sched = zuul.scheduler.Scheduler()
+ self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
+ FakeSwiftClientConnection))
+ self.swift = zuul.lib.swift.Swift(self.config)
+
def URLOpenerFactory(*args, **kw):
args = [self.fake_gerrit] + list(args)
return FakeURLOpener(self.upstream_root, *args, **kw)
urllib2.urlopen = URLOpenerFactory
- self.launcher = zuul.launcher.gearman.Gearman(self.config, self.sched)
+
+ self.launcher = zuul.launcher.gearman.Gearman(self.config, self.sched,
+ self.swift)
self.merge_client = zuul.merger.client.MergeClient(
self.config, self.sched)
@@ -2061,13 +2081,8 @@
).result, 'SUCCESS')
self.assertEqual(self.getJobFromHistory('layered-project-test4'
).result, 'SUCCESS')
- # test5 should run twice because two templates define it
- test5_count = 0
- for job in self.worker.build_history:
- if job.name == 'layered-project-foo-test5':
- test5_count += 1
- self.assertEqual(job.result, 'SUCCESS')
- self.assertEqual(test5_count, 2)
+ self.assertEqual(self.getJobFromHistory('layered-project-foo-test5'
+ ).result, 'SUCCESS')
self.assertEqual(self.getJobFromHistory('project-test6').result,
'SUCCESS')
@@ -3731,3 +3746,127 @@
self.assertEqual(failure_body, self.smtp_messages[0]['body'])
self.assertEqual(success_body, self.smtp_messages[1]['body'])
+
+ def test_merge_failure_reporters(self):
+ """Check that the config is set up correctly"""
+
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-merge-failure.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ self.assertEqual(
+ "Merge Failed.\n\nThis change was unable to be automatically "
+ "merged with the current state of the repository. Please rebase "
+ "your change and upload a new patchset.",
+ self.sched.layout.pipelines['check'].merge_failure_message)
+ self.assertEqual(
+ "The merge failed! For more information...",
+ self.sched.layout.pipelines['gate'].merge_failure_message)
+
+ self.assertEqual(
+ len(self.sched.layout.pipelines['check'].merge_failure_actions), 1)
+ self.assertEqual(
+ len(self.sched.layout.pipelines['gate'].merge_failure_actions), 2)
+
+ self.assertTrue(isinstance(
+ self.sched.layout.pipelines['check'].merge_failure_actions[0].
+ reporter, zuul.reporter.gerrit.Reporter))
+
+ self.assertTrue(
+ (
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[0].reporter,
+ zuul.reporter.smtp.Reporter) and
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[1].reporter,
+ zuul.reporter.gerrit.Reporter)
+ ) or (
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[0].reporter,
+ zuul.reporter.gerrit.Reporter) and
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[1].reporter,
+ zuul.reporter.smtp.Reporter)
+ )
+ )
+
+ def test_merge_failure_reports(self):
+ """Check that when a change fails to merge the correct message is sent
+ to the correct reporter"""
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-merge-failure.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ # Check a test failure isn't reported to SMTP
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('CRVW', 2)
+ self.worker.addFailTest('project-test1', A)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(3, len(self.history)) # 3 jobs
+ self.assertEqual(0, len(self.smtp_messages))
+
+ # Check a merge failure is reported to SMTP
+ # B should be merged, but C will conflict with B
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ B.addPatchset(['conflict'])
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ C.addPatchset(['conflict'])
+ B.addApproval('CRVW', 2)
+ C.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(6, len(self.history)) # A and B jobs
+ self.assertEqual(1, len(self.smtp_messages))
+ self.assertEqual('The merge failed! For more information...',
+ self.smtp_messages[0]['body'])
+
+ def test_swift_instructions(self):
+ "Test that the correct swift instructions are sent to the workers"
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-swift.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ self.worker.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+
+ A.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(
+ "https://storage.example.org/V1/AUTH_account/merge_logs/1/1/1/"
+ "gate/test-merge/",
+ self.builds[0].parameters['SWIFT_logs_URL'][:-32])
+ self.assertEqual(5,
+ len(self.builds[0].parameters['SWIFT_logs_HMAC_BODY'].
+ split('\n')))
+ self.assertIn('SWIFT_logs_SIGNATURE', self.builds[0].parameters)
+
+ self.assertEqual(
+ "https://storage.example.org/V1/AUTH_account/logs/1/1/1/"
+ "gate/test-test/",
+ self.builds[1].parameters['SWIFT_logs_URL'][:-32])
+ self.assertEqual(5,
+ len(self.builds[1].parameters['SWIFT_logs_HMAC_BODY'].
+ split('\n')))
+ self.assertIn('SWIFT_logs_SIGNATURE', self.builds[1].parameters)
+
+ self.assertEqual(
+ "https://storage.example.org/V1/AUTH_account/stash/1/1/1/"
+ "gate/test-test/",
+ self.builds[1].parameters['SWIFT_MOSTLY_URL'][:-32])
+ self.assertEqual(5,
+ len(self.builds[1].
+ parameters['SWIFT_MOSTLY_HMAC_BODY'].split('\n')))
+ self.assertIn('SWIFT_MOSTLY_SIGNATURE', self.builds[1].parameters)
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
diff --git a/zuul/cmd/server.py b/zuul/cmd/server.py
index 13e6283..8caa1fd 100755
--- a/zuul/cmd/server.py
+++ b/zuul/cmd/server.py
@@ -180,6 +180,7 @@
import zuul.scheduler
import zuul.launcher.gearman
import zuul.merger.client
+ import zuul.lib.swift
import zuul.reporter.gerrit
import zuul.reporter.smtp
import zuul.trigger.gerrit
@@ -195,8 +196,10 @@
self.log = logging.getLogger("zuul.Server")
self.sched = zuul.scheduler.Scheduler()
+ self.swift = zuul.lib.swift.Swift(self.config)
- gearman = zuul.launcher.gearman.Gearman(self.config, self.sched)
+ gearman = zuul.launcher.gearman.Gearman(self.config, self.sched,
+ self.swift)
merger = zuul.merger.client.MergeClient(self.config, self.sched)
gerrit = zuul.trigger.gerrit.Gerrit(self.config, self.sched)
timer = zuul.trigger.timer.Timer(self.config, self.sched)
diff --git a/zuul/launcher/gearman.py b/zuul/launcher/gearman.py
index 9ab1f61..b0d8546 100644
--- a/zuul/launcher/gearman.py
+++ b/zuul/launcher/gearman.py
@@ -16,6 +16,7 @@
import inspect
import json
import logging
+import os
import time
import threading
from uuid import uuid4
@@ -151,8 +152,10 @@
log = logging.getLogger("zuul.Gearman")
negative_function_cache_ttl = 5
- def __init__(self, config, sched):
+ def __init__(self, config, sched, swift):
+ self.config = config
self.sched = sched
+ self.swift = swift
self.builds = {}
self.meta_jobs = {} # A list of meta-jobs like stop or describe
@@ -215,6 +218,50 @@
self.log.debug("Function %s is not registered" % name)
return False
+ def updateBuildParams(self, job, item, params):
+ """Allow the job to modify and add build parameters"""
+
+ # NOTE(jhesketh): The params need to stay in a key=value data pair
+ # as workers cannot necessarily handle lists.
+
+ if job.swift and self.swift.connection:
+
+ for name, s in job.swift.items():
+ swift_instructions = {}
+ s_config = {}
+ s_config.update((k, v.format(item=item, job=job,
+ change=item.change))
+ for k, v in s.items())
+
+ (swift_instructions['URL'],
+ swift_instructions['HMAC_BODY'],
+ swift_instructions['SIGNATURE']) = \
+ self.swift.generate_form_post_middleware_params(
+ params['LOG_PATH'], **s_config)
+
+ if 'logserver_prefix' in s_config:
+ swift_instructions['LOGSERVER_PREFIX'] = \
+ s_config['logserver_prefix']
+ elif self.config.has_option('swift',
+ 'default_logserver_prefix'):
+ swift_instructions['LOGSERVER_PREFIX'] = \
+ s_config['logserver_prefix']
+
+ # Create a set of zuul instructions for each instruction-set
+ # given in the form of NAME_PARAMETER=VALUE
+ for key, value in swift_instructions.items():
+ params['_'.join(['SWIFT', name, key])] = value
+
+ if callable(job.parameter_function):
+ pargs = inspect.getargspec(job.parameter_function)
+ if len(pargs.args) == 2:
+ job.parameter_function(item, params)
+ else:
+ job.parameter_function(item, job, params)
+ self.log.debug("Custom parameter function used for job %s, "
+ "change: %s, params: %s" % (job, item.change,
+ params))
+
def launch(self, job, item, pipeline, dependent_items=[]):
self.log.info("Launch job %s for change %s with dependent changes %s" %
(job, item.change,
@@ -252,6 +299,16 @@
params['ZUUL_REF'] = item.change.ref
params['ZUUL_COMMIT'] = item.change.newrev
+ # The destination_path is a unqiue path for this build request
+ # and generally where the logs are expected to be placed
+ destination_path = os.path.join(item.change.getBasePath(),
+ pipeline.name, job.name, uuid)
+ params['BASE_LOG_PATH'] = item.change.getBasePath()
+ params['LOG_PATH'] = destination_path
+
+ # Allow the job to update the params
+ self.updateBuildParams(job, item, params)
+
# This is what we should be heading toward for parameters:
# required:
@@ -273,16 +330,6 @@
# ZUUL_OLDREV
# ZUUL_NEWREV
- if callable(job.parameter_function):
- pargs = inspect.getargspec(job.parameter_function)
- if len(pargs.args) == 2:
- job.parameter_function(item, params)
- else:
- job.parameter_function(item, job, params)
- self.log.debug("Custom parameter function used for job %s, "
- "change: %s, params: %s" % (job, item.change,
- params))
-
if 'ZUUL_NODE' in params:
name = "build:%s:%s" % (job.name, params['ZUUL_NODE'])
else:
diff --git a/zuul/layoutvalidator.py b/zuul/layoutvalidator.py
index de58c25..15aa687 100644
--- a/zuul/layoutvalidator.py
+++ b/zuul/layoutvalidator.py
@@ -79,11 +79,13 @@
'description': str,
'success-message': str,
'failure-message': str,
+ 'merge-failure-message': str,
'footer-message': str,
'dequeue-on-new-patchset': bool,
'trigger': trigger,
'success': report_actions,
'failure': report_actions,
+ 'merge-failure': report_actions,
'start': report_actions,
'window': window,
'window-floor': window_floor,
@@ -97,6 +99,14 @@
project_template = {v.Required('name'): str}
project_templates = [project_template]
+ swift = {v.Required('name'): str,
+ 'container': str,
+ 'expiry': int,
+ 'max_file_size': int,
+ 'max_file_count': int,
+ 'logserver_prefix': int,
+ }
+
job = {v.Required('name'): str,
'failure-message': str,
'success-message': str,
@@ -107,6 +117,7 @@
'parameter-function': str,
'branch': toList(str),
'files': toList(str),
+ 'swift': toList(swift),
}
jobs = [job]
diff --git a/zuul/lib/swift.py b/zuul/lib/swift.py
new file mode 100644
index 0000000..8f926ad
--- /dev/null
+++ b/zuul/lib/swift.py
@@ -0,0 +1,140 @@
+# Copyright 2014 Rackspace Australia
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import hmac
+from hashlib import sha1
+from time import time
+import os
+import random
+import string
+import swiftclient
+import urlparse
+
+
+class Swift(object):
+ def __init__(self, config):
+ self.config = config
+ self.connection = False
+ if self.config.has_option('swift', 'X-Account-Meta-Temp-Url-Key'):
+ self.secure_key = self.config.get('swift',
+ 'X-Account-Meta-Temp-Url-Key')
+ else:
+ self.secure_key = ''.join(
+ random.choice(string.ascii_uppercase + string.digits)
+ for x in range(20)
+ )
+
+ self.connect()
+
+ def connect(self):
+ if self.config.has_section('swift'):
+ # required
+ authurl = self.config.get('swift', 'authurl')
+
+ user = (self.config.get('swift', 'user')
+ if self.config.has_option('swift', 'user') else None)
+ key = (self.config.get('swift', 'key')
+ if self.config.has_option('swift', 'key') else None)
+ retries = (self.config.get('swift', 'retries')
+ if self.config.has_option('swift', 'retries') else 5)
+ preauthurl = (self.config.get('swift', 'preauthurl')
+ if self.config.has_option('swift', 'preauthurl')
+ else None)
+ preauthtoken = (self.config.get('swift', 'preauthtoken')
+ if self.config.has_option('swift', 'preauthtoken')
+ else None)
+ snet = (self.config.get('swift', 'snet')
+ if self.config.has_option('swift', 'snet') else False)
+ starting_backoff = (self.config.get('swift', 'starting_backoff')
+ if self.config.has_option('swift',
+ 'starting_backoff')
+ else 1)
+ max_backoff = (self.config.get('swift', 'max_backoff')
+ if self.config.has_option('swift', 'max_backoff')
+ else 64)
+ tenant_name = (self.config.get('swift', 'tenant_name')
+ if self.config.has_option('swift', 'tenant_name')
+ else None)
+ auth_version = (self.config.get('swift', 'auth_version')
+ if self.config.has_option('swift', 'auth_version')
+ else 2.0)
+ cacert = (self.config.get('swift', 'cacert')
+ if self.config.has_option('swift', 'cacert') else None)
+ insecure = (self.config.get('swift', 'insecure')
+ if self.config.has_option('swift', 'insecure')
+ else False)
+ ssl_compression = (self.config.get('swift', 'ssl_compression')
+ if self.config.has_option('swift',
+ 'ssl_compression')
+ else True)
+
+ available_os_options = ['tenant_id', 'auth_token', 'service_type',
+ 'endpoint_type', 'tenant_name',
+ 'object_storage_url', 'region_name']
+
+ os_options = {}
+ for os_option in available_os_options:
+ if self.config.has_option('swift', os_option):
+ os_options[os_option] = self.config.get('swift', os_option)
+
+ self.connection = swiftclient.client.Connection(
+ authurl=authurl, user=user, key=key, retries=retries,
+ preauthurl=preauthurl, preauthtoken=preauthtoken, snet=snet,
+ starting_backoff=starting_backoff, max_backoff=max_backoff,
+ tenant_name=tenant_name, os_options=os_options,
+ auth_version=auth_version, cacert=cacert, insecure=insecure,
+ ssl_compression=ssl_compression)
+
+ # Tell swift of our key
+ headers = {}
+ headers['X-Account-Meta-Temp-Url-Key'] = self.secure_key
+ self.connection.post_account(headers)
+
+ self.storage_url, self.auth_token = self.connection.get_auth()
+
+ def generate_form_post_middleware_params(self, destination_prefix='',
+ **kwargs):
+ """Generate the FormPost middleware params for the given settings"""
+
+ # Define the available settings and their defaults
+ settings = {
+ 'container': '',
+ 'expiry': 7200,
+ 'max_file_size': 104857600,
+ 'max_file_count': 10,
+ 'file_path_prefix': ''
+ }
+
+ for key, default in settings.iteritems():
+ if key in kwargs:
+ settings[key] = kwargs[key]
+ elif self.config.has_option('swift', 'default_' + key):
+ settings[key] = self.config.get('swift', 'default_' + key)
+
+ expires = int(time() + settings['expiry'])
+ redirect = ''
+
+ url = os.path.join(self.storage_url, settings['container'],
+ settings['file_path_prefix'],
+ destination_prefix)
+ u = urlparse.urlparse(url)
+
+ hmac_body = '%s\n%s\n%s\n%s\n%s' % (u.path, redirect,
+ settings['max_file_size'],
+ settings['max_file_count'],
+ expires)
+
+ signature = hmac.new(self.secure_key, hmac_body, sha1).hexdigest()
+
+ return url, hmac_body, signature
diff --git a/zuul/model.py b/zuul/model.py
index b20c08e..9028577 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -63,6 +63,7 @@
self.name = name
self.description = None
self.failure_message = None
+ self.merge_failure_message = None
self.success_message = None
self.footer_message = None
self.dequeue_on_new_patchset = True
@@ -171,6 +172,11 @@
return False
return True
+ def didMergerSucceed(self, item):
+ if item.current_build_set.unable_to_merge:
+ return False
+ return True
+
def didAnyJobFail(self, item):
for job in self.getJobs(item.change):
if not job.voting:
@@ -206,9 +212,8 @@
fakebuild.result = 'SKIPPED'
item.addBuild(fakebuild)
- def setUnableToMerge(self, item, msg):
+ def setUnableToMerge(self, item):
item.current_build_set.unable_to_merge = True
- item.current_build_set.unable_to_merge_message = msg
root = self.getJobTree(item.change.project)
for job in root.getJobs():
fakebuild = Build(job, None)
@@ -532,6 +537,7 @@
self._branches = []
self.files = []
self._files = []
+ self.swift = {}
def __str__(self):
return self.name
@@ -556,6 +562,8 @@
if other.files:
self.files = other.files[:]
self._files = other._files[:]
+ if other.swift:
+ self.swift.update(other.swift)
self.hold_following_changes = other.hold_following_changes
self.voting = other.voting
@@ -591,9 +599,10 @@
self.job_trees = []
def addJob(self, job):
- t = JobTree(job)
- self.job_trees.append(t)
- return t
+ if job not in [x.job for x in self.job_trees]:
+ t = JobTree(job)
+ self.job_trees.append(t)
+ return t
def getJobs(self):
jobs = []
@@ -677,7 +686,6 @@
self.commit = None
self.zuul_url = None
self.unable_to_merge = False
- self.unable_to_merge_message = None
self.failing_reasons = []
self.merge_state = self.NEW
@@ -759,6 +767,16 @@
def __init__(self, project):
self.project = project
+ def getBasePath(self):
+ base_path = ''
+ if hasattr(self, 'refspec'):
+ base_path = "%s/%s/%s" % (
+ self.number[-2:], self.number, self.patchset)
+ elif hasattr(self, 'ref'):
+ base_path = "%s/%s" % (self.newrev[:2], self.newrev)
+
+ return base_path
+
def equals(self, other):
raise NotImplementedError()
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index c941a98..18f44db 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -226,6 +226,11 @@
pipeline.precedence = precedence
pipeline.failure_message = conf_pipeline.get('failure-message',
"Build failed.")
+ pipeline.merge_failure_message = conf_pipeline.get(
+ 'merge-failure-message', "Merge Failed.\n\nThis change was "
+ "unable to be automatically merged with the current state of "
+ "the repository. Please rebase your change and upload a new "
+ "patchset.")
pipeline.success_message = conf_pipeline.get('success-message',
"Build succeeded.")
pipeline.footer_message = conf_pipeline.get('footer-message', "")
@@ -233,7 +238,7 @@
'dequeue-on-new-patchset', True)
action_reporters = {}
- for action in ['start', 'success', 'failure']:
+ for action in ['start', 'success', 'failure', 'merge-failure']:
action_reporters[action] = []
if conf_pipeline.get(action):
for reporter_name, params \
@@ -247,6 +252,11 @@
pipeline.start_actions = action_reporters['start']
pipeline.success_actions = action_reporters['success']
pipeline.failure_actions = action_reporters['failure']
+ if len(action_reporters['merge-failure']) > 0:
+ pipeline.merge_failure_actions = \
+ action_reporters['merge-failure']
+ else:
+ pipeline.merge_failure_actions = action_reporters['failure']
pipeline.window = conf_pipeline.get('window', 20)
pipeline.window_floor = conf_pipeline.get('window-floor', 3)
@@ -337,6 +347,10 @@
if files:
job._files = files
job.files = [re.compile(x) for x in files]
+ swift = toList(config_job.get('swift'))
+ if swift:
+ for s in swift:
+ job.swift[s['name']] = s
def add_jobs(job_tree, config_jobs):
for job in config_jobs:
@@ -374,11 +388,10 @@
config_project.update(
{pipeline.name: expanded[pipeline.name] +
config_project.get(pipeline.name, [])})
- # TODO: future enhancement -- add an option to the
- # template block to indicate that duplicate jobs should be
- # merged (especially to handle the case where they have
- # children and you want all of the children to run after a
- # single run of the parent).
+ # TODO: future enhancement -- handle the case where
+ # duplicate jobs have different children and you want all
+ # of the children to run after a single run of the
+ # parent).
layout.projects[config_project['name']] = project
mode = config_project.get('merge-mode', 'merge-resolve')
@@ -936,6 +949,8 @@
self.log.info(" %s" % self.pipeline.success_actions)
self.log.info(" On failure:")
self.log.info(" %s" % self.pipeline.failure_actions)
+ self.log.info(" On merge-failure:")
+ self.log.info(" %s" % self.pipeline.merge_failure_actions)
def getSubmitAllowNeeds(self):
# Get a list of code review labels that are allowed to be
@@ -1334,10 +1349,7 @@
build_set.commit = item.change.newrev
if not build_set.commit:
self.log.info("Unable to merge change %s" % item.change)
- msg = ("This change was unable to be automatically merged "
- "with the current state of the repository. Please "
- "rebase your change and upload a new patchset.")
- self.pipeline.setUnableToMerge(item, msg)
+ self.pipeline.setUnableToMerge(item)
def reportItem(self, item):
if item.reported:
@@ -1370,10 +1382,12 @@
self.log.debug("Reporting change %s" % item.change)
ret = True # Means error as returned by trigger.report
if self.pipeline.didAllJobsSucceed(item):
- self.log.debug("success %s %s" % (self.pipeline.success_actions,
- self.pipeline.failure_actions))
+ self.log.debug("success %s" % (self.pipeline.success_actions))
actions = self.pipeline.success_actions
item.setReportedResult('SUCCESS')
+ elif not self.pipeline.didMergerSucceed(item):
+ actions = self.pipeline.merge_failure_actions
+ item.setReportedResult('MERGER_FAILURE')
else:
actions = self.pipeline.failure_actions
item.setReportedResult('FAILURE')
@@ -1395,66 +1409,72 @@
def formatReport(self, item):
ret = ''
+
+ if not self.pipeline.didMergerSucceed(item):
+ ret += self.pipeline.merge_failure_message
+ if item.dequeued_needing_change:
+ ret += ('\n\nThis change depends on a change that failed to '
+ 'merge.')
+ if self.pipeline.footer_message:
+ ret += '\n\n' + self.pipeline.footer_message
+ return ret
+
if self.pipeline.didAllJobsSucceed(item):
ret += self.pipeline.success_message + '\n\n'
else:
ret += self.pipeline.failure_message + '\n\n'
- if item.dequeued_needing_change:
- ret += "This change depends on a change that failed to merge."
- elif item.current_build_set.unable_to_merge_message:
- ret += item.current_build_set.unable_to_merge_message
+ if self.sched.config.has_option('zuul', 'url_pattern'):
+ url_pattern = self.sched.config.get('zuul', 'url_pattern')
else:
- if self.sched.config.has_option('zuul', 'url_pattern'):
- url_pattern = self.sched.config.get('zuul', 'url_pattern')
+ url_pattern = None
+
+ for job in self.pipeline.getJobs(item.change):
+ build = item.current_build_set.getBuild(job.name)
+ result = build.result
+ pattern = url_pattern
+ if result == 'SUCCESS':
+ if job.success_message:
+ result = job.success_message
+ if job.success_pattern:
+ pattern = job.success_pattern
+ elif result == 'FAILURE':
+ if job.failure_message:
+ result = job.failure_message
+ if job.failure_pattern:
+ pattern = job.failure_pattern
+ if pattern:
+ url = pattern.format(change=item.change,
+ pipeline=self.pipeline,
+ job=job,
+ build=build)
else:
- url_pattern = None
- for job in self.pipeline.getJobs(item.change):
- build = item.current_build_set.getBuild(job.name)
- result = build.result
- pattern = url_pattern
- if result == 'SUCCESS':
- if job.success_message:
- result = job.success_message
- if job.success_pattern:
- pattern = job.success_pattern
- elif result == 'FAILURE':
- if job.failure_message:
- result = job.failure_message
- if job.failure_pattern:
- pattern = job.failure_pattern
- if pattern:
- url = pattern.format(change=item.change,
- pipeline=self.pipeline,
- job=job,
- build=build)
+ url = build.url or job.name
+ if not job.voting:
+ voting = ' (non-voting)'
+ else:
+ voting = ''
+ if self.report_times and build.end_time and build.start_time:
+ dt = int(build.end_time - build.start_time)
+ m, s = divmod(dt, 60)
+ h, m = divmod(m, 60)
+ if h:
+ elapsed = ' in %dh %02dm %02ds' % (h, m, s)
+ elif m:
+ elapsed = ' in %dm %02ds' % (m, s)
else:
- url = build.url or job.name
- if not job.voting:
- voting = ' (non-voting)'
- else:
- voting = ''
- if self.report_times and build.end_time and build.start_time:
- dt = int(build.end_time - build.start_time)
- m, s = divmod(dt, 60)
- h, m = divmod(m, 60)
- if h:
- elapsed = ' in %dh %02dm %02ds' % (h, m, s)
- elif m:
- elapsed = ' in %dm %02ds' % (m, s)
- else:
- elapsed = ' in %ds' % (s)
- else:
- elapsed = ''
- name = ''
- if self.sched.config.has_option('zuul', 'job_name_in_report'):
- if self.sched.config.getboolean('zuul',
- 'job_name_in_report'):
- name = job.name + ' '
- ret += '- %s%s : %s%s%s\n' % (name, url, result, elapsed,
- voting)
- ret += '\n'
- ret += self.pipeline.footer_message
+ elapsed = ' in %ds' % (s)
+ else:
+ elapsed = ''
+ name = ''
+ if self.sched.config.has_option('zuul', 'job_name_in_report'):
+ if self.sched.config.getboolean('zuul',
+ 'job_name_in_report'):
+ name = job.name + ' '
+ ret += '- %s%s : %s%s%s\n' % (name, url, result, elapsed,
+ voting)
+ if self.pipeline.footer_message:
+ ret += '\n' + self.pipeline.footer_message
return ret
def formatDescription(self, build):