1# Copyright (C) 2006, 2012-2015 Red Hat, Inc.
2# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
3#
4# This work is licensed under the GNU GPLv2 or later.
5# See the COPYING file in the top-level directory.
6
7from gi.repository import Gio
8from gi.repository import GLib
9from gi.repository import Gtk
10
11from virtinst import DomainCpu
12from virtinst import log
13
14from .lib.inspection import vmmInspection
15
16
17CSSDATA = """
18/* Lighter colored text in some wizard summary fields */
19.vmm-lighter {
20    color: @insensitive_fg_color
21}
22
23/* Text on the blue header in our wizards */
24.vmm-header-text {
25    color: white
26}
27
28/* Subtext on the blue header in our wizards */
29.vmm-header-subtext {
30    color: #59B0E2
31}
32
33/* The blue header */
34.vmm-header {
35    background-color: #0072A8
36}
37"""
38
39
40class _SettingsWrapper(object):
41    """
42    Wrapper class to simplify interacting with gsettings APIs.
43    Basically it allows simple get/set of gconf style paths, and
44    we internally convert it to the settings nested hierarchy. Makes
45    client code much smaller.
46    """
47    def __init__(self, settings_id, gsettings_keyfile):
48        self._root = settings_id
49
50        if gsettings_keyfile:
51            backend = Gio.keyfile_settings_backend_new(gsettings_keyfile, "/")
52        else:
53            backend = Gio.SettingsBackend.get_default()
54
55        self._settings = Gio.Settings.new_with_backend(self._root, backend)
56
57        self._settingsmap = {"": self._settings}
58        self._handler_map = {}
59
60        for child in self._settings.list_children():
61            childschema = self._root + "." + child
62            self._settingsmap[child] = Gio.Settings.new_with_backend(
63                    childschema, backend)
64
65
66    ###################
67    # Private helpers #
68    ###################
69
70    def _parse_key(self, key):
71        value = key.strip("/")
72        settingskey = ""
73        if "/" in value:
74            settingskey, value = value.rsplit("/", 1)
75        return settingskey, value
76
77    def _find_settings(self, key):
78        settingskey, value = self._parse_key(key)
79        return self._settingsmap[settingskey], value
80
81
82    ###############
83    # Public APIs #
84    ###############
85
86    def make_vm_settings(self, key):
87        """
88        Initialize per-VM relocatable schema if necessary
89        """
90        settingskey = self._parse_key(key)[0]
91        if settingskey in self._settingsmap:
92            return True
93
94        schema = self._root + ".vm"
95        path = "/" + self._root.replace(".", "/") + key.rsplit("/", 1)[0] + "/"
96        self._settingsmap[settingskey] = Gio.Settings.new_with_path(
97                schema, path)
98        return True
99
100    def make_conn_settings(self, key):
101        """
102        Initialize per-conn relocatable schema if necessary
103        """
104        settingskey = self._parse_key(key)[0]
105        if settingskey in self._settingsmap:
106            return True
107
108        schema = self._root + ".connection"
109        path = "/" + self._root.replace(".", "/") + key.rsplit("/", 1)[0] + "/"
110        self._settingsmap[settingskey] = Gio.Settings.new_with_path(
111                schema, path)
112        return True
113
114    def notify_add(self, key, cb, *args, **kwargs):
115        settings, key = self._find_settings(key)
116        def wrapcb(*ignore):
117            return cb(*args, **kwargs)
118        ret = settings.connect("changed::%s" % key, wrapcb, *args, **kwargs)
119        self._handler_map[ret] = settings
120        return ret
121    def notify_remove(self, h):
122        settings = self._handler_map.pop(h)
123        return settings.disconnect(h)
124
125    def get(self, key):
126        settings, key = self._find_settings(key)
127        return settings.get_value(key).unpack()
128    def set(self, key, value, *args, **kwargs):
129        settings, key = self._find_settings(key)
130        fmt = settings.get_value(key).get_type_string()
131        return settings.set_value(key, GLib.Variant(fmt, value),
132                                  *args, **kwargs)
133
134
135class vmmConfig(object):
136    # key names for saving last used paths
137    CONFIG_DIR_IMAGE = "image"
138    CONFIG_DIR_ISO_MEDIA = "isomedia"
139    CONFIG_DIR_FLOPPY_MEDIA = "floppymedia"
140    CONFIG_DIR_SCREENSHOT = "screenshot"
141    CONFIG_DIR_FS = "fs"
142
143    # Metadata mapping for browse types. Prob shouldn't go here, but works
144    # for now.
145    browse_reason_data = {
146        CONFIG_DIR_IMAGE: {
147            "enable_create":  True,
148            "storage_title":  _("Locate or create storage volume"),
149            "local_title":    _("Locate existing storage"),
150            "dialog_type":    Gtk.FileChooserAction.SAVE,
151            "choose_button":  Gtk.STOCK_OPEN,
152            "gsettings_key": "image",
153        },
154
155        CONFIG_DIR_SCREENSHOT: {
156            "gsettings_key": "screenshot",
157        },
158
159        CONFIG_DIR_ISO_MEDIA: {
160            "enable_create":  False,
161            "storage_title":  _("Locate ISO media volume"),
162            "local_title":    _("Locate ISO media"),
163            "gsettings_key": "media",
164        },
165
166        CONFIG_DIR_FLOPPY_MEDIA: {
167            "enable_create":  False,
168            "storage_title":  _("Locate floppy media volume"),
169            "local_title":    _("Locate floppy media"),
170            "gsettings_key": "media",
171        },
172
173        CONFIG_DIR_FS: {
174            "enable_create":  False,
175            "storage_title":  _("Locate directory volume"),
176            "local_title":    _("Locate directory volume"),
177            "dialog_type":    Gtk.FileChooserAction.SELECT_FOLDER,
178        },
179    }
180
181    CONSOLE_SCALE_NEVER = 0
182    CONSOLE_SCALE_FULLSCREEN = 1
183    CONSOLE_SCALE_ALWAYS = 2
184
185    _instance = None
186
187    @classmethod
188    def get_instance(cls, *args, **kwargs):
189        if not cls._instance:
190            cls._instance = vmmConfig(*args, **kwargs)
191        return cls._instance
192
193    @classmethod
194    def is_initialized(cls):
195        return bool(cls._instance)
196
197    def __init__(self, BuildConfig, CLITestOptions):
198        self.appname = "virt-manager"
199        self.appversion = BuildConfig.version
200        self.conf_dir = "/org/virt-manager/%s/" % self.appname
201        self.ui_dir = BuildConfig.ui_dir
202
203        self.conf = _SettingsWrapper("org.virt-manager.virt-manager",
204                CLITestOptions.gsettings_keyfile)
205
206        self.CLITestOptions = CLITestOptions
207        if self.CLITestOptions.xmleditor_enabled:
208            self.set_xmleditor_enabled(True)
209        if self.CLITestOptions.enable_libguestfs:
210            self.set_libguestfs_inspect_vms(True)
211        if self.CLITestOptions.disable_libguestfs:
212            self.set_libguestfs_inspect_vms(False)
213
214        # We don't create it straight away, since we don't want
215        # to block the app pending user authorization to access
216        # the keyring
217        self._keyring = None
218
219        self.default_graphics_from_config = BuildConfig.default_graphics
220        self.default_hvs = BuildConfig.default_hvs
221
222        self.default_storage_format_from_config = "qcow2"
223        self.default_console_resizeguest = 0
224
225        self._objects = []
226        self.color_insensitive = None
227        self._init_css()
228
229    def _init_css(self):
230        from gi.repository import Gdk
231        screen = Gdk.Screen.get_default()
232
233        css_provider = Gtk.CssProvider()
234        css_provider.load_from_data(CSSDATA.encode("utf-8"))
235
236        context = Gtk.StyleContext()
237        context.add_provider_for_screen(screen, css_provider,
238             Gtk.STYLE_PROVIDER_PRIORITY_USER)
239
240        found, color = context.lookup_color("insensitive_fg_color")
241        if not found:  # pragma: no cover
242            log.debug("Didn't find insensitive_fg_color in theme")
243            return
244        self.color_insensitive = color.to_string()
245
246
247    # General app wide helpers (gsettings agnostic)
248
249    def get_appname(self):
250        return self.appname
251    def get_appversion(self):
252        return self.appversion
253    def get_ui_dir(self):
254        return self.ui_dir
255
256    def embeddable_graphics(self):
257        ret = ["vnc", "spice"]
258        return ret
259
260    def inspection_supported(self):
261        if not vmmInspection.libguestfs_installed():
262            return False  # pragma: no cover
263        return self.get_libguestfs_inspect_vms()
264
265    def remove_notifier(self, h):
266        self.conf.notify_remove(h)
267
268    # Used for debugging reference leaks, we keep track of all objects
269    # come and go so we can do a leak report at app shutdown
270    def add_object(self, obj):
271        self._objects.append(obj)
272    def remove_object(self, obj):
273        self._objects.remove(obj)
274    def get_objects(self):
275        return self._objects[:]
276
277
278    #####################################
279    # Wrappers for setting per-VM value #
280    #####################################
281
282    def _make_pervm_key(self, uuid, key):
283        return "/vms/%s%s" % (uuid.replace("-", ""), key)
284
285    def listen_pervm(self, uuid, key, *args, **kwargs):
286        key = self._make_pervm_key(uuid, key)
287        self.conf.make_vm_settings(key)
288        return self.conf.notify_add(key, *args, **kwargs)
289
290    def set_pervm(self, uuid, key, *args, **kwargs):
291        key = self._make_pervm_key(uuid, key)
292        self.conf.make_vm_settings(key)
293        ret = self.conf.set(key, *args, **kwargs)
294        return ret
295
296    def get_pervm(self, uuid, key):
297        key = self._make_pervm_key(uuid, key)
298        self.conf.make_vm_settings(key)
299        return self.conf.get(key)
300
301
302    ########################################
303    # Wrappers for setting per-conn values #
304    ########################################
305
306    def _make_perconn_key(self, uri, key):
307        return "/conns/%s%s" % (uri.replace("/", ""), key)
308
309    def listen_perconn(self, uri, key, *args, **kwargs):
310        key = self._make_perconn_key(uri, key)
311        self.conf.make_conn_settings(key)
312        return self.conf.notify_add(key, *args, **kwargs)
313
314    def set_perconn(self, uri, key, *args, **kwargs):
315        key = self._make_perconn_key(uri, key)
316        self.conf.make_conn_settings(key)
317        ret = self.conf.set(key, *args, **kwargs)
318        return ret
319
320    def get_perconn(self, uri, key):
321        key = self._make_perconn_key(uri, key)
322        self.conf.make_conn_settings(key)
323        return self.conf.get(key)
324
325
326    ###################
327    # General helpers #
328    ###################
329
330    # Manager stats view preferences
331    def is_vmlist_guest_cpu_usage_visible(self):
332        return self.conf.get("/vmlist-fields/cpu-usage")
333    def is_vmlist_host_cpu_usage_visible(self):
334        return self.conf.get("/vmlist-fields/host-cpu-usage")
335    def is_vmlist_memory_usage_visible(self):
336        return self.conf.get("/vmlist-fields/memory-usage")
337    def is_vmlist_disk_io_visible(self):
338        return self.conf.get("/vmlist-fields/disk-usage")
339    def is_vmlist_network_traffic_visible(self):
340        return self.conf.get("/vmlist-fields/network-traffic")
341
342    def set_vmlist_guest_cpu_usage_visible(self, state):
343        self.conf.set("/vmlist-fields/cpu-usage", state)
344    def set_vmlist_host_cpu_usage_visible(self, state):
345        self.conf.set("/vmlist-fields/host-cpu-usage", state)
346    def set_vmlist_memory_usage_visible(self, state):
347        self.conf.set("/vmlist-fields/memory-usage", state)
348    def set_vmlist_disk_io_visible(self, state):
349        self.conf.set("/vmlist-fields/disk-usage", state)
350    def set_vmlist_network_traffic_visible(self, state):
351        self.conf.set("/vmlist-fields/network-traffic", state)
352
353    def on_vmlist_guest_cpu_usage_visible_changed(self, cb):
354        return self.conf.notify_add("/vmlist-fields/cpu-usage", cb)
355    def on_vmlist_host_cpu_usage_visible_changed(self, cb):
356        return self.conf.notify_add("/vmlist-fields/host-cpu-usage", cb)
357    def on_vmlist_memory_usage_visible_changed(self, cb):
358        return self.conf.notify_add("/vmlist-fields/memory-usage", cb)
359    def on_vmlist_disk_io_visible_changed(self, cb):
360        return self.conf.notify_add("/vmlist-fields/disk-usage", cb)
361    def on_vmlist_network_traffic_visible_changed(self, cb):
362        return self.conf.notify_add("/vmlist-fields/network-traffic", cb)
363
364    # Keys preferences
365    def get_keys_combination(self):
366        ret = self.conf.get("/console/grab-keys")
367        if not ret:
368            # Left Control + Left Alt
369            return "65507,65513"
370        return ret
371    def set_keys_combination(self, val):
372        # Val have to be a list of integers
373        val = ','.join([str(v) for v in val])
374        self.conf.set("/console/grab-keys", val)
375    def on_keys_combination_changed(self, cb):
376        return self.conf.notify_add("/console/grab-keys", cb)
377
378    # Confirmation preferences
379    def get_confirm_forcepoweroff(self):
380        return self.conf.get("/confirm/forcepoweroff")
381    def get_confirm_poweroff(self):
382        return self.conf.get("/confirm/poweroff")
383    def get_confirm_pause(self):
384        return self.conf.get("/confirm/pause")
385    def get_confirm_removedev(self):
386        return self.conf.get("/confirm/removedev")
387    def get_confirm_unapplied(self):
388        return self.conf.get("/confirm/unapplied-dev")
389    def get_confirm_delstorage(self):
390        return self.conf.get("/confirm/delete-storage")
391
392    def set_confirm_forcepoweroff(self, val):
393        self.conf.set("/confirm/forcepoweroff", val)
394    def set_confirm_poweroff(self, val):
395        self.conf.set("/confirm/poweroff", val)
396    def set_confirm_pause(self, val):
397        self.conf.set("/confirm/pause", val)
398    def set_confirm_removedev(self, val):
399        self.conf.set("/confirm/removedev", val)
400    def set_confirm_unapplied(self, val):
401        self.conf.set("/confirm/unapplied-dev", val)
402    def set_confirm_delstorage(self, val):
403        self.conf.set("/confirm/delete-storage", val)
404
405
406    # System tray visibility
407    def on_view_system_tray_changed(self, cb):
408        return self.conf.notify_add("/system-tray", cb)
409    def get_view_system_tray(self):
410        return self.conf.get("/system-tray")
411    def set_view_system_tray(self, val):
412        self.conf.set("/system-tray", val)
413
414
415    # XML editor enabled
416    def on_xmleditor_enabled_changed(self, cb):
417        return self.conf.notify_add("/xmleditor-enabled", cb)
418    def get_xmleditor_enabled(self):
419        return self.conf.get("/xmleditor-enabled")
420    def set_xmleditor_enabled(self, val):
421        self.conf.set("/xmleditor-enabled", val)
422
423
424    # Libguestfs VM inspection
425    def get_libguestfs_inspect_vms(self):
426        return self.conf.get("/enable-libguestfs-vm-inspection")
427    def set_libguestfs_inspect_vms(self, val):
428        self.conf.set("/enable-libguestfs-vm-inspection", val)
429
430
431    # Stats history and interval length
432    def get_stats_history_length(self):
433        return 120
434    def get_stats_update_interval(self):
435        if self.CLITestOptions.short_poll:
436            return .1
437        interval = self.conf.get("/stats/update-interval")
438        return max(interval, 1)
439    def set_stats_update_interval(self, interval):
440        self.conf.set("/stats/update-interval", interval)
441    def on_stats_update_interval_changed(self, cb):
442        return self.conf.notify_add("/stats/update-interval", cb)
443
444
445    # Disable/Enable different stats polling
446    def get_stats_enable_cpu_poll(self):
447        return self.conf.get("/stats/enable-cpu-poll")
448    def get_stats_enable_disk_poll(self):
449        return self.conf.get("/stats/enable-disk-poll")
450    def get_stats_enable_net_poll(self):
451        return self.conf.get("/stats/enable-net-poll")
452    def get_stats_enable_memory_poll(self):
453        return self.conf.get("/stats/enable-memory-poll")
454
455    def set_stats_enable_cpu_poll(self, val):
456        self.conf.set("/stats/enable-cpu-poll", val)
457    def set_stats_enable_disk_poll(self, val):
458        self.conf.set("/stats/enable-disk-poll", val)
459    def set_stats_enable_net_poll(self, val):
460        self.conf.set("/stats/enable-net-poll", val)
461    def set_stats_enable_memory_poll(self, val):
462        self.conf.set("/stats/enable-memory-poll", val)
463
464    def on_stats_enable_cpu_poll_changed(self, cb, row=None):
465        return self.conf.notify_add("/stats/enable-cpu-poll", cb, row)
466    def on_stats_enable_disk_poll_changed(self, cb, row=None):
467        return self.conf.notify_add("/stats/enable-disk-poll", cb, row)
468    def on_stats_enable_net_poll_changed(self, cb, row=None):
469        return self.conf.notify_add("/stats/enable-net-poll", cb, row)
470    def on_stats_enable_memory_poll_changed(self, cb, row=None):
471        return self.conf.notify_add("/stats/enable-memory-poll", cb, row)
472
473    def get_console_scaling(self):
474        return self.conf.get("/console/scaling")
475    def set_console_scaling(self, pref):
476        self.conf.set("/console/scaling", pref)
477
478    def get_console_resizeguest(self):
479        val = self.conf.get("/console/resize-guest")
480        if val == -1:
481            val = self.default_console_resizeguest
482        return val
483    def set_console_resizeguest(self, pref):
484        self.conf.set("/console/resize-guest", pref)
485
486    def get_auto_usbredir(self):
487        return bool(self.conf.get("/console/auto-redirect"))
488    def set_auto_usbredir(self, state):
489        self.conf.set("/console/auto-redirect", state)
490
491    def get_console_autoconnect(self):
492        return bool(self.conf.get("/console/autoconnect"))
493    def set_console_autoconnect(self, val):
494        return self.conf.set("/console/autoconnect", val)
495
496    # Show VM details toolbar
497    def get_details_show_toolbar(self):
498        res = self.conf.get("/details/show-toolbar")
499        if res is None:
500            res = True  # pragma: no cover
501        return res
502    def set_details_show_toolbar(self, state):
503        self.conf.set("/details/show-toolbar", state)
504
505    # New VM preferences
506    def get_graphics_type(self, raw=False):
507        ret = self.conf.get("/new-vm/graphics-type")
508        if ret not in ["system", "vnc", "spice"]:
509            ret = "system"  # pragma: no cover
510        if ret == "system" and not raw:
511            return self.default_graphics_from_config
512        return ret
513    def set_graphics_type(self, gtype):
514        self.conf.set("/new-vm/graphics-type", gtype.lower())
515
516    def get_default_storage_format(self, raw=False):
517        ret = self.conf.get("/new-vm/storage-format")
518        if ret not in ["default", "raw", "qcow2"]:
519            ret = "default"  # pragma: no cover
520        if ret == "default" and not raw:
521            return self.default_storage_format_from_config
522        return ret
523    def set_storage_format(self, typ):
524        self.conf.set("/new-vm/storage-format", typ.lower())
525
526    def get_default_cpu_setting(self):
527        ret = self.conf.get("/new-vm/cpu-default")
528
529        if ret not in DomainCpu.SPECIAL_MODES:
530            ret = DomainCpu.SPECIAL_MODE_APP_DEFAULT  # pragma: no cover
531        return ret
532    def set_default_cpu_setting(self, val):
533        self.conf.set("/new-vm/cpu-default", val.lower())
534
535
536    # URL/Media path history
537    def _url_add_helper(self, gsettings_path, url):
538        maxlength = 10
539        urls = self.conf.get(gsettings_path) or []
540
541        if urls.count(url) == 0 and len(url) > 0 and not url.isspace():
542            # The url isn't already in the list, so add it
543            urls.insert(0, url)
544            if len(urls) > maxlength:
545                del urls[len(urls) - 1]  # pragma: no cover
546            self.conf.set(gsettings_path, urls)
547
548    def add_container_url(self, url):
549        self._url_add_helper("/urls/containers", url)
550    def get_container_urls(self):
551        return self.conf.get("/urls/containers") or []
552
553    def add_media_url(self, url):
554        self._url_add_helper("/urls/urls", url)
555    def get_media_urls(self):
556        return self.conf.get("/urls/urls") or []
557
558    def add_iso_path(self, path):
559        self._url_add_helper("/urls/isos", path)
560    def get_iso_paths(self):
561        return self.conf.get("/urls/isos") or []
562    def on_iso_paths_changed(self, cb):
563        return self.conf.notify_add("/urls/isos", cb)
564
565
566    # Whether to ask about fixing path permissions
567    def add_perms_fix_ignore(self, pathlist):
568        current_list = self.get_perms_fix_ignore() or []
569        for path in pathlist:
570            if path in current_list:
571                continue  # pragma: no cover
572            current_list.append(path)
573        self.conf.set("/paths/perms-fix-ignore", current_list)
574    def get_perms_fix_ignore(self):
575        return self.conf.get("/paths/perms-fix-ignore")
576
577
578    # Manager view connection list
579    def get_conn_uris(self):
580        return self.conf.get("/connections/uris") or []
581    def add_conn_uri(self, uri):
582        uris = self.get_conn_uris()
583        if uri not in uris:
584            uris.insert(len(uris) - 1, uri)
585            self.conf.set("/connections/uris", uris)
586    def remove_conn_uri(self, uri):
587        uris = self.get_conn_uris()
588        if uri in uris:
589            uris.remove(uri)
590            self.conf.set("/connections/uris", uris)
591
592        if self.get_conn_autoconnect(uri):
593            uris = self.conf.get("/connections/autoconnect")
594            uris.remove(uri)
595            self.conf.set("/connections/autoconnect", uris)
596
597    # Manager default window size
598    def get_manager_window_size(self):
599        w = self.conf.get("/manager-window-width")
600        h = self.conf.get("/manager-window-height")
601        return (w, h)
602    def set_manager_window_size(self, w, h):
603        self.conf.set("/manager-window-width", w)
604        self.conf.set("/manager-window-height", h)
605
606    # URI autoconnect
607    def get_conn_autoconnect(self, uri):
608        uris = self.conf.get("/connections/autoconnect")
609        return ((uris is not None) and (uri in uris))
610
611    def set_conn_autoconnect(self, uri, val):
612        uris = self.conf.get("/connections/autoconnect") or []
613        if not val and uri in uris:
614            uris.remove(uri)
615        elif val and uri not in uris:
616            uris.append(uri)
617
618        self.conf.set("/connections/autoconnect", uris)
619
620
621    # Default directory location dealings
622    def get_default_directory(self, conn, _type):
623        ignore = conn
624        browsedata = self.browse_reason_data.get(_type, {})
625        key = browsedata.get("gsettings_key", None)
626        path = None
627
628        if key:
629            path = self.conf.get("/paths/%s-default" % key)
630
631        log.debug("directory for type=%s returning=%s", _type, path)
632        return path
633
634    def set_default_directory(self, folder, _type):
635        browsedata = self.browse_reason_data.get(_type, {})
636        key = browsedata.get("gsettings_key", None)
637        if not key:
638            return  # pragma: no cover
639
640        log.debug("saving directory for type=%s to %s", key, folder)
641        self.conf.set("/paths/%s-default" % key, folder)
642