systemd: logs: Use reportd for reporting problems

Currently, crash reporting is done by running the command-line tool, which asks
for user input on the standard streams. This completely breaks most workflows,
as, for example, one needs to log in to their RH Bugzilla account to file a bug,
but the prompt is shown on the server-side only.

reportd was written for the express purpose of dealing with such cases.
With version 0.5, it supports signaling clients about incoming prompts
and provides an API to deal with them.

Closes: #11150
This commit is contained in:
Ernestas Kulik 2019-02-11 09:39:05 +01:00 committed by Martin Pitt
parent 6dc388a470
commit d1b7154026
9 changed files with 814 additions and 129 deletions

View File

@ -434,3 +434,40 @@ body {
.fa-exclamation-circle {
color: var(--pf-global--danger-color--100);
}
.full-width {
width: 100%;
}
table.reporting-table tr:first-child td {
border-top: none;
}
table.reporting-table td:first-child {
white-space: nowrap;
width: 40%;
}
table.reporting-table td:nth-child(2) {
text-align: start;
width: 60%;
}
table.reporting-table td:nth-child(2) > .spinner {
display: inline-block;
margin-right: 0.5em;
vertical-align: middle;
}
td.report-column {
flex-direction: row-reverse;
}
#journal-entry-heading {
padding-bottom: 0.5rem;
}
#journal-entry-message {
padding-left: 1.5rem;
}

View File

@ -81,17 +81,11 @@ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
<div id="journal-entry" class="container-fluid" hidden>
<ol class="breadcrumb">
<li><a tabindex="0" id="journal-navigate-home" translate="yes">Logs</a></li>
<li class="active" translate="yes">Entry</li>
<li id="journal-entry-crumb" class="active" translate="yes">Entry</li>
</ol>
<div class="panel panel-default">
<div class="panel-heading">
<span id="journal-entry-id"></span>
<span id="journal-entry-date" class="pull-right"></span>
</div>
<div id="journal-entry-message"></div>
<table class="info-table-ct" id="journal-entry-fields">
</table>
</div>
<h2 id="journal-entry-heading"></h2>
<table class="info-table-ct panel panel-default" id="journal-entry-fields">
</table>
</div>
<script type="text/javascript" src="logs.js"></script>

View File

@ -20,6 +20,8 @@
import $ from "jquery";
import cockpit from "cockpit";
import { journal } from "journal";
import moment from "moment";
import { init_reporting } from "./reporting.jsx";
import ReactDOM from 'react-dom';
import React from 'react';
@ -426,12 +428,14 @@ $(function() {
var cursor = cockpit.location.path[0];
var out = $('#journal-entry-fields');
const reportingTable = document.getElementById("journal-entry-reporting-table");
if (reportingTable != null) {
reportingTable.remove();
}
out.empty();
function show_entry(entry) {
var d = new Date(entry.__REALTIME_TIMESTAMP / 1000);
$('#journal-entry-date').text(d.toString());
var id;
if (entry.SYSLOG_IDENTIFIER)
id = entry.SYSLOG_IDENTIFIER;
@ -446,13 +450,20 @@ $(function() {
id = entry.PROBLEM_BINARY;
}
$('#journal-entry-id').text(id);
$('#journal-entry-heading').text(id);
const crumb = $("#journal-entry-crumb");
const date = moment(new Date(entry.__REALTIME_TIMESTAMP / 1000));
if (is_problem) {
crumb.text(cockpit.format(_("$0: crash at $1"), id, date.format("YYYY-MM-DD HH:mm:ss")));
find_problems().done(function() {
create_problem(out, entry);
});
} else {
crumb.text(cockpit.format(_("Entry at $0"), date.format("YYYY-MM-DD HH:mm:ss")));
create_entry(out, entry);
}
}
@ -476,8 +487,21 @@ $(function() {
});
}
function create_message_row(entry) {
const reasonColumn = document.createElement("th");
reasonColumn.setAttribute("colspan", 2);
reasonColumn.setAttribute("id", "journal-entry-message");
reasonColumn.appendChild(document.createTextNode(journal.printable(entry.MESSAGE)));
const reason = document.createElement("tr");
reason.appendChild(reasonColumn);
return reason;
}
function create_entry(out, entry) {
$('#journal-entry-message').text(journal.printable(entry.MESSAGE));
out.append(create_message_row(entry));
var keys = Object.keys(entry).sort();
$.each(keys, function (i, key) {
if (key !== 'MESSAGE') {
@ -515,7 +539,11 @@ $(function() {
.replaceWith(new_content);
}
$('#journal-entry-message').text('');
const heading = document.createElement("h3");
heading.appendChild(document.createTextNode(_("Extended Information")));
const caption = document.createElement("caption");
caption.appendChild(heading);
var ge_t = $('<li class="active">').append($('<a tabindex="0">').append($('<span translate="yes">').text(_("General"))));
var pi_t = $('<li>').append($('<a tabindex="0">').append($('<span translate="yes">').text(_("Problem info"))));
@ -527,56 +555,18 @@ $(function() {
.append(
$('<tr>').append($('<div class="panel-group" id="accordion-markup">')));
var tab = $('<ul class="nav nav-tabs nav-tabs-pf">');
var tab = $('<ul class="nav nav-tabs nav-tabs-pf">')
.attr("id", "problem-navigation");
var d_btn = $('<button class="btn btn-danger problem-btn btn-delete pficon pficon-delete">');
var r_btn = $();
if (problem.IsReported) {
for (var pid = 0; pid < problem.Reports.length; pid++) {
if (problem.Reports[pid][0] === 'ABRT Server') {
var url = problem.Reports[pid][1].URL.v.v;
r_btn = $('<a class="problem-btn">')
.attr('href', url)
.attr("target", "_blank")
.attr("rel", "noopener noreferrer")
.text(_("Reported"));
break;
}
}
} else if (problem.CanBeReported) {
r_btn = $('<button class="btn btn-primary problem-btn">').text(_("Report"));
r_btn.click(function() {
tab.children(':last-child').replaceWith($('<div class="spinner problem-btn">'));
var proc = cockpit.spawn(['reporter-ureport', '-d', problem.ID], { superuser: 'true' });
proc.done(function() {
window.location.reload();
});
proc.fail(function(ex) {
var message;
// 70 is 'This problem has already been reported'
if (ex.exit_status === 70) {
window.location.reload();
return;
} else if (ex.problem === 'access-denied') {
message = _("Not authorized to upload-report");
} else if (ex.problem === "not-found") {
message = _("Reporter 'reporter-ureport' not found.");
} else {
message = _("Reporting was unsucessful. Try running `reporter-ureport -d " + problem.ID + "`");
}
const reportingTable = document.createElement("div");
reportingTable.setAttribute("id", "journal-entry-reporting-table");
$('<div class="pf-c-alert pf-m-danger pf-m-inline" aria-label="inline danger alert">')
.append($('<div class="pf-c-alert__icon">' +
'<span class="pficon pficon-error-circle-o">' +
'</div>'),
$('<h4 class="pf-c-alert__title">').text(message)
)
.insertAfter(".breadcrumb");
tab.children(':last-child').replaceWith($('<span>'));
});
});
}
const journalTable = document.getElementById("journal-entry-fields");
journalTable.insertAdjacentElement("beforebegin", reportingTable);
init_reporting(problem, reportingTable);
ge_t.click(function() {
switch_tab(ge_t, ge);
@ -615,13 +605,15 @@ $(function() {
tab.append(pi_t);
tab.append(pd_t);
tab.append(d_btn);
tab.append(r_btn);
var header = $('<tr>').append(
$('<th colspan=2>').append(tab));
out.html(header).append(ge);
out.html(header).append(create_message_row(entry));
out.append(ge);
out.prepend(caption);
out.css("margin-bottom", "0px");
create_problem_details(problem, pi, pd);
}

511
pkg/systemd/reporting.jsx Normal file
View File

@ -0,0 +1,511 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import cockpit from "cockpit";
import React from "react";
import ReactDOM from 'react-dom';
import { show_modal_dialog } from "cockpit-components-dialog.jsx";
const _ = cockpit.gettext;
const TaskState = Object.freeze({
READY: 0,
RUNNING: 1,
COMPLETED: 2,
ERROR: 3,
CANCELED: 4,
});
const PromptType = Object.freeze({
ASK: 0,
ASK_YES_NO: 1,
ASK_YES_NO_YESFOREVER: 2,
ASK_YES_NO_SAVE: 3,
ASK_PASSWORD: 4,
});
const ProblemState = Object.freeze({
REPORTABLE: 0,
REPORTING: 1,
REPORTED: 2,
UNREPORTABLE: 3,
});
const client = cockpit.dbus("org.freedesktop.problems", { superuser: "try" });
var reportd_client;
// For one-off fetches of properties to avoid setting up a cache for everything.
function get_problem_properties(problem) {
function executor(resolve, reject) {
client.wait().then(() => resolve(client));
}
return new Promise(executor)
.then(() => client.call(problem.path,
"org.freedesktop.DBus.Properties",
"GetAll", ["org.freedesktop.Problems2.Entry"]));
}
class FAFWorkflowRow extends React.Component {
constructor(props) {
super(props);
this.state = {
problemState: ProblemState.REPORTABLE,
process: null,
reportLinks: [],
message: "",
};
this._onCancelButtonClick = this._onCancelButtonClick.bind(this);
this._onReportButtonClick = this._onReportButtonClick.bind(this);
this.updateStatusFromBus = this.updateStatusFromBus.bind(this);
this.updateStatusFromBus();
}
updateStatusFromBus() {
get_problem_properties(this.props.problem)
.catch(exception => {
this.setState({ problemState: ProblemState.UNREPORTABLE });
console.error(cockpit.format("Getting properties for problem $0 failed: $1", this.props.problem.path, exception));
})
.then((properties) => {
if (!properties) {
return;
}
if (!properties[0].CanBeReported.v) {
this.setState({ problemState: ProblemState.UNREPORTABLE });
return;
}
const reportLinks = [];
let reported = false;
for (const report of properties[0].Reports.v) {
if (report[0] === "ABRT Server") {
if ("URL" in report[1]) {
reportLinks.push(report[1].URL.v.v);
}
reported = true;
}
}
if (reported) {
this.setState({
problemState: ProblemState.REPORTED,
reportLinks: reportLinks,
});
}
});
}
_onCancelButtonClick(event) {
this.state.process.close("canceled");
}
_onReportButtonClick(event) {
this.setState({ problemState: ProblemState.UNREPORTABLE });
const process = cockpit.spawn(["reporter-ureport", "-d", this.props.problem.ID],
{
err: "out",
superuser: "true",
})
.stream((data) => this.setState({ message: data, }))
.then(() => this.setState({ problemState: ProblemState.REPORTED, }))
.catch(exception => {
this.setState({ problemState: ProblemState.REPORTABLE, });
if (exception.exit_signal != null) {
console.error(cockpit.format("reporter-ureport was killed with signal $0", exception.exit_signal));
}
})
.finally(() => this.updateStatusFromBus());
this.setState({
problemState: ProblemState.REPORTING,
process: process,
});
}
render() {
return <WorkflowRow label={_("Report to ABRT Analytics")}
message={this.state.message}
onCancelButtonClick={this._onCancelButtonClick}
onReportButtonClick={this._onReportButtonClick}
problemState={this.state.problemState}
reportLinks={this.state.reportLinks} />;
}
}
class BusWorkflowRow extends React.Component {
constructor(props) {
super(props);
this.state = {
label: this.props.workflow[1],
message: "",
problemState: ProblemState.REPORTABLE,
reportLinks: [],
task: null,
};
this._createTask = this._createTask.bind(this);
this._onCancelButtonClick = this._onCancelButtonClick.bind(this);
this._onCreateTask = this._onCreateTask.bind(this);
this._onReportButtonClick = this._onReportButtonClick.bind(this);
this.updateStatusFromBus = this.updateStatusFromBus.bind(this);
this.updateStatusFromBus();
}
_createTask(client) {
return client.call("/org/freedesktop/reportd/Service",
"org.freedesktop.reportd.Service", "CreateTask",
[this.props.workflow[0], this.props.problem.path])
.then(result => this._onCreateTask(result[0], client));
}
_onCancelButtonClick(event) {
this.state.task.Cancel();
}
_onCreateTask(object_path, client) {
const task_proxy = client.proxy("org.freedesktop.reportd.Task", object_path);
task_proxy
.wait()
.then((object_path) => {
task_proxy.addEventListener("changed", (event, data) => {
switch (data.Status) {
case TaskState.RUNNING:
// To avoid a needless D-Bus round trip.
return;
case TaskState.CANCELED:
this.setState({ message: _("Reporting was canceled"), });
// falls through
case TaskState.ERROR:
this.setState({ problemState: ProblemState.REPORTABLE, });
break;
case TaskState.COMPLETED:
this.setState({ problemState: ProblemState.REPORTED, });
break;
default:
break;
}
this.updateStatusFromBus();
});
task_proxy.addEventListener("Prompt", (event, object_path, message, type) => {
this.setState({ message: _("Waiting for input…") });
const task_prompt = client.proxy("org.freedesktop.reportd.Task.Prompt", object_path);
const props = {
body: <p>{message}</p>,
};
const footerProps = {
actions: [],
cancel_clicked: () => {
task_proxy.Cancel();
},
};
switch (type) {
case PromptType.ASK:
case PromptType.ASK_PASSWORD:
props.body = (
<div>
<p>{message}</p>
<input className="full-width"
ref={(input) => { this.input = input }}
type={type == PromptType.ASK_PASSWORD ? "password" : "text"} />
</div>
);
footerProps.actions.push(
{
caption: _("Send"),
clicked: () => {
return task_prompt.wait().then(() => {
task_prompt.Input = this.input.value;
task_prompt.Commit();
});
},
style: "primary",
}
);
break;
case PromptType.ASK_YES_NO_YESFOREVER:
case PromptType.ASK_YES_NO:
case PromptType.ASK_YES_NO_SAVE:
footerProps.actions.push(
{
caption: _("Yes"),
clicked: () => {
return task_prompt.wait().then(() => {
task_prompt.Response = true;
task_prompt.Commit();
});
},
},
{
caption: _("No"),
clicked: (callback) => {
return task_prompt.wait().then(() => {
task_prompt.Response = false;
task_prompt.Commit();
});
},
},
);
}
show_modal_dialog(props, footerProps);
});
task_proxy.addEventListener("Progress", (event, message) => {
if (/^\.+$/.exec(message) === null) {
// abrt-retrace-client starts printing dots if the last message it receives is repeated
this.setState({ message: message, });
}
});
this.setState({ task: task_proxy, });
task_proxy.Start().catch(ex => {
/* GLib encodes errors for transport over the wire,
* but we dont have a good way of decoding them without calling into GIO.
*
* https://developer.gnome.org/gio/stable/gio-GDBusError.html#g-dbus-error-encode-gerror
*
* 19 is G_IO_ERROR_CANCELLED. No need to handle user cancellations.
*/
if (/Code19/.exec(ex.name) != null) {
return;
}
console.error(cockpit.format("reportd task for workflow $0 did not finish: $1", this.props.workflow[0], (ex.problem || ex.message)));
this.setState({ message: _("Reporting failed") });
});
})
.catch(ex => console.error(cockpit.format("Setting up a D-Bus proxy for $0 failed: $1", object_path, ex)));
}
_onReportButtonClick(event) {
this.setState({ problemState: ProblemState.UNREPORTABLE });
this.setState({
message: _("Waiting to start…"),
problemState: ProblemState.REPORTING,
});
reportd_client
.wait()
.catch(exception => console.error(cockpit.format("Channel for reportd D-Bus client closed: $0", exception.problem || exception.message)))
.then(() => this._createTask(reportd_client))
.catch(exception => {
const message = cockpit.format("reportd task could not be created: $0", (exception.problem || exception.message));
this.setState({
message: message,
problemState: ProblemState.REPORTABLE,
});
console.error(message);
});
}
render() {
return <WorkflowRow label={this.state.label}
message={this.state.message}
onCancelButtonClick={this._onCancelButtonClick}
onReportButtonClick={this._onReportButtonClick}
problemState={this.state.problemState}
reportLinks={this.state.reportLinks} />;
}
updateStatusFromBus() {
const on_get_properties = properties => {
if (!properties[0].CanBeReported.v) {
this.setState({ problemState: ProblemState.UNREPORTABLE });
return;
}
const reportLinks = [];
let reported = false;
for (const report of properties[0].Reports.v) {
if (!("WORKFLOW" in report[1])) {
continue;
}
if (this.props.workflow[0] !== report[1].WORKFLOW.v.v) {
continue;
}
if (report[0] === "ABRT Server" || report[0] === "uReport") {
continue;
}
if ("URL" in report[1]) {
reportLinks.push(report[1].URL.v.v);
}
reported = true;
}
if (reported) {
this.setState({
problemState: ProblemState.REPORTED,
reportLinks: reportLinks,
});
}
};
const on_get_properties_rejected = exception => {
this.setState({ problemState: ProblemState.UNREPORTABLE });
console.error(cockpit.format("Getting properties for problem $0 failed: $1", this.props.problem.path, exception));
};
get_problem_properties(this.props.problem).then(on_get_properties, on_get_properties_rejected);
}
}
function WorkflowRow(props) {
let status = props.message;
if (props.problemState === ProblemState.REPORTED) {
const icon = <i className="fa fa-external-link" aria-hidden="true" />;
if (props.reportLinks.length === 1) {
status = (
<a href={props.reportLinks[0]} rel="noopener noreferrer" target="_blank">
{_("View report")} {icon}
</a>
);
} else if (props.reportLinks.length > 1) {
const reportLinks = props.reportLinks.map((reportLink, index) => [
index > 0 && ", ",
<a key={index.toString()} href={reportLink} rel="noopener noreferrer" target="_blank">
{index + 1} {icon}
</a>
]);
status = <p>{_("Reports:")} {reportLinks}</p>;
} else {
status = _("Reported; no links available");
}
}
let button = null;
if (props.problemState === ProblemState.REPORTING) {
button = (
<button key={"cancel_" + props.label}
className="btn btn-danger"
onClick={props.onCancelButtonClick}>
{_("Cancel")}
</button>
);
} else {
button = (
<button key={"report_" + props.label}
className="btn btn-primary"
disabled={props.problemState !== ProblemState.REPORTABLE}
onClick={props.problemState === ProblemState.REPORTABLE ? props.onReportButtonClick : undefined}>
{_("Report")}
</button>
);
}
return (
<tr>
<td>{props.label}</td>
<td>
{props.problemState === ProblemState.REPORTING && <span className="spinner spinner-xs" />}
{status}
</td>
<td>{button}</td>
</tr>
);
}
class ReportingTable extends React.Component {
constructor(props) {
super(props);
this.state = {
workflows: [],
};
this.getWorkflows = this.getWorkflows.bind(this);
reportd_client
.wait()
.then(() => this.getWorkflows(reportd_client))
.catch(exception => console.error(cockpit.format("Channel for reportd D-Bus client closed: $0", exception.problem || exception.message)));
}
getWorkflows(client) {
client.call("/org/freedesktop/reportd/Service", "org.freedesktop.reportd.Service", "GetWorkflows", [this.props.problem.path])
.then((args, options) => this.setState({ workflows: args[0], }))
.catch(exception => console.error(cockpit.format("Failed to get workflows for problem $0: $1", this.props.problem.path, (exception.problem || exception.message))));
}
render() {
return (
<table className="panel panel-default reporting-table table">
<caption>
<h3>{_("Crash Reporting")}</h3>
</caption>
<tbody>
<FAFWorkflowRow problem={this.props.problem} />
{
this.state.workflows.map((workflow, index) => [
<BusWorkflowRow key={index.toString()}
problem={this.props.problem}
workflow={workflow} />
])
}
</tbody>
</table>
);
}
}
export function init_reporting(problem, container) {
const permission = cockpit.permission({ admin: true });
const on_permission_changed = () => {
// reportd may use ABRT API that requires authorization, but it cannot
// be given using polkit, as the calling process will always be reportd
// and not cockpit-bridge. However, UID 0 is always authorized, hence
// the spawning of the system service and using the system bus here.
//
// TODO: Only use system bus when reportd is merged into ABRT
// (https://github.com/abrt/reportd/issues/8).
reportd_client = cockpit.dbus("org.freedesktop.reportd",
{
bus: permission.allowed ? "system" : "session",
track: true,
});
permission.close();
permission.removeEventListener("changed", on_permission_changed);
ReactDOM.render(<ReportingTable problem={problem} />, container);
};
permission.addEventListener("changed", on_permission_changed);
}

View File

@ -583,7 +583,7 @@ CMD ["/bin/sh"]
b.enter_page("/system/logs")
# check for abrtd service in text
b.wait_in_text("#journal-entry-id", "sleep")
b.wait_in_text("#journal-entry-heading", "sleep")
b.wait_in_text("#journal-entry-fields", "abrtd.service")

View File

@ -41,6 +41,13 @@ class TestJournal(MachineCase):
else:
browser.click(button_text_selector)
def crash(self):
m = self.machine
m.execute("ulimit -c unlimited")
sleep = m.spawn("sleep 1m", "sleep.log")
m.execute("kill -SEGV %d" % (sleep))
def testBasic(self):
b = self.browser
b.wait_timeout(120)
@ -380,12 +387,11 @@ ExecStart=/bin/sh -c 'for s in $(seq 10); do echo SLOW; sleep 0.1; done; sleep 1
def testAbrtSegv(self):
self.allow_core_dumps = True
b = self.browser
m = self.machine
self.crash()
self.login_and_go("/system/logs")
m.execute("ulimit -c unlimited; timeout --signal=SEGV 0.1s sleep 5s || true")
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in %s')" % self.crash_fn
self.select_from_dropdown(b, "#journal-prio-menu", "Critical and above")
@ -420,30 +426,33 @@ ExecStart=/bin/sh -c 'for s in $(seq 10); do echo SLOW; sleep 0.1; done; sleep 1
def testAbrtDelete(self):
self.allow_core_dumps = True
b = self.browser
m = self.machine
# A bit of a race might happen if you delete the journal entry whilst
# the reporting code is doing its thing.
self.allow_browser_errors("Failed to get workflows for problem /org/freedesktop/Problems2/Entry/.*:.*")
self.allow_browser_errors("Getting properties for problem /org/freedesktop/Problems2/Entry/.* failed:.*")
self.crash()
self.login_and_go("/system/logs")
m.execute("ulimit -c unlimited; timeout --signal=SEGV 0.1s sleep 5s || true")
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in %s')" % self.crash_fn
b.click(sel)
b.wait_in_text("#journal-entry-id", "sleep")
b.wait_in_text("#journal-entry-heading", "sleep")
sel = "#journal-entry-fields .nav .btn-danger"
b.click(sel)
b.wait_visible("#journal-box")
b.wait_in_text('#journal-box', "crashed in " + self.crash_fn)
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in %s')" % self.crash_fn
b.click(sel)
b.wait_in_text("#journal-entry-id", "sleep")
b.wait_in_text("#journal-entry-heading", "sleep")
# details view should hide log view
b.wait_not_visible('.cockpit-log-panel')
b.wait_present("#journal-entry-message:contains('crashed in %s')" % self.crash_fn)
b.wait_not_present("#journal-entry-fields .nav")
b.wait_not_present("#journal-entry-fields .nav .btn-danger")
@skipImage("ABRT does not work on i386", "fedora-i386")
@skipImage("ABRT not available", "debian-stable", "debian-testing", "ubuntu-stable",
@ -455,54 +464,139 @@ ExecStart=/bin/sh -c 'for s in $(seq 10); do echo SLOW; sleep 0.1; done; sleep 1
b = self.browser
m = self.machine
# We restart the reportd service,
# which causes the D-Bus proxy to spit out an error when it tries to
# query its managed objects.
self.allow_journal_messages('.*Remote peer disconnected')
m.upload(["verify/files/mock-bugzilla-server.py"], "/tmp/")
m.spawn("setsid /tmp/mock-bugzilla-server.py", "mock-bugzilla.log")
m.execute("echo 'BugzillaURL=http://localhost:8080' >> /etc/libreport/plugins/bugzilla.conf")
# start mock FAF server
m.upload(["verify/files/mock-faf-server.py"], "/tmp/")
m.spawn("setsid /tmp/mock-faf-server.py", "mock-faf.log")
m.execute("echo 'URL=http://localhost:12345' >> /etc/libreport/plugins/ureport.conf")
self.crash()
self.login_and_go("/system/logs")
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in __nanosleep()')"
b.click(sel)
sel = "table.reporting-table tr:first-child .btn:contains('Report')"
b.click(sel)
sel = "table.reporting-table tr:first-child a[href$='/reports/bthash/123deadbeef'"
b.wait_present(sel)
# "Unreport" the problem to test reporting unknown problem
m.execute('find /var/spool/abrt -name "reported_to" -or -name "ureports_counter" | xargs rm')
# The service also needs to be restarted, because the daemon maintains
# its own cache that interferes with resetting the state.
m.execute('systemctl restart reportd')
# We have no network access.
m.execute("sed -i 's|abrt-action-perform-ccpp-analysis|true|' /etc/libreport/events.d/ccpp_event.conf")
# reporter-bugzilla will not be run without a duphash file present.
m.execute("find /var/spool/abrt -depth -maxdepth 1 | head -1 | xargs -I {} cp '{}/uuid' '{}/duphash'")
b.reload()
b.enter_page("/system/logs")
sel = "table.reporting-table tr:nth-child(2) button:contains('Report')"
b.click(sel)
test_user = 'correcthorsebatterystaple'
for purpose in ['text', 'password']:
b.wait_visible("#cockpit_modal_dialog .modal-content input[type='%s']" % (purpose))
sel = "#cockpit_modal_dialog .modal-content input"
b.set_val(sel, test_user)
sel = "#cockpit_modal_dialog .modal-footer button:contains('Send')"
b.click(sel)
sel = "#cockpit_modal_dialog .modal-content p:contains('Password')"
b.wait_not_present(sel)
sel = "#cockpit_modal_dialog .modal-footer button:contains('No')"
b.click(sel)
sel = "table.reporting-table tr:nth-child(2) a[href='https://bugzilla.example.com/show_bug.cgi?id=123456']"
b.wait_present(sel)
@skipImage("ABRT does not work on i386", "fedora-i386")
@skipImage("ABRT not available", "debian-stable", "debian-testing", "ubuntu-stable",
"ubuntu-1804", "fedora-coreos", "rhel-8-1", "rhel-8-1-distropkg", "rhel-8-2")
def testAbrtReportCancel(self):
self.allow_core_dumps = True
b = self.browser
m = self.machine
m.upload(["verify/files/mock-bugzilla-server.py"], "/tmp/")
m.spawn("setsid /tmp/mock-bugzilla-server.py", "mock-bugzilla.log")
m.execute("echo 'BugzillaURL=http://localhost:8080' >> /etc/libreport/plugins/bugzilla.conf")
# start mock FAF server
m.upload(["verify/files/mock-faf-server.py"], "/tmp/")
m.spawn("setsid /tmp/mock-faf-server.py", "mock-faf.log")
m.execute("echo 'URL=http://localhost:12345' >> /etc/libreport/plugins/ureport.conf")
self.crash()
self.login_and_go("/system/logs")
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in __nanosleep()')"
b.click(sel)
# Something long-running to not pop up an unexpected dialog.
m.execute("sed -i 's|abrt-action-perform-ccpp-analysis|echo Cancel me; sleep 5m|' /etc/libreport/events.d/ccpp_event.conf")
sel = "table.reporting-table tr:nth-child(2) button:contains('Report')"
b.click(sel)
sel = "table.reporting-table tr:nth-child(2) td:contains('Cancel me')"
b.wait_visible(sel)
sel = "table.reporting-table tr:nth-child(2) button:contains('Cancel')"
b.click(sel)
sel = "table.reporting-table tr:nth-child(2) td:contains('Reporting was canceled')"
b.wait_present(sel)
@skipImage("ABRT does not work on i386", "fedora-i386")
@skipImage("ABRT not available", "debian-stable", "debian-testing", "ubuntu-stable",
"ubuntu-1804", "fedora-coreos", "rhel-8-1", "rhel-8-1-distropkg", "rhel-8-2")
def testAbrtReportNoReportd(self):
self.allow_core_dumps = True
b = self.browser
m = self.machine
self.allow_browser_errors("Channel for reportd D-Bus client closed: .*")
# start mock FAF server
m.upload(["verify/files/mock-faf-server.py"], "/tmp/")
m.execute("setsid /tmp/mock-faf-server.py >/tmp/mock-faf.log 2>&1 &")
m.execute("echo 'URL=http://localhost:12345' >> /etc/libreport/plugins/ureport.conf")
self.login_and_go("/system/logs")
m.execute("systemctl mask --now reportd")
m.execute("ulimit -c unlimited; echo 'sleep 42m &' > slp; chmod u+x slp")
m.execute("./slp; pkill -x -SEGV sleep")
self.crash()
self.login_and_go("/system/logs")
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in __nanosleep()')"
b.click(sel)
# Wait until loaded (when delete button is loaded, all is loaded)
sel = "#journal-entry-fields .nav .btn-danger"
b.wait_visible(sel)
sel = "#journal-entry-fields .nav .btn-primary:contains('Report')"
sel = "table.reporting-table tr:first-child .btn:contains('Report')"
b.click(sel)
# jQuery magic on this page updates the whole frame for changing to "Reported" state
b.expect_load_frame("cockpit1:localhost/system/logs")
sel = "#journal-entry-fields .nav .problem-btn:contains('Reported')"
b.wait_visible(sel)
self.assertIn("/reports/bthash/123deadbeef", b.attr(sel, 'href'))
# "Unreport" the problem to test reporting unknown problem
m.execute('find /var/spool/abrt -name "reported_to" | xargs rm')
b.reload()
b.enter_page("/system/logs")
sel = "#journal-entry-fields"
sel = "table.reporting-table tr:first-child a[href$='/reports/bthash/123deadbeef'"
b.wait_present(sel)
self.allow_journal_messages('.*This problem has already been reported.')
self.allow_journal_messages('.*http://localhost:12345/reports/42/')
self.allow_journal_messages('.*https://bugzilla.example.com/show_bug.cgi\?id=123456')
sel = "#journal-entry-fields .nav .btn-primary:contains('Report')"
b.click(sel)
# jQuery magic on this page updates the whole frame for changing to "Reported" state
b.expect_load_frame("cockpit1:localhost/system/logs")
sel = "#journal-entry-fields .nav .problem-btn:contains('Reported')"
b.wait_visible(sel)
self.assertIn("/reports/bthash/123deadbeef", b.attr(sel, 'href'))
if __name__ == '__main__':
test_main()

View File

@ -0,0 +1,44 @@
#!/usr/bin/python3
from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
class RequestHandler(SimpleXMLRPCRequestHandler):
rpc_paths = ('/xmlrpc.cgi')
with SimpleXMLRPCServer(('', 8080), requestHandler=RequestHandler) as server:
class Bug:
@server.register_function(name='Bug.add_attachment')
def add_attachment(self):
return {'ids': [42]}
@server.register_function(name='Bug.create')
def create(self):
return {'id': 42}
@server.register_function(name='Bug.search')
def search(self):
return {'bugs': []}
@server.register_function(name='Bug.update')
def update(self):
return {'bugs': []}
class Bugzilla:
@server.register_function(name='Bugzilla.version')
def version(self):
return {'version': '42'}
class User:
@server.register_function(name='User.login')
def login(self):
return {'id': 0, 'token': '70k3n'}
@server.register_function(name='User.logout')
def logout(self):
return {}
server.register_instance(Bugzilla())
server.serve_forever()

View File

@ -9,6 +9,29 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_POST_attach(self):
self.wfile.write(json.dumps({'result': True}).encode("UTF-8"))
def do_POST_new(self):
response = {
'bthash': '123deadbeef',
'message': 'http://localhost:12345/reports/42/\nhttps://bugzilla.example.com/show_bug.cgi?id=123456',
'reported_to': [
{
'type': 'url',
'value': 'http://localhost:12345/reports/42/',
'reporter': 'ABRT Server'
},
{
'type': 'url',
'value': 'https://bugzilla.example.com/show_bug.cgi?id=123456',
'reporter': 'Bugzilla'
}
],
'result': False
}
self.wfile.write(json.dumps(response, indent=2).encode('UTF-8'))
def do_POST(self):
form = cgi.FieldStorage(
fp=self.rfile,
@ -32,27 +55,12 @@ class Handler(BaseHTTPRequestHandler):
sys.stderr.write('Received invalid JSON data:\n{0}\n'.format(json_str))
return
response = {
'bthash': '123deadbeef',
'message': 'http://localhost:12345/reports/42/\nhttps://bugzilla.example.com/show_bug.cgi?id=123456',
'reported_to': [
{
'type': 'url',
'value': 'http://localhost:12345/reports/42/',
'reporter': 'ABRT Server'
},
{
'type': 'url',
'value': 'https://bugzilla.example.com/show_bug.cgi?id=123456',
'reporter': 'Bugzilla'
}
],
'result': next(Handler.known)
}
self.wfile.write(json.dumps(response, indent=2).encode('UTF-8'))
if self.path == '/reports/attach/':
self.do_POST_attach()
elif self.path == '/reports/new/':
self.do_POST_new()
PORT = 12345
Handler.known = [True, False].__iter__()
httpd = HTTPServer(("", PORT), Handler)
httpd.serve_forever()

View File

@ -369,6 +369,11 @@ Recommends: setroubleshoot-server >= 3.3.3
Provides: cockpit-selinux = %{version}-%{release}
Provides: cockpit-sosreport = %{version}-%{release}
%endif
%if 0%{?fedora} >= 29
# 0.7.0 (actually) supports task cancellation.
# 0.7.1 fixes tasks never announcing completion.
Recommends: (reportd >= 0.7.1 if abrt)
%endif
# NPM modules which are also available as packages
Provides: bundled(js-jquery) = %{npm-version:jquery}
Provides: bundled(js-moment) = %{npm-version:moment}