diff --git a/data/gtkbuilder/dialog-auth.ui b/data/gtkbuilder/dialog-auth.ui
index b630fff..ab664ce 100644
--- a/data/gtkbuilder/dialog-auth.ui
+++ b/data/gtkbuilder/dialog-auth.ui
@@ -1,114 +1,130 @@
-
+
+
+
diff --git a/data/gtkbuilder/dialog-livepatch-error.ui b/data/gtkbuilder/dialog-livepatch-error.ui
index 9d64306..6ee1f44 100644
--- a/data/gtkbuilder/dialog-livepatch-error.ui
+++ b/data/gtkbuilder/dialog-livepatch-error.ui
@@ -1,58 +1,139 @@
-
+
-
- False
+
+
+ Livepatch
+ False
+ True
dialog
- error
- Sorry, there’s been a problem in setting up Canonical Livepatch.
+ True
+ False
+ True
+ True
-
- False
+
vertical
2
-
- False
+
end
Settings…
- True
True
- True
+ False
True
True
- 0
Ignore
True
+ True
True
True
- 0.51999998092651367
True
True
- 1
False
False
- 0
+
+
+
+
+ True
+ 12
+ vertical
+
+
+ True
+ 12
+ 12
+
+
+ True
+ 0
+
+
+ 1
+ 0
+
+
+
+
+ True
+ center
+ start
+ gtk-dialog-error
+ True
+ 6
+
+
+ 0
+ 0
+ 3
+
+
+
+
+ True
+ The error was:
+ 0
+
+
+ 1
+ 1
+
+
+
+
+ 100
+ True
+ True
+ True
+ 6
+ 6
+ False
+ word
+ 6
+ 6
+ False
+ textbuffer_message
+ False
+
+
+ 1
+ 2
+
+
+
+
+ True
+ True
+
+
+
+
+ True
+ True
+
+
+
diff --git a/data/gtkbuilder/main.ui b/data/gtkbuilder/main.ui
index 146a9ed..557e844 100644
--- a/data/gtkbuilder/main.ui
+++ b/data/gtkbuilder/main.ui
@@ -1,7 +1,13 @@
-
+
-
+
+
+
+
+
+
+
@@ -91,6 +97,7 @@
To install from a CD-ROM or DVD, insert the medium into the drive.
+
False
6
@@ -696,84 +703,6 @@
-
- True
- False
- 12
-
-
- True
- False
- center
- False
- False
- 6
- 6
-
-
- True
- False
- center
- 6
-
-
- True
- False
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- True
- 0
-
-
- False
- True
- 2
-
-
-
-
- 1
- 1
-
-
-
-
- Use Canonical Livepatch to increase security between restarts
- False
- True
- True
- False
- True
- 0
- True
-
-
- 1
- 0
-
-
-
-
-
-
-
-
-
- True
- True
- 2
-
-
-
True
False
@@ -820,7 +749,7 @@
False
False
- 3
+ 2
@@ -1187,6 +1116,214 @@
False
+
+
+ True
+ False
+ 12
+ vertical
+ 12
+
+
+ True
+ False
+ Canonical Livepatch helps keep your system secure by applying security updates that don't require a restart. <a href="https://www.ubuntu.com/livepatch">Learn More</a>
+ True
+ True
+ 1
+ 0
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ 6
+
+
+ True
+ False
+ True
+
+
+ False
+ True
+ 0
+
+
+
+
+ False
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+
+
+ False
+ True
+ 2
+
+
+
+
+ True
+ True
+
+
+ False
+ True
+ end
+ 3
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ crossfade
+ True
+
+
+ True
+ True
+ in
+
+
+ True
+ True
+ 6
+ False
+ word
+ 6
+ 6
+ False
+ textbuffer_livepatch
+ False
+
+
+
+
+ page_livepatch_message
+
+
+
+
+ True
+ False
+ vertical
+ 12
+
+
+ True
+ False
+ 0
+
+
+ False
+ True
+ 0
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ True
+ in
+
+
+ True
+ True
+ model_livepatch_fixes
+ False
+ False
+ False
+
+
+
+
+
+ column
+
+
+ 100
+ word
+ 100
+
+
+ 0
+
+
+
+
+
+
+
+
+ True
+ True
+ 2
+
+
+
+
+ page_livepatch_status
+ 1
+
+
+
+
+ True
+ True
+ 2
+
+
+
+
+ 6
+
+
+
+
+ True
+ False
+ Livepatch
+
+
+ 6
+ False
+
+
True
@@ -1246,6 +1383,9 @@
+
+
+
diff --git a/data/icons/16x16/apps/livepatch.svg b/data/icons/16x16/apps/livepatch.svg
new file mode 100644
index 0000000..6d82412
Binary files /dev/null and b/data/icons/16x16/apps/livepatch.svg differ
diff --git a/data/icons/24x24/apps/livepatch.svg b/data/icons/24x24/apps/livepatch.svg
new file mode 100644
index 0000000..8c13c57
Binary files /dev/null and b/data/icons/24x24/apps/livepatch.svg differ
diff --git a/data/icons/48x48/apps/livepatch.svg b/data/icons/48x48/apps/livepatch.svg
new file mode 100644
index 0000000..65a3049
Binary files /dev/null and b/data/icons/48x48/apps/livepatch.svg differ
diff --git a/data/icons/64x64/apps/livepatch.svg b/data/icons/64x64/apps/livepatch.svg
new file mode 100644
index 0000000..46c3dea
Binary files /dev/null and b/data/icons/64x64/apps/livepatch.svg differ
diff --git a/data/icons/scalable/apps/livepatch.svg b/data/icons/scalable/apps/livepatch.svg
new file mode 100644
index 0000000..8bdc8b5
--- /dev/null
+++ b/data/icons/scalable/apps/livepatch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/data/software-properties-livepatch.desktop.in b/data/software-properties-livepatch.desktop.in
new file mode 100644
index 0000000..c9f168a
--- /dev/null
+++ b/data/software-properties-livepatch.desktop.in
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Keywords=Livepatch;
+Exec=/usr/bin/software-properties-gtk --open-tab=6
+Icon=livepatch
+Terminal=false
+Type=Application
+OnlyShowIn=GNOME;
+Categories=GTK;Settings;HardwareSettings
+X-AppStream-Ignore=true
+Name=Livepatch
+_Comment=Manage Canonical Livepatch
+_Keywords=Security;Update;
diff --git a/debian/changelog b/debian/changelog
index a1449b1..1d7a3e8 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,15 @@
+software-properties (0.96.24.32.8) UNRELEASED; urgency=medium
+
+ * debian/control: Update Vcs-*: URLs
+ * debian/gbp.conf: Add default configuration
+ * Add .keep files to preserve empty directories
+ * Backport Livepatch changes from Disco (LP: #1823761):
+ - Implement new design for authentication dialog.
+ - Add livepatch desktop file and icon.
+ - Move Livepatch UI in a diffrent tab.
+
+ -- Andrea Azzarone Mon, 08 Apr 2019 16:52:15 +0100
+
software-properties (0.96.24.32.7) bionic; urgency=medium
* SoftwarePropertiesGtk.py: when checking a package's depends for DKMS also
diff --git a/debian/control b/debian/control
index fd8d887..cd54cef 100644
--- a/debian/control
+++ b/debian/control
@@ -7,13 +7,17 @@ Build-Depends: debhelper (>= 9),
libxml-parser-perl,
intltool,
dbus-x11 ,
+ gir1.2-gtk-3.0 ,
gir1.2-snapd-1 ,
lsb-release ,
pyflakes3 ,
python3-apt ,
+ python3-dateutil ,
python3-dbus ,
+ python3-distro-info ,
python3-gi ,
python3-mock ,
+ python3-requests-unixsocket ,
xauth ,
xvfb ,
python3-all,
@@ -22,7 +26,7 @@ Build-Depends: debhelper (>= 9),
dh-migrations,
dh-translations
Standards-Version: 3.9.6
-Vcs-Bzr: http://code.launchpad.net/~ubuntu-core-dev/software-properties/main
+Vcs-Git: https://git.launchpad.net/software-properties -b ubuntu/bionic
XS-Testsuite: autopkgtest
Package: python3-software-properties
@@ -58,10 +62,11 @@ Depends: ${python3:Depends}, ${misc:Depends}, python3,
python3-gi,
gir1.2-gtk-3.0,
gir1.2-goa-1.0 (>= 3.27.92-1ubuntu1),
- gir1.2-secret-1,
gir1.2-snapd-1,
python3-aptdaemon.gtk3widgets,
+ python3-dateutil,
python3-distro-info,
+ python3-requests-unixsocket,
software-properties-common,
ubuntu-drivers-common (>= 1:0.2.75),
python3-gi,
diff --git a/debian/gbp.conf b/debian/gbp.conf
new file mode 100644
index 0000000..0bb5f1a
--- /dev/null
+++ b/debian/gbp.conf
@@ -0,0 +1,3 @@
+[DEFAULT]
+debian-branch = ubuntu/bionic
+debian-tag = %(version)s
diff --git a/debian/software-properties-gtk.install b/debian/software-properties-gtk.install
index c71d9d2..cfc4d82 100644
--- a/debian/software-properties-gtk.install
+++ b/debian/software-properties-gtk.install
@@ -5,6 +5,7 @@ debian/tmp/usr/share/mime/packages
debian/tmp/usr/share/icons
debian/tmp/usr/share/applications/software-properties-gtk.desktop
debian/tmp/usr/share/applications/software-properties-drivers.desktop
+debian/tmp/usr/share/applications/software-properties-livepatch.desktop
debian/tmp/usr/share/metainfo/software-properties-gtk.appdata.xml
debian/tmp/usr/share/glib-2.0/schemas
#debian/tmp/usr/share/gnome/help/software-properties
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d1917fd..47830e4 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -5,6 +5,7 @@ data/software-properties-gtk.desktop.in
data/software-properties-gtk.appdata.xml.in
data/software-properties-kde.desktop.in
data/software-properties-drivers.desktop.in
+data/software-properties-livepatch.desktop.in
software-properties-gtk
software-properties-kde
add-apt-repository
@@ -28,8 +29,12 @@ softwareproperties/gtk/dialogs.py
softwareproperties/gtk/DialogEdit.py
softwareproperties/gtk/DialogAdd.py
softwareproperties/gtk/DialogCacheOutdated.py
+softwareproperties/gtk/DialogLivepatchError.py
+softwareproperties/gtk/LivepatchPage.py
softwareproperties/CountryInformation.py
softwareproperties/AptAuth.py
+softwareproperties/LivepatchService.py
+softwareproperties/LivepatchSnap.py
[type: gettext/glade]data/designer/dialog_mirror.ui
[type: gettext/glade]data/designer/dialog_edit.ui
[type: gettext/glade]data/designer/main.ui
@@ -41,3 +46,4 @@ softwareproperties/AptAuth.py
[type: gettext/glade]data/gtkbuilder/dialog-mirror.ui
[type: gettext/glade]data/gtkbuilder/dialog-add.ui
[type: gettext/glade]data/gtkbuilder/dialog-auth.ui
+[type: gettext/glade]data/gtkbuilder/dialog-livepatch-error.ui
diff --git a/setup.cfg b/setup.cfg
index ef8c2a0..9b28335 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,6 +4,7 @@ domain=software-properties
desktop_files=[("share/applications",
("data/software-properties-gtk.desktop.in",
"data/software-properties-drivers.desktop.in",
+ "data/software-properties-livepatch.desktop.in",
"data/software-properties-kde.desktop.in",),
)
]
diff --git a/softwareproperties/GoaAuth.py b/softwareproperties/GoaAuth.py
index 6583a4a..eb45bf7 100644
--- a/softwareproperties/GoaAuth.py
+++ b/softwareproperties/GoaAuth.py
@@ -21,7 +21,9 @@
import gi
gi.require_version('Goa', '1.0')
-from gi.repository import Gio, Goa, GObject
+from gi.repository import Gio, GLib, Goa, GObject
+
+import logging
class GoaAuth(GObject.GObject):
@@ -32,12 +34,21 @@ class GoaAuth(GObject.GObject):
def __init__(self):
GObject.GObject.__init__(self)
- self.goa_client = Goa.Client.new_sync(None)
self.account = None
+ self.cancellable = Gio.Cancellable()
+ Goa.Client.new(self.cancellable, self._on_goa_client_ready)
self.settings = Gio.Settings.new('com.ubuntu.SoftwareProperties')
self.settings.connect('changed::goa-account-id', self._on_settings_changed)
- self._load()
+
+ def _on_goa_client_ready(self, source, res):
+ try:
+ self.goa_client = Goa.Client.new_finish(res)
+ except GLib.Error as e:
+ logging.error('Failed to get a Gnome Online Account: {}'.format(e.message))
+ self.goa_client = None
+ else:
+ self._load()
def login(self, account):
assert(account)
@@ -50,7 +61,7 @@ class GoaAuth(GObject.GObject):
@GObject.Property
def token(self):
- if self.account is None:
+ if self.account is None or self.goa_client is None:
return None
obj = self.goa_client.lookup_by_id(self.account.props.id)
@@ -64,7 +75,7 @@ class GoaAuth(GObject.GObject):
return pbased.call_get_password_sync('livepatch')
def _update_state_from_account_id(self, account_id):
- if account_id:
+ if account_id and self.goa_client is not None:
# Make sure the account-id is valid
obj = self.goa_client.lookup_by_id(account_id)
if obj is None:
diff --git a/softwareproperties/LivepatchService.py b/softwareproperties/LivepatchService.py
new file mode 100644
index 0000000..694e64c
--- /dev/null
+++ b/softwareproperties/LivepatchService.py
@@ -0,0 +1,254 @@
+#
+# Copyright (c) 2019 Canonical
+#
+# Authors:
+# Andrea Azzarone
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+from gettext import gettext as _
+import logging
+
+import gi
+from gi.repository import Gio, GLib, GObject
+
+try:
+ import dateutil.parser
+ import requests_unixsocket
+
+ gi.require_version('Snapd', '1')
+ from gi.repository import Snapd
+except(ImportError, ValueError):
+ pass
+
+from softwareproperties.gtk.utils import (
+ has_gnome_online_accounts,
+ is_current_distro_lts,
+ is_current_distro_supported,
+ retry
+)
+
+from softwareproperties.LivepatchSnap import LivepatchSnap
+
+
+def datetime_parser(json_dict):
+ for (key, value) in json_dict.items():
+ try:
+ json_dict[key] = dateutil.parser.parse(value)
+ except (ValueError, TypeError):
+ pass
+ return json_dict
+
+class LivepatchAvailability:
+ FALSE = 0
+ TRUE = 1
+ NO_CONNECTIVITY=3
+ CHECKING = 2
+
+
+class LivepatchService(GObject.GObject):
+
+ # Constants
+ STATUS_ENDPOINT = 'http+unix://%2Fvar%2Fsnap%2Fcanonical-livepatch%2Fcurrent%2Flivepatchd.sock/status'
+ ENABLE_ENDPOINT = 'http+unix://%2Fvar%2Fsnap%2Fcanonical-livepatch%2Fcurrent%2Flivepatchd-priv.sock/enable'
+ DISABLE_ENDPOINT = 'http+unix://%2Fvar%2Fsnap%2Fcanonical-livepatch%2Fcurrent%2Flivepatchd-priv.sock/disable'
+ LIVEPATCH_RUNNING_FILE = '/var/snap/canonical-livepatch/common/machine-token'
+
+ ENABLE_ERROR_MSG = _('Failed to enable Livepatch: {}')
+ DISABLE_ERROR_MSG = _('Failed to disable Livepatch: {}')
+
+ # GObject.GObject
+ __gproperties__ = {
+ 'availability': (
+ int, None, None,
+ LivepatchAvailability.FALSE,
+ LivepatchAvailability.CHECKING,
+ LivepatchAvailability.FALSE,
+ GObject.PARAM_READABLE),
+ 'availability-message': (
+ str, None, None, None, GObject.PARAM_READABLE),
+ 'enabled': (
+ bool, None, None, False, GObject.PARAM_READABLE),
+ }
+
+ def __init__(self):
+ GObject.GObject.__init__(self)
+
+ self._timeout_id = 0
+
+ self._snap = LivepatchSnap()
+ self._session = requests_unixsocket.Session()
+
+ # Init Properties
+ self._availability = LivepatchAvailability.FALSE
+ self._availability_message = None
+ lp_file = Gio.File.new_for_path(path=self.LIVEPATCH_RUNNING_FILE)
+ self._enabled = lp_file.query_exists()
+
+ # Monitor connectivity status
+ self._nm = Gio.NetworkMonitor.get_default()
+ self._nm.connect('notify::connectivity', self._network_changed_cb)
+
+ # Monitor status of canonical-livepatch
+ self._lp_monitor = lp_file.monitor_file(Gio.FileMonitorFlags.NONE)
+ self._lp_monitor.connect('changed', self._livepatch_enabled_changed_cb)
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'availability':
+ return self._availability
+ elif pspec.name == 'availability-message':
+ return self._availability_message
+ elif pspec.name == 'enabled':
+ return self._enabled
+ else:
+ raise AssertionError
+
+ # Public API
+ def trigger_availability_check(self):
+ """Trigger a Livepatch availability check to be executed after a short
+ timeout. Multiple triggers will result in a single request.
+
+ A notify::availability will be emitted when the check starts, and
+ another one when the check ends.
+ """
+ def _update_availability():
+ # each rule is a tuple of two elements, a callable and a string. The
+ # string rapresents the error message that needs to be shown if the
+ # callable returns false.
+ rules = [
+ (lambda: self._snap.get_status() != Snapd.SnapStatus.UNKNOWN,
+ _('Canonical Livepatch snap is not available.')),
+ (has_gnome_online_accounts,
+ _('Gnome Online Accounts is required to enable Livepatch.')),
+ (is_current_distro_lts,
+ _('Livepatch is not available for this release.')),
+ (is_current_distro_supported,
+ _('The current release is no longer supported.'))]
+
+ if self._nm.props.connectivity != Gio.NetworkConnectivity.FULL:
+ self._availability = LivepatchAvailability.NO_CONNECTIVITY
+ self._availability_message = None
+ else:
+ for func, message in rules:
+ if not func():
+ self._availability = LivepatchAvailability.FALSE
+ self._availability_message = message
+ break
+ else:
+ self._availability = LivepatchAvailability.TRUE
+ self._availability_message = None
+
+ self.notify('availability')
+ self.notify('availability-message')
+
+ self._timeout_id = 0
+ return False
+
+ self._availability = LivepatchAvailability.CHECKING
+ self._availability_message = None
+ self.notify('availability')
+ self.notify('availability-message')
+
+ if self._timeout_id == 0:
+ self._timeout_id = GLib.timeout_add_seconds(3, _update_availability)
+
+ def set_enabled(self, enabled, token):
+ """Enable or disable Canonical Livepatch in the current system. This
+ function will return once the operation succeeded or failed.
+
+ Args:
+ enabled(bool): wheater to enable or disable the service.
+ token(str): the authentication token to be used to enable Canonical
+ Livepatch service.
+
+ Returns:
+ (False, '') if successful, (True, error_message) otherwise.
+ """
+ if self._enabled == enabled:
+ return False, ''
+
+ if not enabled:
+ return self._disable_service()
+ elif self._snap.get_status() == Snapd.SnapStatus.ACTIVE:
+ return self._enable_service(token)
+ else:
+ success, msg = self._snap.enable_or_install()
+ return self._enable_service(token) if success else (True, msg)
+
+ def get_status(self):
+ """Synchronously retrieve the status of Canonical Livepatch.
+
+ Returns:
+ str: The status. A valid string for success, None otherwise.
+ """
+ try:
+ params = {'verbosity': 3, 'format': 'json'}
+ r = self._session.get(self.STATUS_ENDPOINT, params=params)
+ return r.json(object_hook=datetime_parser)
+ except Exception as e:
+ logging.debug('Failed to get Livepatch status: {}'.format(str(e)))
+ return None
+
+ # Private methods
+ def _enable_service(self, token):
+ """Enable Canonical Livepatch in the current system. This function will
+ return once the operation succeeded or failed.
+
+ Args:
+ token(str): the authentication token to be used to enable Canonical
+ Livepatch service.
+
+ Returns:
+ (False, '') if successful, (True, error_message) otherwise.
+ """
+ try:
+ return self._enable_service_with_retry(token)
+ except Exception as e:
+ return True, self.ENABLE_ERROR_MSG.format(str(e))
+
+ @retry(Exception)
+ def _enable_service_with_retry(self, token):
+ params = {'auth-token': token}
+ r = self._session.put(self.ENABLE_ENDPOINT, params=params)
+ return not r.ok, '' if r.ok else self.ENABLE_ERROR_MSG.format(r.text)
+
+ def _disable_service(self):
+ """Disable Canonical Livepatch in the current system. This function will
+ return once the operation succeeded or failed.
+
+ Returns:
+ (False, '') if successful, (True, error_message) otherwise.
+ """
+ try:
+ return self._disable_service_with_retry()
+ except Exception as e:
+ return True, self.DISABLE_ERROR_MSG.format(str(e))
+
+
+ @retry(Exception)
+ def _disable_service_with_retry(self):
+ r = self._session.put(self.DISABLE_ENDPOINT)
+ return not r.ok, '' if r.ok else self.DISABLE_ERROR_MSG.format(r.text)
+
+ # Signals handlers
+ def _network_changed_cb(self, monitor, network_available):
+ self.trigger_availability_check()
+
+ def _livepatch_enabled_changed_cb(self, fm, file, other_file, event_type):
+ enabled = file.query_exists()
+ if self._enabled != enabled:
+ self._enabled = enabled
+ self.notify('enabled')
diff --git a/softwareproperties/LivepatchSnap.py b/softwareproperties/LivepatchSnap.py
new file mode 100644
index 0000000..32b76be
--- /dev/null
+++ b/softwareproperties/LivepatchSnap.py
@@ -0,0 +1,135 @@
+#
+# Copyright (c) 2019 Canonical
+#
+# Authors:
+# Andrea Azzarone
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+from gettext import gettext as _
+import logging
+
+import gi
+from gi.repository import Gio, GLib
+
+try:
+ gi.require_version('Snapd', '1')
+ from gi.repository import Snapd
+except(ImportError, ValueError):
+ pass
+
+
+class LivepatchSnap(object):
+
+ # Constants
+ SNAP_NAME = 'canonical-livepatch'
+
+ # Public API
+ def __init__(self):
+ self._snapd_client = Snapd.Client()
+ self._cancellable = Gio.Cancellable()
+
+ def get_status(self):
+ """ Get the status of canonical-livepatch snap.
+
+ Returns:
+ Snapd.SnapStatus.Enun: An enum indicating the status of the snap.
+ """
+ snap = self._get_raw_snap()
+ return snap.get_status() if snap else Snapd.SnapStatus.UNKNOWN
+
+ def enable_or_install(self):
+ """Enable or install canonical-livepatch snap.
+
+ Returns:
+ (True, '') if successful, (False, error_message) otherwise.
+ """
+ status = self.get_status()
+
+ if status == Snapd.SnapStatus.ACTIVE:
+ logging.warning('{} snap is already active'.format(self.SNAP_NAME))
+ return True, ''
+ elif status == Snapd.SnapStatus.AVAILABLE:
+ return self._install()
+ elif status == Snapd.SnapStatus.INSTALLED:
+ return self._enable()
+ else:
+ logging.warning('{} snap is in an unknown state'.format(self.SNAP_NAME))
+ return False, _('Canonical Livepatch snap cannot be installed.')
+
+ # Private methods
+ def _get_raw_snap(self):
+ """Get the Sanpd.Snap raw object of the canonical-livepatch snapd.
+
+ Returns:
+ Sanpd.Snap if successful, None otherwise.
+ """
+ try:
+ snap = self._snapd_client.get_snap_sync(
+ name=self.SNAP_NAME,
+ cancellable=self._cancellable)
+ except GLib.Error as e:
+ logging.debug('Snapd.Client.get_snap_sync failed: {}'.format(e.message))
+ snap = None
+
+ if snap:
+ return snap
+
+ try:
+ (snaps, ignored) = self._snapd_client.find_sync(
+ flags=Snapd.FindFlags.MATCH_NAME,
+ query=self.SNAP_NAME,
+ cancellable=self._cancellable)
+ snap = snaps[0]
+ except GLib.Error as e:
+ logging.debug('Snapd.Client.find_sync failed: {}'.format(e.message))
+
+ return snap
+
+ def _install(self):
+ """Install canonical-livepatch snap.
+
+ Returns:
+ (True, '') if successful, (False, error_message) otherwise.
+ """
+ assert self.get_status() == Snapd.SnapStatus.AVAILABLE
+
+ try:
+ self._snapd_client.install2_sync(
+ flags=Snapd.InstallFlags.NONE,
+ name=self.SNAP_NAME,
+ cancellable=self._cancellable)
+ except GLib.Error as e:
+ return False, _('Canonical Livepatch snap cannot be installed: {}'.format(e.message))
+ else:
+ return True, ''
+
+ def _enable(self):
+ """Enable the canonical-livepatch snap.
+
+ Returns:
+ (True, '') if successful, (False, error_message) otherwise.
+ """
+ assert self.get_status() == Snapd.SnapStatus.INSTALLED
+
+ try:
+ self._snapd_client.enable_sync(
+ name=self.SNAP_NAME,
+ cancellable=self._cancellable)
+ except GLib.Error as e:
+ return False, _('Canonical Livepatch snap cannot be enabled: {}'.format(e.message))
+ else:
+ return True, ''
diff --git a/softwareproperties/SoftwareProperties.py b/softwareproperties/SoftwareProperties.py
index 20d20e9..46ed68c 100644
--- a/softwareproperties/SoftwareProperties.py
+++ b/softwareproperties/SoftwareProperties.py
@@ -32,7 +32,6 @@ import re
import os
import glob
import shutil
-import subprocess
import threading
import atexit
import tempfile
@@ -65,15 +64,8 @@ from . import shortcuts
from . import ppa
from . import cloudarchive
-import gi
from gi.repository import Gio
-try:
- gi.require_version('Snapd', '1')
- from gi.repository import Snapd
-except (ImportError, ValueError):
- pass
-
_SHORTCUT_FACTORIES = [
ppa.shortcut_handler,
cloudarchive.shortcut_handler,
@@ -100,9 +92,6 @@ class SoftwareProperties(object):
RELEASE_UPGRADES_NEVER : 'never',
}
- # file to monitor canonical-livepatch status
- LIVEPATCH_RUNNING_FILE = '/var/snap/canonical-livepatch/common/machine-token'
-
def __init__(self, datadir=None, options=None, rootdir="/"):
""" Provides the core functionality to configure the used software
repositories, the corresponding authentication keys and
@@ -874,147 +863,6 @@ class SoftwareProperties(object):
except:
return False
- #
- # Livepatch
- #
- def init_snapd(self):
- self.snapd_client = Snapd.Client()
-
- def get_livepatch_snap_async(self, callback):
- assert self.snapd_client
- self.snapd_client.list_one_async('canonical-livepatch',
- self.cancellable,
- self.on_list_one_ready_cb,
- callback)
-
- def on_list_one_ready_cb(self, source_object, result, user_data):
- callback = user_data
- try:
- snap = source_object.list_one_finish(result)
- except:
- snap = None
- if snap:
- if callback:
- callback(snap)
- return
- else:
- assert self.snapd_client
- self.snapd_client.find_async(Snapd.FindFlags.MATCH_NAME,
- 'canonical-livepatch',
- self.cancellable,
- self.on_find_ready_cb,
- callback)
-
- def on_find_ready_cb(self, source_object, result, user_data):
- callback = user_data
- try:
- snaps = source_object.find_finish(result)[0]
- except:
- snaps = list()
- snap = snaps[0] if len(snaps) else None
- if callback:
- callback(snap)
-
- def get_livepatch_snap_status(self, snap):
- if snap is None:
- return Snapd.SnapStatus.UNKNOWN
- return snap.get_status()
-
- # glib-snapd does not keep track of the status of the snap. Use this decorator
- # to make it easy to write async functions that will always have an updated
- # snap object.
- def require_livepatch_snap(func):
- def get_livepatch_snap_and_call(*args, **kwargs):
- return args[0].get_livepatch_snap_async(lambda snap: func(snap=snap, *args, **kwargs))
- return get_livepatch_snap_and_call
-
- def is_livepatch_enabled(self):
- file = Gio.File.new_for_path(path=self.LIVEPATCH_RUNNING_FILE)
- return file.query_exists(None)
-
- @require_livepatch_snap
- def set_livepatch_enabled_async(self, enabled, token, callback, snap=None):
- status = self.get_livepatch_snap_status(snap)
- if status == Snapd.SnapStatus.UNKNOWN:
- if callback:
- callback(True, _("Canonical Livepatch snap cannot be installed."))
- elif status == Snapd.SnapStatus.ACTIVE:
- if enabled:
- error = self.enable_livepatch_service(token)
- else:
- error = self.disable_livepatch_service()
- if callback:
- callback(len(error) > 0, error)
- elif status == Snapd.SnapStatus.INSTALLED:
- if enabled:
- self.snapd_client.enable_async(name='canonical-livepatch',
- cancellable=self.cancellable,
- callback=self.livepatch_enable_snap_cb,
- user_data=(callback, token))
- else:
- if callback:
- callback(False, "")
- elif status == Snapd.SnapStatus.AVAILABLE:
- if enabled:
- self.snapd_client.install_async(name='canonical-livepatch',
- cancellable=self.cancellable,
- callback=self.livepatch_install_snap_cb,
- user_data=(callback, token))
- else:
- if callback:
- callback(False, "")
-
- def livepatch_enable_snap_cb(self, source_object, result, user_data):
- (callback, token) = user_data
- try:
- if source_object.enable_finish(result):
- error = self.enable_livepatch_service(token)
- if callback:
- callback(len(error) > 0, error)
- except Exception:
- if callback:
- callback(True, _("Canonical Livepatch snap cannot be enabled."))
-
- def livepatch_install_snap_cb(self, source_object, result, user_data):
- (callback, token) = user_data
- try:
- if source_object.install_finish(result):
- error = self.enable_livepatch_service(token)
- if callback:
- callback(len(error) > 0, error)
- except Exception:
- if callback:
- callback(True, _("Canonical Livepatch snap cannot be installed."))
-
- def enable_livepatch_service(self, token):
- generic_error = _("Canonical Livepatch cannot be enabled.")
-
- if self.is_livepatch_enabled():
- return ""
-
- try:
- subprocess.check_output(['/snap/bin/canonical-livepatch', 'enable', token], stderr=subprocess.STDOUT)
- return ""
- except subprocess.CalledProcessError as e:
- return e.output if e.output else generic_error
- except:
- return generic_error
-
-
- def disable_livepatch_service(self):
- generic_error = _("Canonical Livepatch cannot be disabled.")
-
- if not self.is_livepatch_enabled():
- return ""
-
- try:
- subprocess.check_output(['/snap/bin/canonical-livepatch', 'disable'], stderr=subprocess.STDOUT)
- return ""
- except subprocess.CalledProcessError as e:
- return e.output if e.output else generic_error
- except:
- return generic_error
-
def shortcut_handler(shortcut):
for factory in _SHORTCUT_FACTORIES:
ret = factory(shortcut)
diff --git a/softwareproperties/dbus/SoftwarePropertiesDBus.py b/softwareproperties/dbus/SoftwarePropertiesDBus.py
index ace8733..2653cd8 100644
--- a/softwareproperties/dbus/SoftwarePropertiesDBus.py
+++ b/softwareproperties/dbus/SoftwarePropertiesDBus.py
@@ -25,10 +25,12 @@ import logging
import subprocess
import tempfile
import sys
+import threading
from aptsources.sourceslist import SourceEntry
from dbus.mainloop.glib import DBusGMainLoop
+from softwareproperties.LivepatchService import LivepatchService
from softwareproperties.SoftwareProperties import SoftwareProperties
DBUS_BUS_NAME = 'com.ubuntu.SoftwareProperties'
@@ -61,7 +63,7 @@ class SoftwarePropertiesDBus(dbus.service.Object, SoftwareProperties):
self.enforce_polkit = True
logging.debug("waiting for connections")
- self.init_snapd()
+ self._livepatch_service = LivepatchService()
# override set_modified_sourceslist to emit a signal
def save_sourceslist(self):
@@ -324,9 +326,13 @@ class SoftwarePropertiesDBus(dbus.service.Object, SoftwareProperties):
sender_keyword="sender", connection_keyword="conn",
in_signature='bs', out_signature='bs', async_callbacks=('reply_handler', 'error_handler'))
def SetLivepatchEnabled(self, enabled, token, reply_handler, error_handler, sender=None, conn=None):
+ def enable_thread_func():
+ ret = self._livepatch_service.set_enabled(enabled, token)
+ GLib.idle_add(lambda: reply_handler(*ret))
+
self._check_policykit_privilege(
sender, conn, "com.ubuntu.softwareproperties.applychanges")
- self.set_livepatch_enabled_async(enabled, token, reply_handler)
+ threading.Thread(target=enable_thread_func).start()
# helper from jockey
def _check_policykit_privilege(self, sender, conn, privilege):
diff --git a/softwareproperties/gtk/DialogAuth.py b/softwareproperties/gtk/DialogAuth.py
index d62e518..421daef 100644
--- a/softwareproperties/gtk/DialogAuth.py
+++ b/softwareproperties/gtk/DialogAuth.py
@@ -19,6 +19,7 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
+from enum import IntEnum
import os
from gettext import gettext as _
@@ -28,9 +29,13 @@ from softwareproperties.gtk.utils import (
import gi
gi.require_version('Goa', '1.0')
-from gi.repository import Gio, GLib, Goa, GObject, Gtk
+from gi.repository import Gio, GLib, Goa, Gtk
import logging
+class Column(IntEnum):
+ ID = 0
+ MAIL = 1
+ ACCOUNT = 2
class DialogAuth:
@@ -38,68 +43,128 @@ class DialogAuth:
"""setup up the gtk dialog"""
self.parent = parent
- setup_ui(self, os.path.join(datadir, "gtkbuilder",
- "dialog-auth.ui"), domain="software-properties")
- self.label_title.set_max_width_chars(50)
+ setup_ui(self, os.path.join(datadir, "gtkbuilder", "dialog-auth.ui"),
+ domain="software-properties")
self.dialog = self.dialog_auth
- self.dialog.use_header_bar = True
+ self.dialog.set_title('')
+ self.dialog.set_deletable(False)
self.dialog.set_transient_for(parent)
- self.listboxrow_new_account.account = None
+ self.button_continue.grab_focus()
self.account = None
self.dispose_on_new_account = False
self.goa_client = Goa.Client.new_sync(None)
- self.listbox_accounts.connect('row-activated', self._listbox_accounts_row_activated_cb)
+ self._setup_model()
+ self._check_ui(select=False)
# Be ready to other accounts
self.goa_client.connect('account-added', self._account_added_cb)
self.goa_client.connect('account-removed', self._account_removed_cb)
- self._setup_listbox_accounts()
- self._check_ui()
-
def run(self):
res = self.dialog.run()
self.dialog.hide()
return res
- def _check_ui(self):
- rows = self.listbox_accounts.get_children()
- has_accounts = len(rows) > 1
-
- if has_accounts:
- title = _('To continue choose an Ubuntu Single Sign-On account.')
- new_account = _('Use another account…')
+ def _setup_model(self):
+ for obj in self.goa_client.get_accounts():
+ self._add_account(obj.get_account(), select=False)
+
+ def _set_header(self, label):
+ self.label_header.set_markup(
+ "%s" % label)
+
+ def _check_ui(self, select):
+ naccounts = len(self.liststore_account)
+
+ if naccounts == 0:
+ self._set_header(
+ _('To use Livepatch, you need to use an Ubuntu One account.'))
+ self.combobox_account.set_visible(False)
+ self.label_account.set_visible(False)
+ self.button_add_another.set_visible(False)
+ self.button_continue.set_label(_('Sign In / Register…'))
+ elif naccounts == 1:
+ self._set_header(
+ _('To use Livepatch, you need to use your Ubuntu One account.'))
+ self.combobox_account.set_visible(False)
+ self.label_account.set_visible(True)
+ self.label_account.set_text(self.liststore_account[0][Column.MAIL])
+ self.button_add_another.set_visible(True)
+ self.button_continue.set_label(_('Continue'))
else:
- title = _('To continue you need an Ubuntu Single Sign-On account.')
- new_account = _('Sign In…')
+ self._set_header(
+ _('To use Livepatch, you need to use an Ubuntu One account.'))
+ self.button_add_another.set_visible(True)
+ self.combobox_account.set_visible(True)
+ self.label_account.set_visible(False)
+ self.button_continue.set_label(_('Use'))
+ if select:
+ self.combobox_account.set_active(naccounts-1)
+ elif self.combobox_account.get_active() == -1:
+ self.combobox_account.set_active(0)
+
+ def _ignore_account(self, account):
+ return account.props.provider_type != 'ubuntusso'
+
+ def _get_account_iter(self, account):
+ row = self.liststore_account.get_iter_first()
+ while row is not None:
+ account_id = self.liststore_account.get_value(row, Column.ID)
+ if account_id == account.props.id:
+ return row
+ row = self.liststore_account.iter_next(row)
+ return None
+
+ def _add_account(self, account, select):
+ if self._ignore_account(account):
+ return
- self.label_title.set_text(title)
- self.label_new_account.set_markup('{}'.format(new_account))
+ account_iter = self._get_account_iter(account)
+ if account_iter is not None:
+ return
- def _setup_listbox_accounts(self):
- for obj in self.goa_client.get_accounts():
- account = obj.get_account()
- if self._is_account_supported(account):
- self._add_account(account)
+ account_iter = self.liststore_account.append()
+ self.liststore_account.set(account_iter,
+ [Column.ID, Column.MAIL, Column.ACCOUNT],
+ [account.props.id, account.props.presentation_identity, account])
+ self._check_ui(select)
- def _is_account_supported(self, account):
- return account.props.provider_type == 'ubuntusso'
+ def _remove_account(self, account):
+ if self._ignore_account(account):
+ return
+
+ account_iter = self._get_account_iter(account)
+ if account_iter is None:
+ return
- def _add_account(self, account):
- row = self._create_row(account)
- self.listbox_accounts.prepend(row)
- self._check_ui()
+ self.liststore_account.remove(account_iter)
+ self._check_ui(select=False)
- def _remove_account(self, account):
- for row in self.listbox_accounts.get_children():
- if row.account == account:
- row.destroy()
- self._check_ui()
- break
+ def _response_if_valid(self, account):
+ def cb(source, res, data):
+ try:
+ source.call_ensure_credentials_finish(res)
+ valid = True
+ except GLib.Error as e:
+ logging.warning("call_ensure_credentials_finish exception: %s",
+ e.message)
+ valid = False
+
+ if not valid:
+ try:
+ self._spawn_goa_with_args(account.props.id, None)
+ except GLib.Error as e:
+ logging.warning ('Failed to spawn gnome-control-center: %s',
+ e.message)
+ else:
+ self.account = account
+ self.dialog.response(Gtk.ResponseType.OK)
+
+ account.call_ensure_credentials(None, cb, None)
def _build_dbus_params(self, action, arg):
builder = GLib.VariantBuilder.new(GLib.VariantType.new('av'))
@@ -118,7 +183,8 @@ class DialogAuth:
v = GLib.Variant.new_variant(s)
builder.add_value(v)
- array = GLib.Variant.new_tuple(GLib.Variant.new_string('online-accounts'), builder.end())
+ array = GLib.Variant.new_tuple(
+ GLib.Variant.new_string('online-accounts'), builder.end())
array = GLib.Variant.new_variant(array)
param = GLib.Variant.new_tuple(
@@ -135,94 +201,43 @@ class DialogAuth:
'org.gtk.Actions', None)
param = self._build_dbus_params(action, arg)
- timeout = 10*60*1000 # 10 minutes should be enough to create an account
- proxy.call_sync('Activate', param, Gio.DBusCallFlags.NONE, timeout, None)
-
- def _create_row(self, account):
- identity = account.props.presentation_identity
- provider_name = account.props.provider_name
-
- row = Gtk.ListBoxRow.new()
- row.show()
-
- hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 6)
- hbox.set_hexpand(True)
- hbox.show()
-
- image = Gtk.Image.new_from_icon_name('avatar-default', Gtk.IconSize.DIALOG)
- image.set_pixel_size(48)
- image.show()
- hbox.pack_start(image, False, False, 0)
-
- vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 2)
- vbox.set_valign(Gtk.Align.CENTER)
- vbox.show()
- hbox.pack_start(vbox, False, False, 0)
-
- if identity:
- ilabel = Gtk.Label.new()
- ilabel.set_halign(Gtk.Align.START)
- ilabel.set_markup('{}'.format(identity))
- ilabel.show()
- vbox.pack_start(ilabel, True, True, 0)
-
- if provider_name:
- plabel = Gtk.Label.new()
- plabel.set_halign(Gtk.Align.START)
- plabel.set_markup('{}'.format(provider_name))
- plabel.show()
- vbox.pack_start(plabel, True, True, 0)
-
- warning_icon = Gtk.Image.new_from_icon_name('dialog-warning-symbolic', Gtk.IconSize.BUTTON)
- warning_icon.set_no_show_all(True)
- warning_icon.set_margin_end (15)
- hbox.pack_end(warning_icon, False, False, 0)
-
- row.add(hbox)
-
- account.bind_property('attention-needed', warning_icon, 'visible',
- GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE)
-
- row.account = account
- return row
-
- # Signals handlers
- def _listbox_accounts_row_activated_cb(self, listbox, row):
- account = row.account
-
- if account is None:
- # TODO (azzar1): there is no easy way to put this to false
- # if the user close the windows without adding an account.
- # We need to discuss with goa's upstream to support such usercases
- try:
- self._spawn_goa_with_args('add', 'ubuntusso')
- self.dispose_on_new_account = True
- except GLib.Error as e:
- logging.warning ('Failed to spawing gnome-control-center: %s', e.message)
- else:
- if account.props.attention_needed:
- try:
- self._spawn_goa_with_args(account.props.id, None)
- except GLib.Error as e:
- logging.warning ('Failed to spawing gnome-control-center: %s', e.message)
- else:
- self.account = account
- self.dialog.response(Gtk.ResponseType.OK)
+ proxy.call_sync('Activate', param, Gio.DBusCallFlags.NONE, -1, None)
def _account_added_cb(self, goa_client, goa_object):
account = goa_object.get_account()
- if not self._is_account_supported(account):
+ if self._ignore_account(account):
return
if not self.dispose_on_new_account:
- self._add_account(account)
+ self._add_account(account, True)
else:
- self.account = account
- self.dialog.response(Gtk.ResponseType.OK)
+ self._response_if_valid(account)
def _account_removed_cb(self, goa_client, goa_object):
account = goa_object.get_account()
- if self._is_account_supported(account):
+ if not self._ignore_account(account):
self._remove_account(account)
+ def _button_add_another_clicked_cb(self, button):
+ try:
+ # There is no easy way to put this to false if the user close the
+ # windows without adding an account.
+ self._spawn_goa_with_args('add', 'ubuntusso')
+ self.dispose_on_new_account = True
+ except GLib.Error as e:
+ logging.warning ('Failed to spawn control-center: %s', e.message)
+
+ def _button_cancel_clicked_cb(self, button):
+ self.dialog.response(Gtk.ResponseType.CANCEL)
+
+ def _button_continue_clicked_cb(self, button):
+ naccounts = len(self.liststore_account)
+ account = None
+ if naccounts >= 1:
+ active_index = self.combobox_account.get_active()
+ account = self.liststore_account[active_index][Column.ACCOUNT]
+ if account is None:
+ self._button_add_another_clicked_cb(self.button_add_another)
+ else:
+ self._response_if_valid(account)
diff --git a/softwareproperties/gtk/DialogLivepatchError.py b/softwareproperties/gtk/DialogLivepatchError.py
index 2d6688c..e27bb69 100644
--- a/softwareproperties/gtk/DialogLivepatchError.py
+++ b/softwareproperties/gtk/DialogLivepatchError.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2017-2018 Canonical
+# Copyright (c) 2017-2019 Canonical
#
# Authors:
# Andrea Azzarone
@@ -21,6 +21,8 @@
import os
+from gettext import gettext as _
+
from softwareproperties.gtk.utils import (
setup_ui,
)
@@ -31,20 +33,27 @@ class DialogLivepatchError:
RESPONSE_SETTINGS = 100
RESPONSE_IGNORE = 101
+ primary = _("Sorry, there's been a problem with setting up Canonical Livepatch.")
+
def __init__(self, parent, datadir):
"""setup up the gtk dialog"""
self.parent = parent
- setup_ui(self, os.path.join(datadir, "gtkbuilder",
- "dialog-livepatch-error.ui"), domain="software-properties")
+ setup_ui(
+ self,
+ os.path.join(datadir, "gtkbuilder", "dialog-livepatch-error.ui"),
+ domain="software-properties")
self.dialog = self.messagedialog_livepatch
- self.dialog.use_header_bar = True
self.dialog.set_transient_for(parent)
def run(self, error, show_settings_button):
- self.dialog.format_secondary_markup(
- "The error was: \"%s\"" % error.strip())
+ p = "{}".format(self.primary)
+ self.label_primary.set_markup(p)
+
+ textbuffer = self.treeview_message.get_buffer()
+ textbuffer.set_text(error)
+
self.button_settings.set_visible(show_settings_button)
res = self.dialog.run()
self.dialog.hide()
diff --git a/softwareproperties/gtk/LivepatchPage.py b/softwareproperties/gtk/LivepatchPage.py
new file mode 100644
index 0000000..6c3ace9
--- /dev/null
+++ b/softwareproperties/gtk/LivepatchPage.py
@@ -0,0 +1,375 @@
+#
+# Copyright (c) 2019 Canonical
+#
+# Authors:
+# Andrea Azzarone
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import datetime
+import gettext
+from gettext import gettext as _
+import gi
+gi.require_version("Gtk", "3.0")
+from gi.repository import GLib, GObject, Gtk
+import logging
+
+from softwareproperties.GoaAuth import GoaAuth
+from softwareproperties.LivepatchService import (
+ LivepatchService,
+ LivepatchAvailability)
+from .DialogAuth import DialogAuth
+from .DialogLivepatchError import DialogLivepatchError
+
+
+class LivepatchPage(object):
+
+ # Constants
+ COMMON_ISSUE_URL = 'https://wiki.ubuntu.com/Kernel/Livepatch#CommonIssues'
+ GENERIC_ERR_MSG = _('Canonical Livepatch has experienced an internal error.'
+ ' Please refer to {} for further information.'.format(COMMON_ISSUE_URL))
+
+ def __init__(self, parent):
+ self._parent = parent
+
+ self._timeout_handler = -1
+ self._waiting_livepatch_response = False
+
+ self._lps = LivepatchService()
+ self._auth = GoaAuth()
+
+ # Connect signals
+ self._lps.connect(
+ 'notify::availability', self._lps_availability_changed_cb)
+ self._lps.connect(
+ 'notify::enabled', self._lps_enabled_changed_cb)
+ self._auth.connect(
+ 'notify', self._auth_changed_cb)
+ self._state_set_handler = self._parent.switch_livepatch.connect(
+ 'state-set', self._switch_state_set_cb)
+ self._parent.button_livepatch_login.connect(
+ 'clicked', self._button_livepatch_login_clicked_cb)
+
+ self._lps.trigger_availability_check()
+
+ @property
+ def waiting_livepatch_response(self):
+ return self._waiting_livepatch_response
+
+ # Private methods
+ def _trigger_ui_update(self, skip=False, error_message=None):
+ """Trigger the update of every single user interface component according
+ to the current state.
+
+ Args:
+ skip (bool): whether to trigger the update after a small timeout.
+ Defaults to False.
+ error_message (str): error message to display. Defaults to None.
+ """
+ def do_ui_update():
+ self._timeout_handler = -1
+
+ self._update_switch()
+ self._update_spinner()
+ self._update_switch_label()
+ self._update_auth_button()
+ self._update_stack(error_message)
+
+ return False
+
+ if self._timeout_handler > 0:
+ GObject.source_remove(self._timeout_handler)
+ self._timeout_handler = -1
+
+ if skip:
+ do_ui_update()
+ else:
+ self._timeout_handler = GLib.timeout_add_seconds(2, do_ui_update)
+
+ def _update_switch(self):
+ """Update the state of the on/off switch."""
+ switch = self._parent.switch_livepatch
+
+ availability = self._lps.props.availability
+ enabled = self._lps.props.enabled
+ logged = self._auth.logged
+
+ switch.set_sensitive(
+ availability == LivepatchAvailability.TRUE and
+ (enabled or logged))
+
+ if self._waiting_livepatch_response:
+ return
+
+ self._parent.switch_livepatch.handler_block(self._state_set_handler)
+ switch.set_state(switch.get_sensitive() and enabled)
+ self._parent.switch_livepatch.handler_unblock(self._state_set_handler)
+
+ def _update_spinner(self):
+ """Update the state of the in-progress spinner."""
+ spinner = self._parent.spinner_livepatch
+ availability = self._lps.props.availability
+
+ spinner.set_visible(availability == LivepatchAvailability.CHECKING)
+ spinner.props.active = (availability == LivepatchAvailability.CHECKING)
+
+ def _update_switch_label(self):
+ """Update the text of the label next to the on/off switch."""
+ availability = self._lps.props.availability
+ logged = self._auth.logged
+
+ if availability == LivepatchAvailability.CHECKING:
+ msg = _('Checking availability…')
+ elif availability == LivepatchAvailability.NO_CONNECTIVITY:
+ msg = _('Livepatch requires an Internet connection.')
+ elif availability == LivepatchAvailability.FALSE:
+ msg = _('Livepatch is not available for this system.')
+ else:
+ if self._parent.switch_livepatch.get_active():
+ msg = _("Livepatch is on.")
+ elif not logged:
+ msg = _("To use Livepatch you need to sign in.")
+ else:
+ msg = _("Livepatch is off.")
+
+ self._parent.label_livepatch_switch.set_label(msg)
+
+ def _update_auth_button(self):
+ """Update the state and the label of the authentication button."""
+ button = self._parent.button_livepatch_login
+
+ availability = self._lps.props.availability
+ logged = self._auth.logged
+
+ button.set_visible(
+ availability == LivepatchAvailability.TRUE and
+ not self._parent.switch_livepatch.get_active())
+ button.set_label(_('Sign Out') if logged else _('Sign In…'))
+
+ def _update_stack(self, error_message):
+ """Update the state of the stack.
+
+ If livepatch is not available nothing will be shown, if an error
+ occurred an error message will be shown in a text view, otherwise the
+ current livepatch status (e.g. a list of CVE fixes) will be shown.
+
+ Args:
+ error_message (str): error message to display.
+ """
+ availability = self._lps.props.availability
+ availability_message = self._lps.props.availability_message
+
+ has_error = (
+ error_message is not None or
+ (availability == LivepatchAvailability.FALSE and
+ availability_message is not None))
+
+ if has_error:
+ self._parent.stack_livepatch.set_visible_child_name('page_livepatch_message')
+ self._parent.stack_livepatch.set_visible(True)
+ text_buffer = self._parent.textview_livepatch.get_buffer()
+ text_buffer.delete(
+ text_buffer.get_start_iter(), text_buffer.get_end_iter())
+ text_buffer.insert_markup(
+ text_buffer.get_end_iter(),
+ error_message or availability_message, -1)
+ return
+
+ if availability == LivepatchAvailability.CHECKING or not self._parent.switch_livepatch.get_active():
+ self._parent.stack_livepatch.set_visible(False)
+ else:
+ self._update_status()
+
+ def _format_timedelta(self, td):
+ days = td.days
+ hours = td.seconds // 3600
+ minutes = td.seconds // 60
+
+ if days > 0:
+ return gettext.ngettext(
+ '({} day ago)',
+ '({} days ago)',
+ days).format(days)
+ elif hours > 0:
+ return gettext.ngettext(
+ '({} hour ago)',
+ '({} hours ago)',
+ hours).format(hours)
+ elif minutes > 0:
+ return gettext.ngettext(
+ '({} minute ago)',
+ '({} minutes ago)',
+ minutes).format(minutes)
+ else:
+ return ''
+
+ def _datetime_to_str(self, dt):
+ gdt = GLib.DateTime.new_from_unix_utc(dt.timestamp())
+ td = datetime.datetime.now(dt.tzinfo) - dt
+ return '{} {}'.format(
+ gdt.to_local().format('%x %H:%M'),
+ self._format_timedelta(td))
+
+ def _update_status(self):
+ """Populate the UI to reflect the Livepatch status"""
+ status = self._lps.get_status()
+
+ if status is None:
+ if not self._waiting_livepatch_response:
+ self._trigger_ui_update(skip=True, error_message=_('Failed to retrieve Livepatch status.'))
+ return
+
+ self._parent.stack_livepatch.set_visible_child_name('page_livepatch_status')
+ self._parent.stack_livepatch.set_visible(True)
+
+ check_state = status['Status'][0]['Livepatch']['CheckState'] if status else None
+ state = status['Status'][0]['Livepatch']['State'] if status else None
+
+ if check_state == 'check-failed':
+ self._trigger_ui_update(skip=True, error_message=self.GENERIC_ERR_MSG)
+ return
+
+ if state in ['applied-with-bug', 'apply-failed', 'unknown']:
+ self._trigger_ui_update(skip=True, error_message=self.GENERIC_ERR_MSG)
+ return
+
+ self._parent.label_livepatch_last_update.set_label(
+ _("Last check for updates: {}").format(
+ self._datetime_to_str(status['Last-Check']) if status else _('None yet')))
+
+ if state in ['unapplied', 'nothing-to-apply'] or state == None:
+ self._parent.label_livepatch_header.set_label(_('No updates currently applied.'))
+ self._parent.scrolledwindow_livepatch_fixes.set_visible(False)
+ elif state == 'applied':
+ self._parent.label_livepatch_header.set_label(_('Updates currently applied:'))
+ self._parent.scrolledwindow_livepatch_fixes.set_visible(True)
+ self._update_fixes(status['Status'][0]['Livepatch']['Fixes'])
+ else:
+ logging.warning('Livepatch status contains an invalid state: {}'.format(state))
+
+ if check_state == 'needs-check' or state == 'unapplied':
+ self._trigger_ui_update()
+
+ def _update_fixes(self, fixes):
+ """Populate the UI to show the list of applied CVE fixes."""
+ treeview = self._parent.treeview_livepatch
+ liststore = treeview.get_model()
+ liststore.clear()
+
+ for fix in fixes:
+ fix_iter = liststore.append()
+ liststore.set(fix_iter, [0], [self._format_fix(fix)])
+
+ def _format_fix(self, fix):
+ """Format a fix in a UI friendly text."""
+ return '{}\n{}'.format(
+ fix['Name'], fix['Description'].replace('\n', ' '))
+
+ def _do_login(self):
+ """Start the authentication flow to retrieve the livepatch token."""
+ dialog = DialogAuth(self._parent.window_main, self._parent.datadir)
+
+ if dialog.run() == Gtk.ResponseType.OK:
+ self._auth.login(dialog.account)
+ self._parent.switch_livepatch.set_state(True)
+
+ def _do_logout(self):
+ """Start the de-authentication flow."""
+ self._auth.logout()
+
+ # Signals handler
+ def _lps_availability_changed_cb(self, o, v):
+ self._trigger_ui_update(skip=True)
+
+ def _lps_enabled_changed_cb(self, o, v):
+ if self._waiting_livepatch_response:
+ return
+ self._trigger_ui_update(skip=False)
+
+ def _auth_changed_cb(self, o, v):
+ self._trigger_ui_update(skip=True)
+
+ def _switch_state_set_cb(self, widget, state):
+ if not self._waiting_livepatch_response:
+ self._waiting_livepatch_response = True
+
+ token = self._auth.token or ''
+ self._parent.backend.SetLivepatchEnabled(
+ state, token,
+ reply_handler=self._enabled_reply_handler,
+ error_handler=self._enabled_error_handler,
+ timeout=1200)
+
+ self._trigger_ui_update(skip=True)
+ self._parent.switch_livepatch.set_state(state)
+
+ return False
+
+ def _button_livepatch_login_clicked_cb(self, button):
+ if self._auth.logged:
+ self._do_logout()
+ else:
+ self._do_login()
+
+ def _show_error_dialog(self, message):
+ dialog = DialogLivepatchError(
+ self._parent.window_main,
+ self._parent.datadir)
+
+ response = dialog.run(
+ error=message,
+ show_settings_button=not self._parent.window_main.is_visible())
+
+ if response == DialogLivepatchError.RESPONSE_SETTINGS:
+ self._parent.window_main.show()
+ self._parent.notebook_main.set_current_page(6)
+ elif not self._parent.window_main.is_visible():
+ self._parent.on_close_button(None)
+
+ # DBus replay handlers
+ def _enabled_reply_handler(self, is_error, prompt):
+ if self._parent.switch_livepatch.get_active() == self._lps.props.enabled:
+ self._waiting_livepatch_response = False
+ self._trigger_ui_update(skip=True)
+
+ if not self._parent.window_main.is_visible():
+ self._parent.on_close_button(None)
+ else:
+ if is_error:
+ self._waiting_livepatch_response = False
+ self._trigger_ui_update(skip=True)
+ self._show_error_dialog(prompt)
+ else:
+ # The user tooggled on/off the switch while we were waiting
+ # livepatch to respond back.
+ self._parent.backend.SetLivepatchEnabled(
+ self._parent.switch_livepatch.get_active(),
+ self._auth.token,
+ reply_handler=self._enabled_reply_handler,
+ error_handler=self._enabled_error_handler,
+ timeout=1200)
+
+ def _enabled_error_handler(self, e):
+ self._waiting_livepatch_response = False
+ self._trigger_ui_update(skip=True)
+
+ if e._dbus_error_name == 'com.ubuntu.SoftwareProperties.PermissionDeniedByPolicy':
+ logging.warning("Authentication canceled, changes have not been saved")
+
+ if not self._parent.window_main.is_visible():
+ self._parent.on_close_button(None)
+ else:
+ self._show_error_dialog(str(e))
diff --git a/softwareproperties/gtk/SimpleGtkbuilderApp.py b/softwareproperties/gtk/SimpleGtkbuilderApp.py
index 06375ff..e14f840 100644
--- a/softwareproperties/gtk/SimpleGtkbuilderApp.py
+++ b/softwareproperties/gtk/SimpleGtkbuilderApp.py
@@ -38,6 +38,9 @@ class SimpleGtkbuilderApp(Gtk.Application):
def on_activate(self, data=None):
self.add_window(self.window_main)
+ if not self.window_main.is_visible():
+ self.window_main.show()
+
def run(self):
"""
Starts the main loop of processing events checking for Control-C.
diff --git a/softwareproperties/gtk/SoftwarePropertiesGtk.py b/softwareproperties/gtk/SoftwarePropertiesGtk.py
index 75ff85d..9271f54 100644
--- a/softwareproperties/gtk/SoftwarePropertiesGtk.py
+++ b/softwareproperties/gtk/SoftwarePropertiesGtk.py
@@ -27,9 +27,6 @@ from __future__ import absolute_import, print_function
import apt
import apt_pkg
-import aptsources.distro
-from datetime import datetime
-import distro_info
import dbus
from gettext import gettext as _
import gettext
@@ -52,11 +49,9 @@ from .DialogMirror import DialogMirror
from .DialogEdit import DialogEdit
from .DialogCacheOutdated import DialogCacheOutdated
from .DialogAddSourcesList import DialogAddSourcesList
-from .DialogLivepatchError import DialogLivepatchError
-from .DialogAuth import DialogAuth
+from .LivepatchPage import LivepatchPage
import softwareproperties
-from softwareproperties.GoaAuth import GoaAuth
import softwareproperties.distro
from softwareproperties.SoftwareProperties import SoftwareProperties
import softwareproperties.SoftwareProperties
@@ -85,8 +80,6 @@ RESPONSE_ADD = 2
STORE_VISIBLE
) = list(range(5))
-LIVEPATCH_TIMEOUT = 1200
-
def error(parent_window, summary, msg):
""" show a error dialog """
@@ -1045,7 +1038,9 @@ class SoftwarePropertiesGtk(SoftwareProperties, SimpleGtkbuilderApp):
d = DialogCacheOutdated(self.window_main,
self.datadir)
d.run()
- if self.waiting_livepatch_response:
+
+ self.quit_when_livepatch_responds = False
+ if self.livepatch_page.waiting_livepatch_response:
self.quit_when_livepatch_responds = True
self.hide()
else:
@@ -1483,163 +1478,5 @@ class SoftwarePropertiesGtk(SoftwareProperties, SimpleGtkbuilderApp):
else:
self.label_driver_action.set_label(_("No proprietary drivers are in use."))
- #
- # Livepatch
- #
def init_livepatch(self):
- self.goa_auth = GoaAuth()
- self.waiting_livepatch_response = False
- self.quit_when_livepatch_responds = False
-
- if not self.is_livepatch_supported():
- self.grid_livepatch.set_visible(False)
- return
-
- self.checkbutton_livepatch.set_active(self.is_livepatch_enabled())
- self.on_goa_auth_changed()
-
- # hacky way to monitor if livepatch is enabled or not
- file = Gio.File.new_for_path(path=self.LIVEPATCH_RUNNING_FILE)
- self.lp_monitor = file.monitor_file(Gio.FileMonitorFlags.NONE)
-
- # connect to signals
- self.handlers[self.goa_auth] = \
- self.goa_auth.connect('notify', lambda o, p: self.on_goa_auth_changed())
- self.handlers[self.checkbutton_livepatch] = \
- self.checkbutton_livepatch.connect('toggled', self.on_checkbutton_livepatch_toggled)
- self.handlers[self.button_ubuntuone] = \
- self.button_ubuntuone.connect('clicked', self.on_button_ubuntuone_clicked)
- self.handlers[self.lp_monitor] = \
- self.lp_monitor.connect('changed', self.on_livepatch_status_changed)
-
- def has_online_accounts(self):
- try:
- d = Gio.DesktopAppInfo.new('gnome-online-accounts-panel.desktop')
- return d != None
- except Exception:
- return False
-
- def is_livepatch_supported(self):
- distro = aptsources.distro.get_distro()
- di = distro_info.UbuntuDistroInfo()
- return self.has_online_accounts() and \
- di.is_lts(distro.codename) and \
- distro.codename in di.supported(datetime.now().date())
-
- def on_goa_auth_changed(self):
- if self.goa_auth.logged:
- self.button_ubuntuone.set_label(_('Sign Out'))
-
- if self.goa_auth.token:
- self.checkbutton_livepatch.set_sensitive(True)
- self.label_livepatch_login.set_label(_('Signed in as %s' % self.goa_auth.username))
- else:
- self.checkbutton_livepatch.set_sensitive(False)
- text = _('%s isn\'t authorized to use Livepatch.' % self.goa_auth.username)
- text = "" + text + ""
- self.label_livepatch_login.set_markup(text)
- else:
- if self.is_livepatch_enabled() and not self.waiting_livepatch_response:
- # Allow the user to disable livepatch even if
- # the account expired (see LP: #1768797)
- self.checkbutton_livepatch.set_sensitive(True)
- self.label_livepatch_login.set_label(_('Livepatch is active.'))
- else:
- self.checkbutton_livepatch.set_sensitive(False)
- self.label_livepatch_login.set_label(_('To use Livepatch you need to sign in.'))
-
- self.button_ubuntuone.set_label(_('Sign In…'))
-
- def on_livepatch_status_changed(self, file_monitor, file, other_file, event_type):
- if not self.waiting_livepatch_response:
- self.checkbutton_livepatch.set_active(self.is_livepatch_enabled())
- self.on_goa_auth_changed()
-
- def on_button_ubuntuone_clicked(self, button):
- if self.goa_auth.logged:
- self.do_logout()
- else:
- self.do_login()
-
- def do_login(self):
- try:
- # Show login dialog!
- dialog = DialogAuth(self.window_main, self.datadir)
- response = dialog.run()
- except Exception as e:
- logging.error(e)
- error(self.window_main,
- _("Error enabling Canonical Livepatch"),
- _("Please check your Internet connection."))
- else:
- if response == Gtk.ResponseType.OK:
- self.goa_auth.login(dialog.account)
- if self.goa_auth.logged:
- self.checkbutton_livepatch.set_active(True)
-
- def do_logout(self):
- self.checkbutton_livepatch.set_active(False)
- self.goa_auth.logout()
-
- def on_checkbutton_livepatch_toggled(self, checkbutton):
- if self.waiting_livepatch_response:
- return
-
- self.waiting_livepatch_response = True
-
- token = ''
- enabled = False
- if self.checkbutton_livepatch.get_active():
- enabled = True
- token = self.goa_auth.token if self.goa_auth.token else ''
- self.backend.SetLivepatchEnabled(enabled, token,
- reply_handler=self.livepatch_enabled_reply_handler,
- error_handler=self.livepatch_enabled_error_handler,
- timeout=LIVEPATCH_TIMEOUT)
-
- def livepatch_enabled_reply_handler(self, is_error, prompt):
- self.sync_checkbutton_livepatch(is_error, prompt)
-
- def livepatch_enabled_error_handler(self, e):
- if e._dbus_error_name == 'com.ubuntu.SoftwareProperties.PermissionDeniedByPolicy':
- logging.error("Authentication canceled, changes have not been saved")
- self.sync_checkbutton_livepatch(is_error=True, prompt=None)
- else:
- self.sync_checkbutton_livepatch(is_error=True, prompt=str(e))
-
- def sync_checkbutton_livepatch(self, is_error, prompt):
- if is_error:
- self.waiting_livepatch_response = False
- self.checkbutton_livepatch.handler_block(self.handlers[self.checkbutton_livepatch])
- self.checkbutton_livepatch.set_active(self.is_livepatch_enabled())
- self.checkbutton_livepatch.handler_unblock(self.handlers[self.checkbutton_livepatch])
-
- if prompt:
- dialog = DialogLivepatchError(self.window_main, self.datadir)
- response = dialog.run(prompt, show_settings_button=self.quit_when_livepatch_responds)
- if response == DialogLivepatchError.RESPONSE_SETTINGS:
- self.window_main.show()
- self.quit_when_livepatch_responds = False
- else:
- do_dbus_call = False
- if self.is_livepatch_enabled() and not self.checkbutton_livepatch.get_active():
- do_dbus_call = True
- enabled = False
- token = ''
- elif not self.is_livepatch_enabled() and self.checkbutton_livepatch.get_active():
- do_dbus_call = True
- enabled = True
- token = self.goa_auth.token if self.goa_auth.token else ''
- else:
- self.waiting_livepatch_response = False
-
- if do_dbus_call:
- self.backend.SetLivepatchEnabled(enabled, token,
- reply_handler=self.livepatch_enabled_reply_handler,
- error_handler=self.livepatch_enabled_error_handler,
- timeout=LIVEPATCH_TIMEOUT)
-
- self.on_goa_auth_changed()
-
- if self.quit_when_livepatch_responds:
- self.on_close_button(self.button_close)
+ self.livepatch_page = LivepatchPage(self)
diff --git a/softwareproperties/gtk/utils.py b/softwareproperties/gtk/utils.py
index 37f0c10..e0ddca9 100644
--- a/softwareproperties/gtk/utils.py
+++ b/softwareproperties/gtk/utils.py
@@ -18,13 +18,19 @@
from __future__ import print_function
+import aptsources.distro
+from datetime import datetime
+import distro_info
+from functools import wraps
import gi
gi.require_version("Gtk", "3.0")
-from gi.repository import Gtk
+from gi.repository import Gio, Gtk
import logging
LOG=logging.getLogger(__name__)
+import time
+
def setup_ui(self, path, domain):
# setup ui
self.builder = Gtk.Builder()
@@ -37,3 +43,52 @@ def setup_ui(self, path, domain):
setattr(self, name, o)
else:
logging.debug("can not get name for object '%s'" % o)
+
+def has_gnome_online_accounts():
+ try:
+ d = Gio.DesktopAppInfo.new('gnome-online-accounts-panel.desktop')
+ return d != None
+ except Exception:
+ return False
+
+def is_current_distro_lts():
+ distro = aptsources.distro.get_distro()
+ di = distro_info.UbuntuDistroInfo()
+ return di.is_lts(distro.codename)
+
+def is_current_distro_supported():
+ distro = aptsources.distro.get_distro()
+ di = distro_info.UbuntuDistroInfo()
+ return distro.codename in di.supported(datetime.now().date())
+
+def retry(exceptions, tries=10, delay=0.1, backoff=2):
+ """
+ Retry calling the decorated function using an exponential backoff.
+
+ Args:
+ exceptions: The exception to check. may be a tuple of
+ exceptions to check.
+ tries: Number of times to try (not retry) before giving up.
+ delay: Initial delay between retries in seconds.
+ backoff: Backoff multiplier (e.g. value of 2 will double the delay
+ each retry).
+ """
+ def deco_retry(f):
+
+ @wraps(f)
+ def f_retry(*args, **kwargs):
+ mtries, mdelay = tries, delay
+ while mtries > 1:
+ try:
+ return f(*args, **kwargs)
+ except exceptions as e:
+ msg = '{}, Retrying in {} seconds...'.format(e, mdelay)
+ logging.warning(msg)
+ time.sleep(mdelay)
+ mtries -= 1
+ mdelay *= backoff
+ return f(*args, **kwargs)
+
+ return f_retry # true decorator
+
+ return deco_retry
diff --git a/tests/aptroot/etc/apt/apt.conf.d/.keep b/tests/aptroot/etc/apt/apt.conf.d/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/aptroot/etc/apt/apt.conf.d/.keep