subscriptions: Install insights-client on demand

Closes #13027
This commit is contained in:
Marius Vollmer 2019-10-22 14:04:45 +03:00 committed by Martin Pitt
parent 1ecaefc71c
commit c10ed08c7b
11 changed files with 321 additions and 77 deletions

View file

@ -125,6 +125,7 @@ SUBST_RULE = \
-e 's,[@]with_appstream_data_packages[@],$(with_appstream_data_packages),g' \
-e 's,[@]with_nfs_client_package[@],$(with_nfs_client_package),g' \
-e 's,[@]with_vdo_package[@],$(with_vdo_package),g' \
-e 's,[@]with_insights_client_package[@],$(with_insights_client_package),g' \
$< > $@.tmp && $(MV) $@.tmp $@ \
$(NULL)

View file

@ -398,6 +398,13 @@ AC_ARG_WITH(vdo-package,
[with_vdo_package='false'])
AC_SUBST(with_vdo_package)
AC_ARG_WITH(insights-client-package,
AC_HELP_STRING([--with-insights-client-package=JSON],
[Package that contains the insights-client, or 'false']),
[],
[with_insights_client_package='false'])
AC_SUBST(with_insights_client_package)
# Address sanitizer
AC_MSG_CHECKING([for asan flags])

View file

@ -29,6 +29,7 @@ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
<script type="text/javascript" src="../base1/cockpit.js"></script>
<script type="text/javascript" src="../*/po.js"></script>
<script type="text/javascript" src="../manifests.js"></script>
<script type="text/javascript" src="subscriptions.js"></script>
</head>

View file

@ -23,6 +23,7 @@ import moment from "moment";
import * as python from "python.js";
import { show_modal_dialog } from "cockpit-components-dialog.jsx";
import * as service from "service.js";
import * as PK from "packagekit.js";
import { client } from "./subscriptions-client";
@ -39,37 +40,52 @@ export function detect() {
return cockpit.spawn([ "which", "insights-client" ], { err: "ignore" }).then(() => true, () => false);
}
function catch_error(err) {
let msg = err.toString();
// The insights-client frequently dumps
// Python backtraces on us. Make them more
// readable by wrapping the text in <pre>.
if (msg.indexOf("\n") > 0)
msg = <pre>{msg}</pre>;
client.setError("error", msg);
function ensure_installed(update_progress) {
return detect().then(installed => {
if (!installed)
return PK.check_missing_packages([ client.insightsPackage ], update_checking_progress(update_progress))
.then(data => {
if (data.unavailable_names.length > 0)
return Promise.reject(cockpit.format(_("The $0 package is not available from any repository."),
data.unavailable_names[0]));
if (data.remove_names.length > 0)
return Promise.reject(cockpit.format(_("The system could not be connected to Insights because installing the $0 package requires the unexpected removal of other packages."),
client.insightsPackage));
return PK.install_missing_packages(data, update_install_progress(update_progress));
});
else
return Promise.resolve();
});
}
export function register() {
return cockpit.spawn([ "insights-client", "--register" ], { superuser: true, err: "message" })
.catch(catch_error);
export function register(update_progress) {
return ensure_installed(update_progress).then(() => {
const proc = cockpit.spawn([ "insights-client", "--register" ], { superuser: true, err: "message" });
if (update_progress)
update_progress(_("Connecting to Insights"), () => { proc.close() });
return proc;
});
}
export function unregister() {
if (insights_timer.enabled) {
return cockpit.spawn([ "insights-client", "--unregister" ], { superuser: true, err: "message" })
.catch(catch_error);
return cockpit.spawn([ "insights-client", "--unregister" ], { superuser: true, err: "message" });
} else {
return cockpit.resolve();
}
}
// TODO - generalize this to arbitrary number of arguments (when needed)
export function fmt_to_fragments(fmt, arg) {
var index = fmt.indexOf("$0");
if (index >= 0)
return <>{fmt.slice(0, index)}{arg}{fmt.slice(index + 2)}</>;
else
return fmt;
export function fmt_to_fragments(fmt) {
const args = Array.prototype.slice.call(arguments, 1);
function replace(part) {
if (part[0] == "$") {
return args[parseInt(part.slice(1))];
} else
return part;
}
return React.createElement.apply(null, [ React.Fragment, { } ].concat(fmt.split(/(\$[0-9]+)/g).map(replace)));
}
function left(func) {
@ -87,29 +103,163 @@ export const blurb =
export const link =
<a href="https://www.redhat.com/en/technologies/management/insights" target="_blank" rel="noopener">Red Hat Insights <i className="fa fa-external-link" /></a>;
function install_data_summary(data) {
if (!data || data.missing_names.length == 0)
return null;
let summary;
if (data.extra_names.length == 0)
summary = fmt_to_fragments(_("The $0 package will be installed."), <strong>{data.missing_names[0]}</strong>);
else
summary = fmt_to_fragments(cockpit.ngettext(_("The $0 package and $1 other package will be installed."),
_("The $0 package and $1 other packages will be installed."),
data.extra_names.length), <strong>{data.missing_names[0]}</strong>, data.extra_names.length);
if (data.remove_names.length > 0) {
summary = <>
{summary}
<br />
<span className="pficon pficon-warning-triangle-o" />{ "\n" }
{cockpit.format(cockpit.ngettext(_("$0 package needs to be removed."),
_("$0 packages need to be removed."),
data.remove_names.length),
data.remove_names.length)}
</>;
}
if (data.extra_names.length > 0 || data.remove_names.length > 0) {
let extra_details = null;
let remove_details = null;
if (data.extra_names.length > 0)
extra_details = (
<div className="scale-up-ct">
{_("Additional packages:")}
<ul className="package-list-ct">{data.extra_names.map(id => <li key={id}>{id}</li>)}</ul>
</div>
);
if (data.remove_names.length > 0)
remove_details = (
<div className="scale-up-ct">
{_("Removals:")}
<ul className="package-list">{data.remove_names.map(id => <li key={id}>{id}</li>)}</ul>
</div>
);
summary = <><p>{summary}</p><Revealer summary={_("Details")}>{extra_details}{remove_details}</Revealer></>;
}
return summary;
}
function update_checking_progress(update_progress) {
return p => {
let pm = null;
if (p.waiting)
pm = _("Waiting for other software management operations to finish");
else
pm = _("Checking installed software");
update_progress(pm, p.cancel);
};
}
function update_install_progress(update_progress) {
return p => {
var text = null;
if (p.waiting) {
text = _("Waiting for other software management operations to finish");
} else if (p.package) {
var fmt;
if (p.info == PK.Enum.INFO_DOWNLOADING)
fmt = _("Downloading $0");
else if (p.info == PK.Enum.INFO_REMOVING)
fmt = _("Removing $0");
else
fmt = _("Installing $0");
text = fmt_to_fragments(fmt, <strong>{p.package}</strong>);
}
update_progress(text, p.cancel);
};
}
function show_connect_dialog() {
show_modal_dialog(
{
let dialog = null;
let cancel = null;
let progress_message = null;
let error_message = null;
let checking_install = false;
let install_data = null;
function update() {
const props = {
title: _("Connect to Red Hat Insights"),
body: (
<div className="modal-body">
<strong>{fmt_to_fragments(_("This system is not connected to $0."), link)}</strong>
<p>{blurb}</p>
{ install_data_summary(install_data) }
</div>
)
},
{
};
const footer = {
actions: [
{
caption: _("Connect"),
style: "primary",
clicked: () => {
return register().catch(catch_error);
}
clicked: (update_progress) => {
return PK.install_missing_packages(install_data, update_install_progress(update_progress)).then(() => register(update_progress));
},
disabled: checking_install,
}
]
],
idle_message: progress_message && <div><div className="spinner spinner-sm" /><span>{ progress_message }</span></div>,
static_error: error_message,
dialog_done: f => { if (!f && cancel) cancel(); }
};
if (dialog) {
dialog.setProps(props);
dialog.setFooterProps(footer);
} else {
dialog = show_modal_dialog(props, footer);
}
);
}
update();
return detect().then(installed => {
if (!installed) {
checking_install = true;
update();
PK.check_missing_packages([ client.insightsPackage ], p => {
cancel = p.cancel;
let pm = null;
if (p.waiting)
pm = _("Waiting for other software management operations to finish");
else
pm = _("Checking installed software");
if (pm != progress_message) {
progress_message = pm;
update();
}
})
.then(data => {
if (data.unavailable_names.length > 0)
error_message = cockpit.format(_("The $0 package is not available from any repository."),
data.unavailable_names[0]);
else
install_data = data;
progress_message = null;
cancel = null;
checking_install = false;
update();
})
.catch(e => {
error_message = e.toString();
update();
});
}
});
}
class Revealer extends React.Component {
@ -307,9 +457,6 @@ export class InsightsStatus extends React.Component {
render() {
let status;
if (!insights_timer.exists || !insights_service.exists)
return null;
if (insights_timer.enabled) {
let warn = (insights_service.state == "failed" &&
insights_service.unit.ActiveExitTimestamp &&

View file

@ -35,12 +35,22 @@ function dismissStatusError() {
dataStore.render();
}
function catchError(err) {
let msg = err.toString();
// The insights-client frequently dumps
// Python backtraces on us. Make them more
// readable by wrapping the text in <pre>.
if (msg.indexOf("\n") > 0)
msg = <pre>{msg}</pre>;
client.setError("error", msg);
}
var registerDialogDetails;
function registerSystem () {
return client.registerSystem(registerDialogDetails).then(() => {
function registerSystem (update_progress) {
return client.registerSystem(registerDialogDetails, update_progress).then(() => {
if (registerDialogDetails.insights)
return Insights.register();
return Insights.register(update_progress).catch(catchError);
});
}
@ -56,34 +66,38 @@ var footerProps = {
function openRegisterDialog() {
registerDialogDetails = subscriptionsRegister.defaultSettings();
// show dialog to register
var renderDialog;
var updatedData = function(prop, data) {
if (prop) {
if (data.target) {
if (data.target.type == "checkbox") {
registerDialogDetails[prop] = data.target.checked;
Insights.detect().then(installed => {
registerDialogDetails.insights_detected = installed;
// show dialog to register
var renderDialog;
var updatedData = function(prop, data) {
if (prop) {
if (data.target) {
if (data.target.type == "checkbox") {
registerDialogDetails[prop] = data.target.checked;
} else {
registerDialogDetails[prop] = data.target.value;
}
} else {
registerDialogDetails[prop] = data.target.value;
registerDialogDetails[prop] = data;
}
} else {
registerDialogDetails[prop] = data;
}
}
registerDialogDetails.onChange = updatedData;
registerDialogDetails.onChange = updatedData;
var dialogProps = {
'title': _("Register system"),
'body': React.createElement(subscriptionsRegister.DialogBody, registerDialogDetails),
var dialogProps = {
'title': _("Register system"),
'body': React.createElement(subscriptionsRegister.DialogBody, registerDialogDetails),
};
if (renderDialog)
renderDialog.setProps(dialogProps);
else
renderDialog = show_modal_dialog(dialogProps, footerProps);
};
if (renderDialog)
renderDialog.setProps(dialogProps);
else
renderDialog = show_modal_dialog(dialogProps, footerProps);
};
updatedData();
updatedData();
});
}
function unregisterSystem() {

View file

@ -9,5 +9,10 @@
"label": "Subscriptions"
}
},
"config": {
"insights_client_package": @with_insights_client_package@
},
"content-security-policy": "img-src 'self' data:"
}

View file

@ -18,6 +18,8 @@
*/
import cockpit from "cockpit";
import * as PK from "packagekit";
const _ = cockpit.gettext;
export var client = { };
@ -120,7 +122,7 @@ function getSubscriptionDetails() {
});
}
client.registerSystem = function(subscriptionDetails) {
client.registerSystem = function(subscriptionDetails, update_progress) {
var dfd = cockpit.defer();
var args = ['subscription-manager', 'register'];
@ -169,7 +171,6 @@ client.registerSystem = function(subscriptionDetails) {
err: "out"
});
var promise;
var buffer = '';
process
.input('')
@ -215,14 +216,15 @@ client.registerSystem = function(subscriptionDetails) {
dfd.reject(error);
});
promise = dfd.promise();
promise.cancel = function cancel() {
process.close("cancelled");
// we have no idea what the current state is
requestUpdate();
};
if (update_progress) {
update_progress(_("Registering"), function cancel() {
process.close("cancelled");
// we have no idea what the current state is
requestUpdate();
});
}
return promise;
return dfd.promise();
};
client.unregisterSystem = function() {
@ -383,12 +385,20 @@ client.setError = (_, message) => {
needRender();
};
client.insightsPackage = (cockpit.manifests.subscriptions &&
cockpit.manifests.subscriptions.config &&
cockpit.manifests.subscriptions.config.insights_client_package);
client.insightsAvailable = false;
const detectInsights = () => {
return cockpit.spawn([ "which", "insights-client" ], { err: "ignore" }).then(
() => { client.insightsAvailable = true },
() => { client.insightsAvailable = false });
() => {
PK.detect().then(pk_available => {
client.insightsAvailable = pk_available && client.insightsPackage;
});
});
};
client.init = function() {

View file

@ -40,7 +40,8 @@ export function defaultSettings() {
activationKeys: '',
org: '',
insights: false,
insights_available: client.insightsAvailable
insights_available: client.insightsAvailable,
insights_detected: false
};
}
/* Subscriptions: registration dialog body
@ -120,6 +121,7 @@ export class DialogBody extends React.Component {
{ Insights.fmt_to_fragments(_("Connect this system to $0."), Insights.link) }
</span>
</label>
{ (this.props.insights && !this.props.insights_detected) && <p>{ Insights.fmt_to_fragments(_("The $0 package will be installed."), <strong>{client.insightsPackage}</strong>)}</p> }
</td>
</tr>;
}

View file

@ -545,10 +545,13 @@ PageServer.prototype = {
refresh_os_updates_state();
});
var insights_client_timer = service.proxy("insights-client.timer");
var insights_client_package = (cockpit.manifests.subscriptions &&
cockpit.manifests.subscriptions.config &&
cockpit.manifests.subscriptions.config.insights_client_package);
var insights_client_timer = service.proxy("insights-client.timer", "Timer");
function refresh_insights_status() {
if (insights_client_timer.exists && !insights_client_timer.enabled) {
if ((insights_client_timer.exists || insights_client_package) && !insights_client_timer.enabled) {
$("#insights_icon").attr("class", "pficon pficon-warning-triangle-o");
set_page_link("#insights_text", "subscriptions",
_("Not connected to Insights"));

View file

@ -21,6 +21,7 @@
import os
import parent
from packagelib import *
from testlib import *
@ -79,18 +80,14 @@ print [ e['id'] for e in data if e['owner']['key'] == 'admin' and e['contractNum
"
"""
@skipImage("No subscriptions", "centos-7", "continuous-atomic", "debian-stable", "debian-testing", "fedora-29",
"fedora-30", "fedora-i386", "fedora-atomic", "fedora-testing", "ubuntu-1804", "ubuntu-stable")
@skipImage("cockpit-subscriptions obsoleted by external subscription-manager-cockpit", "rhel-8-0", "rhel-8-0-distropkg", "rhel-8-1")
class TestSubscriptions(MachineCase):
class SubscriptionsCase(MachineCase):
provision = {
"0": {"address": "10.111.113.1/20"},
"candlepin": {"image": "candlepin", "address": "10.111.113.5/20"}
}
def setUp(self):
MachineCase.setUp(self)
super(SubscriptionsCase, self).setUp()
self.candlepin = self.machines['candlepin']
# wait for candlepin to be active and verify
@ -134,6 +131,11 @@ insecure_connection=True
m.upload([ "verify/files/mock-insights" ], "/var/tmp")
m.spawn("/var/tmp/mock-insights", "mock-insights")
@skipImage("No subscriptions", "centos-7", "continuous-atomic", "debian-stable", "debian-testing", "fedora-29",
"fedora-30", "fedora-i386", "fedora-atomic", "fedora-testing", "ubuntu-1804", "ubuntu-stable")
@skipImage("cockpit-subscriptions obsoleted by external subscription-manager-cockpit", "rhel-8-0", "rhel-8-0-distropkg", "rhel-8-1")
class TestSubscriptions(SubscriptionsCase):
def testRegister(self):
b = self.browser
@ -370,5 +372,55 @@ insecure_connection=True
b.wait_not_present("label:contains('Insights')")
m.execute("test -f /etc/insights-client/.unregistered")
@skipImage("No subscriptions", "centos-7", "continuous-atomic", "debian-stable", "debian-testing", "fedora-29",
"fedora-30", "fedora-i386", "fedora-atomic", "fedora-testing", "ubuntu-1804", "ubuntu-stable")
@skipImage("cockpit-subscriptions obsoleted by external subscription-manager-cockpit", "rhel-8-0", "rhel-8-0-distropkg", "rhel-8-1")
class TestSubscriptionsPackages(SubscriptionsCase, PackageCase):
@skipImage("insights-client not installable", "rhel-atomic")
def testMissingPackages(self):
m = self.machine
b = self.browser
m.execute("pkcon remove -y insights-client")
self.createPackage("insights-client", "999", "1")
self.enableRepo()
m.execute("pkcon refresh")
self.login_and_go("/subscriptions")
b.click("label:contains('Status') + button:contains('Register')")
b.wait_visible("#subscription-register-url")
b.set_val("#subscription-register-url", "custom")
b.set_input_text("#subscription-register-url-custom", "10.111.113.5:8443/candlepin")
b.set_input_text("#subscription-register-username", "admin")
b.set_input_text("#subscription-register-password", "admin")
b.set_input_text("#subscription-register-org", "admin")
b.set_checked("#subscription-insights", True)
b.wait_present('.modal-body:contains("The insights-client package will be installed")')
b.click('.modal-footer button.apply')
with b.wait_timeout(360):
b.wait_not_present(".modal-dialog")
# This will not have worked because the insights-client binary
# is not actually there. Try again with the connection dialog.
m.execute("test -f /stamp-insights-client-999-1")
m.execute("pkcon remove -y insights-client")
m.execute("pkcon refresh")
b.click("label:contains('Insights') a:contains('Not connected')")
b.wait_present('.modal-body:contains("This system is not connected")')
b.wait_present('.modal-body:contains("The insights-client package will be installed")')
b.click('.modal-footer button.apply')
with b.wait_timeout(360):
b.wait_present('.modal-footer:contains("not-found")')
b.click('.modal-footer button.cancel')
m.execute("test -f /stamp-insights-client-999-1")
if __name__ == '__main__':
test_main()

View file

@ -76,6 +76,7 @@
%if 0%{?rhel} >= 7
%define vdo_on_demand 1
%define insights_client_on_demand 1
%endif
Name: cockpit
@ -169,7 +170,8 @@ exec 2>&1
%endif
--with-appstream-data-packages='[ "appstream-data" ]' \
--with-nfs-client-package='"nfs-utils"' \
%{?vdo_on_demand:--with-vdo-package='"vdo"'}
%{?vdo_on_demand:--with-vdo-package='"vdo"'} \
%{?insights_client_on_demand:--with-insights-client-package='"insights-client"'}
make -j4 %{?extra_flags} all
%check