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