1# Copyright (C) 2006-2008, 2015 Red Hat, Inc. 2# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com> 3# Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@redhat.com> 4# 5# This work is licensed under the GNU GPLv2 or later. 6# See the COPYING file in the top-level directory. 7 8from gi.repository import Gdk 9from gi.repository import GObject 10 11import gi 12gi.require_version('GtkVnc', '2.0') 13from gi.repository import GtkVnc 14try: 15 gi.require_version('SpiceClientGtk', '3.0') 16 from gi.repository import SpiceClientGtk 17 from gi.repository import SpiceClientGLib 18 have_spice_gtk = True 19except (ValueError, ImportError): # pragma: no cover 20 have_spice_gtk = False 21 22from virtinst import log 23 24from .sshtunnels import SSHTunnels 25from ..baseclass import vmmGObject 26 27 28################################## 29# VNC/Spice abstraction handling # 30################################## 31 32class Viewer(vmmGObject): 33 """ 34 Base class for viewer abstraction 35 """ 36 __gsignals__ = { 37 "add-display-widget": (vmmGObject.RUN_FIRST, None, [object]), 38 "size-allocate": (vmmGObject.RUN_FIRST, None, [object]), 39 "pointer-grab": (vmmGObject.RUN_FIRST, None, []), 40 "pointer-ungrab": (vmmGObject.RUN_FIRST, None, []), 41 "keyboard-grab": (vmmGObject.RUN_FIRST, None, []), 42 "keyboard-ungrab": (vmmGObject.RUN_FIRST, None, []), 43 "connected": (vmmGObject.RUN_FIRST, None, []), 44 "disconnected": (vmmGObject.RUN_FIRST, None, [str, str]), 45 "auth-error": (vmmGObject.RUN_FIRST, None, [str, bool]), 46 "need-auth": (vmmGObject.RUN_FIRST, None, [bool, bool]), 47 "agent-connected": (vmmGObject.RUN_FIRST, None, []), 48 "usb-redirect-error": (vmmGObject.RUN_FIRST, None, [str]), 49 } 50 51 def __init__(self, vm, ginfo): 52 vmmGObject.__init__(self) 53 self._display = None 54 self._vm = vm 55 self._ginfo = ginfo 56 self._tunnels = SSHTunnels(self._ginfo) 57 self._keyboard_grab = False 58 59 self.add_gsettings_handle( 60 self.config.on_keys_combination_changed(self._refresh_grab_keys)) 61 62 self.connect("add-display-widget", self._common_init) 63 64 def _cleanup(self): 65 self.close() 66 67 if self._display: 68 self._display.destroy() 69 self._display = None 70 self._vm = None 71 72 self._tunnels.close_all() 73 74 75 ######################## 76 # Internal helper APIs # 77 ######################## 78 79 def _make_signal_proxy(self, new_signal): 80 """ 81 Helper for redirecting a signal from self._display out 82 through the viewer 83 """ 84 def _proxy_signal(src, *args, **kwargs): 85 ignore = src 86 self.emit(new_signal, *args, **kwargs) 87 return _proxy_signal 88 89 def _common_init(self, ignore1, ignore2): 90 self._refresh_grab_keys() 91 92 self._display.connect("size-allocate", 93 self._make_signal_proxy("size-allocate")) 94 95 96 97 ######################### 98 # Generic internal APIs # 99 ######################### 100 101 def _grab_focus(self): 102 if self._display: 103 self._display.grab_focus() 104 def _set_size_request(self, *args, **kwargs): 105 return self._display.set_size_request(*args, **kwargs) 106 def _size_allocate(self, *args, **kwargs): 107 return self._display.size_allocate(*args, **kwargs) 108 109 def _get_pixbuf(self): 110 return self._display.get_pixbuf() 111 112 def _get_fd_for_open(self): 113 if self._ginfo.need_tunnel(): 114 return self._tunnels.open_new() 115 116 if self._vm.conn.is_remote(): 117 # OpenGraphics only works for local libvirtd connections 118 return None 119 120 if self._ginfo.gtlsport and not self._ginfo.gport: 121 # This makes spice loop requesting an fd. Disable until spice is 122 # fixed: https://bugzilla.redhat.com/show_bug.cgi?id=1334071 123 return None # pragma: no cover 124 125 if not self._vm.conn.support.domain_open_graphics(): 126 return None 127 128 return self._vm.open_graphics_fd() 129 130 def _open(self): 131 if self._ginfo.bad_config(): 132 raise RuntimeError(self._ginfo.bad_config()) 133 134 fd = self._get_fd_for_open() 135 if fd is not None: 136 self._open_fd(fd) 137 else: 138 self._open_host() 139 140 def _get_grab_keys(self): 141 return self._display.get_grab_keys().as_string() 142 143 def _emit_disconnected(self, errdetails=None): 144 ssherr = self._tunnels.get_err_output() 145 self.emit("disconnected", errdetails, ssherr) 146 147 148 ####################################################### 149 # Internal API that will be overwritten by subclasses # 150 ####################################################### 151 152 def close(self): 153 raise NotImplementedError() 154 155 def _is_open(self): 156 raise NotImplementedError() 157 158 def _set_username(self, cred): 159 raise NotImplementedError() 160 def _set_password(self, cred): 161 raise NotImplementedError() 162 163 def _send_keys(self, keys): 164 raise NotImplementedError() 165 166 def _refresh_grab_keys(self): 167 raise NotImplementedError() 168 169 def _open_host(self): 170 raise NotImplementedError() 171 def _open_fd(self, fd): 172 raise NotImplementedError() 173 174 def _get_desktop_resolution(self): 175 raise NotImplementedError() 176 177 def _get_scaling(self): 178 raise NotImplementedError() 179 def _set_scaling(self, scaling): 180 raise NotImplementedError() 181 182 def _set_resizeguest(self, val): 183 raise NotImplementedError() 184 def _get_resizeguest(self): 185 raise NotImplementedError() 186 187 def _get_usb_widget(self): 188 raise NotImplementedError() 189 def _has_usb_redirection(self): 190 raise NotImplementedError() 191 def _has_agent(self): 192 raise NotImplementedError() 193 194 195 #################################### 196 # APIs accessed by vmmConsolePages # 197 #################################### 198 199 def console_is_open(self): 200 return self._is_open() 201 202 def console_grab_focus(self): 203 return self._grab_focus() 204 def console_has_keyboard_grab(self): 205 return bool(self._display and self._keyboard_grab) 206 def console_set_size_request(self, *args, **kwargs): 207 return self._set_size_request(*args, **kwargs) 208 def console_size_allocate(self, *args, **kwargs): 209 return self._size_allocate(*args, **kwargs) 210 211 def console_get_pixbuf(self): 212 return self._get_pixbuf() 213 214 def console_open(self): 215 return self._open() 216 217 def console_set_password(self, val): 218 return self._set_password(val) 219 def console_set_username(self, val): 220 return self._set_username(val) 221 222 def console_send_keys(self, keys): 223 return self._send_keys(keys) 224 225 def console_get_grab_keys(self): 226 return self._get_grab_keys() 227 228 def console_get_desktop_resolution(self): 229 ret = self._get_desktop_resolution() 230 if not ret: 231 return ret # pragma: no cover 232 233 # Don't pass on bogus resolutions 234 if (ret[0] == 0) or (ret[1] == 0): 235 return None 236 return ret 237 238 def console_get_scaling(self): 239 return self._get_scaling() 240 def console_set_scaling(self, val): 241 return self._set_scaling(val) 242 243 def console_get_resizeguest(self): 244 return self._get_resizeguest() 245 def console_set_resizeguest(self, val): 246 return self._set_resizeguest(val) 247 248 def console_get_usb_widget(self): 249 return self._get_usb_widget() 250 def console_has_usb_redirection(self): 251 return self._has_usb_redirection() 252 def console_has_agent(self): 253 return self._has_agent() 254 255 def console_remove_display_from_widget(self, widget): 256 if self._display and self._display in widget.get_children(): 257 widget.remove(self._display) 258 259 260#################### 261# VNC viewer class # 262#################### 263 264class VNCViewer(Viewer): 265 viewer_type = "vnc" 266 267 def __init__(self, *args, **kwargs): 268 Viewer.__init__(self, *args, **kwargs) 269 self._display = None 270 self._desktop_resolution = None 271 272 273 ################### 274 # Private helpers # 275 ################### 276 277 def _init_widget(self): 278 self._display = GtkVnc.Display() 279 280 # Make sure viewer doesn't force resize itself 281 self._display.set_force_size(False) 282 self._display.set_pointer_grab(True) 283 284 self.emit("add-display-widget", self._display) 285 self._display.realize() 286 287 self._display.connect("vnc-pointer-grab", 288 self._make_signal_proxy("pointer-grab")) 289 self._display.connect("vnc-pointer-ungrab", 290 self._make_signal_proxy("pointer-ungrab")) 291 self._display.connect("vnc-keyboard-grab", self._keyboard_grab_cb) 292 self._display.connect("vnc-keyboard-ungrab", self._keyboard_ungrab_cb) 293 294 self._display.connect("vnc-auth-credential", self._auth_credential) 295 self._display.connect("vnc-auth-failure", self._auth_failure_cb) 296 self._display.connect("vnc-initialized", self._connected_cb) 297 self._display.connect("vnc-disconnected", self._disconnected_cb) 298 self._display.connect("vnc-desktop-resize", self._desktop_resize) 299 300 self._display.show() 301 302 def _keyboard_grab_cb(self, src): 303 self._keyboard_grab = True 304 self.emit("keyboard-grab") 305 306 def _keyboard_ungrab_cb(self, src): 307 self._keyboard_grab = False 308 self.emit("keyboard-ungrab") 309 310 def _connected_cb(self, ignore): 311 self._tunnels.unlock() 312 self.emit("connected") 313 314 def _disconnected_cb(self, ignore): 315 self._tunnels.unlock() 316 self._emit_disconnected() 317 318 def _desktop_resize(self, src_ignore, w, h): 319 self._desktop_resolution = (w, h) 320 # Queue a resize 321 self.emit("size-allocate", None) 322 323 def _auth_failure_cb(self, ignore, msg): 324 log.debug("VNC auth failure. msg=%s", msg) 325 self.emit("auth-error", msg, True) 326 327 def _auth_credential(self, src_ignore, credList): 328 values = [] 329 for idx in range(int(credList.n_values)): 330 values.append(credList.get_nth(idx)) 331 332 if self.config.CLITestOptions.fake_vnc_username: 333 values.append(GtkVnc.DisplayCredential.USERNAME) 334 335 withUsername = False 336 withPassword = False 337 for cred in values: 338 log.debug("Got credential request %s", cred) 339 if cred == GtkVnc.DisplayCredential.PASSWORD: 340 withPassword = True 341 elif cred == GtkVnc.DisplayCredential.USERNAME: 342 withUsername = True 343 elif cred == GtkVnc.DisplayCredential.CLIENTNAME: # pragma: no cover 344 self._display.set_credential(cred, "libvirt-vnc") 345 else: # pragma: no cover 346 errmsg = ( 347 _("Unable to provide requested credentials to the VNC server.\n" 348 "The credential type %s is not supported") % 349 str(cred.value_name)) 350 self.emit("auth-error", errmsg, True) 351 return 352 353 if withUsername or withPassword: 354 self.emit("need-auth", withPassword, withUsername) 355 356 357 ############################### 358 # Private API implementations # 359 ############################### 360 361 def close(self): 362 self._display.close() 363 364 def _is_open(self): 365 return self._display.is_open() 366 367 def _get_scaling(self): 368 if self._display: 369 return self._display.get_scaling() 370 def _set_scaling(self, scaling): 371 if self._display: 372 return self._display.set_scaling(scaling) 373 374 def _get_grab_keys(self): 375 return self._display.get_grab_keys().as_string() 376 377 def _refresh_grab_keys(self): 378 if not self._display: 379 return # pragma: no cover 380 381 try: 382 keys = self.config.get_keys_combination() 383 if not keys: 384 return # pragma: no cover 385 386 try: 387 keys = [int(k) for k in keys.split(',')] 388 except Exception: # pragma: no cover 389 log.debug("Error in grab_keys configuration in Gsettings", 390 exc_info=True) 391 return 392 393 seq = GtkVnc.GrabSequence.new(keys) 394 self._display.set_grab_keys(seq) 395 except Exception as e: # pragma: no cover 396 log.debug("Error when getting the grab keys combination: %s", 397 str(e)) 398 399 def _send_keys(self, keys): 400 return self._display.send_keys([Gdk.keyval_from_name(k) for k in keys]) 401 402 def _get_desktop_resolution(self): 403 return self._desktop_resolution 404 405 def _set_username(self, cred): 406 self._display.set_credential(GtkVnc.DisplayCredential.USERNAME, cred) 407 def _set_password(self, cred): 408 self._display.set_credential(GtkVnc.DisplayCredential.PASSWORD, cred) 409 410 def _set_resizeguest(self, val): 411 ignore = val 412 def _get_resizeguest(self): 413 return False 414 415 def _get_usb_widget(self): 416 return None # pragma: no cover 417 def _has_usb_redirection(self): 418 return False 419 def _has_agent(self): 420 return False # pragma: no cover 421 422 423 ####################### 424 # Connection routines # 425 ####################### 426 427 def _open(self): 428 self._init_widget() 429 return Viewer._open(self) 430 431 def _open_host(self): 432 host, port, ignore = self._ginfo.get_conn_host() 433 log.debug("VNC connecting to host=%s port=%s", host, port) 434 self._display.open_host(host, port) 435 436 def _open_fd(self, fd): 437 self._display.open_fd(fd) 438 439 440###################### 441# Spice viewer class # 442###################### 443 444class _SignalTracker: 445 # Helper class to more conveniently connect and disconnect signals 446 # from spice objects. This ensures we don't leave circular references 447 # at object destroy time 448 def __init__(self): 449 self._sigmap = {} 450 451 def _add_hid(self, obj, hid): 452 if obj not in self._sigmap: 453 self._sigmap[obj] = [] 454 self._sigmap[obj].append(hid) 455 456 def connect(self, obj, name, handler, *args): 457 hid = GObject.GObject.connect(obj, name, handler, *args) 458 self._add_hid(obj, hid) 459 460 def connect_after(self, obj, name, handler, *args): 461 hid = GObject.GObject.connect_after(obj, name, handler, *args) 462 self._add_hid(obj, hid) 463 464 def disconnect_obj_signals(self, obj): 465 for hid in self._sigmap.get(obj, []): 466 GObject.GObject.disconnect(obj, hid) 467 468 469_SIGS = _SignalTracker() 470 471 472class SpiceViewer(Viewer): 473 viewer_type = "spice" 474 475 def __init__(self, *args, **kwargs): 476 Viewer.__init__(self, *args, **kwargs) 477 self._spice_session = None 478 self._display = None 479 self._audio = None 480 self._main_channel = None 481 self._display_channel = None 482 self._usbdev_manager = None 483 self._channels = set() 484 485 486 ################### 487 # Private helpers # 488 ################### 489 490 def _mouse_grab_cb(self, src, grab): 491 if grab: 492 self.emit("pointer-grab") 493 else: 494 self.emit("pointer-ungrab") 495 496 def _keyboard_grab_cb(self, src, grab): 497 self._keyboard_grab = grab 498 if grab: 499 self.emit("keyboard-grab") 500 else: 501 self.emit("keyboard-ungrab") 502 503 def _init_widget(self): 504 self.emit("add-display-widget", self._display) 505 self._display.realize() 506 507 self._display.connect("mouse-grab", self._mouse_grab_cb) 508 self._display.connect("keyboard-grab", self._keyboard_grab_cb) 509 510 self._display.show() 511 512 def _create_spice_session(self): 513 self._spice_session = SpiceClientGLib.Session() 514 SpiceClientGLib.set_session_option(self._spice_session) 515 gtk_session = SpiceClientGtk.GtkSession.get(self._spice_session) 516 gtk_session.set_property("auto-clipboard", True) 517 518 _SIGS.connect(self._spice_session, "channel-new", self._channel_new_cb) 519 520 # Distros might have usb redirection compiled out, like OpenBSD 521 # https://bugzilla.redhat.com/show_bug.cgi?id=1348479 522 try: 523 self._usbdev_manager = SpiceClientGLib.UsbDeviceManager.get( 524 self._spice_session) 525 _SIGS.connect( 526 self._usbdev_manager, "auto-connect-failed", 527 self._usbdev_redirect_error) 528 _SIGS.connect( 529 self._usbdev_manager, "device-error", 530 self._usbdev_redirect_error) 531 532 autoredir = self.config.get_auto_usbredir() 533 if autoredir: 534 gtk_session.set_property("auto-usbredir", True) 535 except Exception: # pragma: no cover 536 self._usbdev_manager = None 537 log.debug("Error initializing spice usb device manager", 538 exc_info=True) 539 540 541 ##################### 542 # Channel listeners # 543 ##################### 544 545 def _main_channel_event_cb(self, channel, event): 546 self._tunnels.unlock() 547 548 if event == SpiceClientGLib.ChannelEvent.CLOSED: 549 self._emit_disconnected() 550 elif event == SpiceClientGLib.ChannelEvent.ERROR_AUTH: 551 if not self._spice_session.get_property("password"): 552 log.debug("Spice channel received ERROR_AUTH, but no " 553 "password set, assuming it wants credentials.") 554 self.emit("need-auth", True, False) 555 else: 556 log.debug("Spice channel received ERROR_AUTH, but a " 557 "password is already set. Assuming authentication failed.") 558 self.emit("auth-error", channel.get_error().message, False) 559 elif "ERROR" in str(event): 560 # SpiceClientGLib.ChannelEvent.ERROR_CONNECT 561 # SpiceClientGLib.ChannelEvent.ERROR_IO 562 # SpiceClientGLib.ChannelEvent.ERROR_LINK 563 # SpiceClientGLib.ChannelEvent.ERROR_TLS 564 error = None 565 if channel.get_error(): 566 error = channel.get_error().message 567 log.debug("Spice channel event=%s message=%s", event, error) 568 569 msg = _("Encountered SPICE %(error-name)s") % { 570 "error-name": event.value_nick} 571 if error: 572 msg += ": %s" % error 573 self._emit_disconnected(msg) 574 575 def _fd_channel_event_cb(self, channel, event): 576 # When we see any event from the channel, release the 577 # associated tunnel lock 578 channel.disconnect_by_func(self._fd_channel_event_cb) 579 self._tunnels.unlock() 580 581 def _channel_open_fd_request(self, channel, tls_ignore): 582 if not self._tunnels: 583 # Can happen if we close the details window and clear self._tunnels 584 # while initially connecting to spice and channel FD requests 585 # are still rolling in 586 return # pragma: no cover 587 588 log.debug("Requesting fd for channel: %s", channel) 589 channel.connect_after("channel-event", self._fd_channel_event_cb) 590 591 fd = self._get_fd_for_open() 592 channel.open_fd(fd) 593 594 def _channel_new_cb(self, session, channel): 595 self._channels.add(channel) 596 _SIGS.connect(channel, "open-fd", self._channel_open_fd_request) 597 598 if (isinstance(channel, SpiceClientGLib.MainChannel) and 599 not self._main_channel): 600 self._main_channel = channel 601 _SIGS.connect_after( 602 self._main_channel, "channel-event", 603 self._main_channel_event_cb) 604 _SIGS.connect_after( 605 self._main_channel, "notify::agent-connected", 606 self._agent_connected_cb) 607 608 elif (type(channel) == SpiceClientGLib.DisplayChannel and 609 not self._display): 610 channel_id = channel.get_property("channel-id") 611 612 if channel_id != 0: # pragma: no cover 613 log.debug("Spice multi-head unsupported") 614 return 615 616 self._display_channel = channel 617 self._display = SpiceClientGtk.Display.new(self._spice_session, 618 channel_id) 619 self._init_widget() 620 self.emit("connected") 621 622 elif (type(channel) in [SpiceClientGLib.PlaybackChannel, 623 SpiceClientGLib.RecordChannel] and 624 not self._audio): 625 # It's unclear why we need this audio handle, but it 626 # does matter: 627 # https://bugzilla.redhat.com/show_bug.cgi?id=1881080 628 self._audio = SpiceClientGLib.Audio.get(self._spice_session, None) 629 630 def _agent_connected_cb(self, src, val): 631 self.emit("agent-connected") # pragma: no cover 632 633 634 ################################ 635 # Internal API implementations # 636 ################################ 637 638 def close(self): 639 if self._spice_session is not None: 640 _SIGS.disconnect_obj_signals(self._spice_session) 641 self._spice_session.disconnect() 642 self._spice_session = None 643 self._audio = None 644 if self._display: 645 self._display.destroy() 646 self._display = None 647 self._display_channel = None 648 649 for channel in self._channels: 650 _SIGS.disconnect_obj_signals(channel) 651 self._channels = None 652 653 self._main_channel = None 654 655 _SIGS.disconnect_obj_signals(self._usbdev_manager) 656 self._usbdev_manager = None 657 658 def _is_open(self): 659 return self._spice_session is not None 660 661 def _refresh_grab_keys(self): 662 if not self._display: 663 return # pragma: no cover 664 665 try: 666 keys = self.config.get_keys_combination() 667 if not keys: 668 return # pragma: no cover 669 670 try: 671 keys = [int(k) for k in keys.split(',')] 672 except Exception: # pragma: no cover 673 log.debug("Error in grab_keys configuration in Gsettings", 674 exc_info=True) 675 return 676 677 seq = SpiceClientGtk.GrabSequence.new(keys) 678 self._display.set_grab_keys(seq) 679 except Exception as e: # pragma: no cover 680 log.debug("Error when getting the grab keys combination: %s", 681 str(e)) 682 683 def _send_keys(self, keys): 684 return self._display.send_keys([Gdk.keyval_from_name(k) for k in keys], 685 SpiceClientGtk.DisplayKeyEvent.CLICK) 686 687 def _get_desktop_resolution(self): 688 if not self._display_channel: 689 return None # pragma: no cover 690 return self._display_channel.get_properties("width", "height") 691 692 def _has_agent(self): 693 if not self._main_channel: 694 return False # pragma: no cover 695 return (self._main_channel.get_property("agent-connected") or 696 self.config.CLITestOptions.spice_agent) 697 698 def _open_host(self): 699 host, port, tlsport = self._ginfo.get_conn_host() 700 self._create_spice_session() 701 702 log.debug("Spice connecting to host=%s port=%s tlsport=%s", 703 host, port, tlsport) 704 self._spice_session.set_property("host", str(host)) 705 if port: 706 self._spice_session.set_property("port", str(port)) 707 if tlsport: 708 self._spice_session.set_property("tls-port", str(tlsport)) 709 710 self._spice_session.connect() 711 712 def _open_fd(self, fd): 713 self._create_spice_session() 714 self._spice_session.open_fd(fd) 715 716 def _set_username(self, cred): 717 ignore = cred # pragma: no cover 718 def _set_password(self, cred): 719 self._spice_session.set_property("password", cred) 720 fd = self._get_fd_for_open() 721 if fd is not None: 722 self._spice_session.open_fd(fd) 723 else: 724 self._spice_session.connect() # pragma: no cover 725 726 def _get_scaling(self): 727 if self._display: 728 return self._display.get_property("scaling") 729 def _set_scaling(self, scaling): 730 if self._display: 731 self._display.set_property("scaling", scaling) 732 733 def _set_resizeguest(self, val): 734 if self._display: 735 self._display.set_property("resize-guest", val) 736 737 def _get_resizeguest(self): 738 if self._display: 739 return self._display.get_property("resize-guest") 740 return False # pragma: no cover 741 742 def _usbdev_redirect_error(self, spice_usbdev_widget, spice_usb_device, 743 errstr): # pragma: no cover 744 ignore = spice_usbdev_widget 745 ignore = spice_usb_device 746 self.emit("usb-redirect-error", errstr) 747 748 def _get_usb_widget(self): 749 if not self._spice_session: 750 return # pragma: no cover 751 752 usbwidget = SpiceClientGtk.UsbDeviceWidget.new(self._spice_session, 753 None) 754 usbwidget.connect("connect-failed", self._usbdev_redirect_error) 755 return usbwidget 756 757 def _has_usb_redirection(self): 758 if not self._spice_session or not self._usbdev_manager: 759 return False # pragma: no cover 760 761 for c in self._spice_session.get_channels(): 762 if c.__class__ is SpiceClientGLib.UsbredirChannel: 763 return True 764 return False 765