From 20bfa25b81320eac095378f7e1e14c9c2c796772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marta=20Marczykowska-G=C3=B3recka?= Date: Thu, 28 May 2020 20:55:01 +0200 Subject: [PATCH] Added notifications and information about VM volumes being full The qui-disk-space widget will now show notifications when a VM's storage space is close to running out. Also information about this will be displayed in the widget menu itself. references QubesOS/qubes-issues#1872 --- qui/tray/disk_space.py | 205 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 181 insertions(+), 24 deletions(-) diff --git a/qui/tray/disk_space.py b/qui/tray/disk_space.py index 3607f384..19840dad 100644 --- a/qui/tray/disk_space.py +++ b/qui/tray/disk_space.py @@ -1,8 +1,9 @@ # pylint: disable=wrong-import-position,import-error import sys +import subprocess import gi gi.require_version('Gtk', '3.0') # isort:skip -from gi.repository import Gtk, GObject, Gio # isort:skip +from gi.repository import Gtk, GObject, Gio, GLib # isort:skip from qubesadmin import Qubes from qubesadmin.utils import size_to_human @@ -16,9 +17,118 @@ URGENT_WARN_LEVEL = 0.95 +class VMUsage: + def __init__(self, vm): + self.vm = vm + self.problem_volumes = {} + + self.check_usage() + + def check_usage(self): + self.problem_volumes = {} + volumes_to_check = ['private'] + if not hasattr(self.vm, 'template'): + volumes_to_check.append('root') + for volume_name in volumes_to_check: + if volume_name in self.vm.volumes: + size = self.vm.volumes[volume_name].size + usage = self.vm.volumes[volume_name].usage + if size > 0 and usage / size > WARN_LEVEL: + self.problem_volumes[volume_name] = usage / size + + +class VMUsageData: + def __init__(self, qubes_app): + self.qubes_app = qubes_app + self.problematic_vms = [] + + self.__populate_vms() + + def __populate_vms(self): + for vm in self.qubes_app.domains: + if vm.is_running(): + usage_data = VMUsage(vm) + if usage_data.problem_volumes: + self.problematic_vms.append(usage_data) + + def get_vms_widgets(self): + for vm_usage in self.problematic_vms: + yield self.__create_widgets(vm_usage) + + @staticmethod + def __create_widgets(vm_usage): + vm = vm_usage.vm + + # icon widget + try: + icon = vm.icon + except AttributeError: + icon = vm.label.icon + icon_vm = Gtk.IconTheme.get_default().load_icon(icon, 16, 0) + icon_img = Gtk.Image.new_from_pixbuf(icon_vm) + + # description widget + label_widget = Gtk.Label(xalign=0) + + label_contents = [] + + for volume_name, usage in vm_usage.problem_volumes.items(): + label_contents.append('volume {} is {:.1%} full'.format( + volume_name, usage)) + + label_text = "{}: ".format(vm.name) + ", ".join(label_contents) + label_widget.set_markup(label_text) + + return vm, icon_img, label_widget + + +class SettingsItem(Gtk.MenuItem): + def __init__(self, vm): + super().__init__() + self.vm = vm + + self.set_label(_('Open Qube Settings')) + + self.connect('activate', launch_preferences_dialog, self.vm.name) + + +def launch_preferences_dialog(_, vm): + vm = str(vm).strip('\'') + subprocess.Popen(['qubes-vm-settings', vm]) + + +class NeverNotifyItem(Gtk.CheckMenuItem): + def __init__(self, vm): + super().__init__() + self.vm = vm + + self.set_label(_('Do not show notifications about this qube')) + + self.set_active(self.vm.features.get('disk-space-not-notify', False)) + + self.connect('toggled', self.toggle_state) + + def toggle_state(self, _item): + if self.get_active(): + self.vm.features['disk-space-not-notify'] = 1 + else: + del self.vm.features['disk-space-not-notify'] + + +class VMMenu(Gtk.Menu): + def __init__(self, vm): + super().__init__() + self.vm = vm + + self.add(NeverNotifyItem(self.vm)) + self.add(SettingsItem(self.vm)) + + self.show_all() + + class PoolUsageData: - def __init__(self): - self.qubes_app = Qubes() + def __init__(self, qubes_app): + self.qubes_app = qubes_app self.pools = [] self.total_size = 0 @@ -86,7 +196,6 @@ def __create_box(pool): name_box.pack_start(metadata_name, True, True, 0) - percentage = pool.usage/pool.size percentage_use = Gtk.Label() @@ -141,15 +250,34 @@ def colored_percentage(value): return result +def emit_notification(gtk_app, title, text, vm=None): + notification = Gio.Notification.new(title) + notification.set_priority(Gio.NotificationPriority.HIGH) + notification.set_body(text) + notification.set_icon(Gio.ThemedIcon.new('dialog-warning')) + + if vm: + notification.add_button('Open qube settings', + "app.prefs::{}".format(vm.name)) + + gtk_app.send_notification(None, notification) + + class DiskSpace(Gtk.Application): def __init__(self, **properties): super().__init__(**properties) self.warned = False + self.qubes_app = Qubes() + self.set_application_id("org.qubes.qui.tray.DiskSpace") self.register() + prefs_action = Gio.SimpleAction.new("prefs", GLib.VariantType.new("s")) + prefs_action.connect("activate", launch_preferences_dialog) + self.add_action(prefs_action) + self.icon = Gtk.StatusIcon() self.icon.connect('button-press-event', self.make_menu) self.refresh_icon() @@ -159,47 +287,52 @@ def __init__(self, **properties): Gtk.main() def refresh_icon(self): - pool_data = PoolUsageData() - warning = pool_data.get_warning() + pool_data = PoolUsageData(self.qubes_app) + vm_data = VMUsageData(self.qubes_app) + pool_warning = pool_data.get_warning() + vm_warning = vm_data.problematic_vms - if warning: + if pool_warning: self.icon.set_from_icon_name("dialog-warning") text = _("Qubes Disk Space Monitor\nWARNING! You are " - "running out of disk space.") + ''.join(warning) + "running out of disk space.") + ''.join(pool_warning) self.icon.set_tooltip_markup(text) if not self.warned: - notification = Gio.Notification.new(_("Disk usage warning!")) - notification.set_priority(Gio.NotificationPriority.HIGH) - notification.set_body( - _("You are running out of disk space.") + ''.join(warning)) - notification.set_icon( - Gio.ThemedIcon.new('dialog-warning')) - - self.send_notification(None, notification) + emit_notification( + self, + _("Disk usage warning!"), + _("You are running out of disk space.") + ''.join( + pool_warning)) self.warned = True - else: self.icon.set_from_icon_name("drive-harddisk") self.icon.set_tooltip_markup( _('Qubes Disk Space Monitor\nView free disk space.')) self.warned = False + if vm_warning: + for vm_data in vm_warning: + vm = vm_data.vm + if not vm.features.get('disk-space-not-notify', False): + emit_notification( + self, + _("Qube usage warning"), + _("Qube {} is running out of storage space.".format( + vm.name)), + vm=vm) + return True # needed for Gtk to correctly loop the function def make_menu(self, _unused, _event): - pool_data = PoolUsageData() + pool_data = PoolUsageData(self.qubes_app) + vm_data = VMUsageData(self.qubes_app) menu = Gtk.Menu() menu.append(self.make_top_box(pool_data)) - title_label = Gtk.Label(xalign=0) - title_label.set_markup(_("Volumes")) - title_menu_item = Gtk.MenuItem() - title_menu_item.add(title_label) - title_menu_item.set_sensitive(False) - menu.append(title_menu_item) + menu.append(self.make_title_item('Volumes')) grid = Gtk.Grid() col_no = 0 @@ -215,11 +348,35 @@ def make_menu(self, _unused, _event): grid_menu_item.set_sensitive(False) menu.append(grid_menu_item) + if vm_data.problematic_vms: + menu.append(self.make_title_item('Qubes warnings')) + + for (vm, label1, label2) in vm_data.get_vms_widgets(): + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + hbox.pack_start(label1, False, False, 0) + hbox.pack_start(label2, False, False, 5) + + vm_menu_item = Gtk.MenuItem() + vm_menu_item.add(hbox) + + vm_menu_item.set_submenu(VMMenu(vm)) + + menu.append(vm_menu_item) + menu.set_reserve_toggle_size(False) menu.show_all() menu.popup_at_pointer(None) # use current event + @staticmethod + def make_title_item(text): + label = Gtk.Label(xalign=0) + label.set_markup(_("{}".format(text))) + menu_item = Gtk.MenuItem() + menu_item.add(label) + menu_item.set_sensitive(False) + return menu_item + @staticmethod def make_top_box(pool_data): grid = Gtk.Grid()