Package x2go :: Package backends :: Package control :: Module _stdout
[frames] | no frames]

Source Code for Module x2go.backends.control._stdout

   1  # -*- coding: utf-8 -*- 
   2   
   3  # Copyright (C) 2010-2012 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de> 
   4  # 
   5  # Python X2Go is free software; you can redistribute it and/or modify 
   6  # it under the terms of the GNU Affero General Public License as published by 
   7  # the Free Software Foundation; either version 3 of the License, or 
   8  # (at your option) any later version. 
   9  # 
  10  # Python X2Go is distributed in the hope that it will be useful, 
  11  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  13  # GNU Affero General Public License for more details. 
  14  # 
  15  # You should have received a copy of the GNU Affero General Public License 
  16  # along with this program; if not, write to the 
  17  # Free Software Foundation, Inc., 
  18  # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. 
  19   
  20  """\ 
  21  X2goControlSessionSTDOUT class - core functions for handling your individual X2Go sessions. 
  22   
  23  This backend handles X2Go server implementations that respond via server-side STDOUT. 
  24   
  25  """ 
  26  __NAME__ = 'x2gocontrolsession-pylib' 
  27   
  28  # modules 
  29  import os 
  30  import types 
  31  import paramiko 
  32  import gevent 
  33  import copy 
  34  import string 
  35  import random 
  36  import re 
  37  import locale 
  38  import threading 
  39  import cStringIO 
  40   
  41  from gevent import socket 
  42   
  43  # Python X2Go modules 
  44  import x2go.sshproxy as sshproxy 
  45  import x2go.log as log 
  46  import x2go.utils as utils 
  47  import x2go.x2go_exceptions as x2go_exceptions 
  48  import x2go.defaults as defaults 
  49  import x2go.checkhosts as checkhosts 
  50   
  51  from x2go.backends.terminal import X2goTerminalSession as _X2goTerminalSession 
  52  from x2go.backends.info import X2goServerSessionInfo as _X2goServerSessionInfo 
  53  from x2go.backends.info import X2goServerSessionList as _X2goServerSessionList 
  54  from x2go.backends.proxy import X2goProxy as _X2goProxy 
  55   
  56  import x2go._paramiko 
  57  x2go._paramiko.monkey_patch_paramiko() 
58 59 -def _rerewrite_blanks(cmd):
60 """\ 61 In command strings X2Go server scripts expect blanks being rewritten to ,,X2GO_SPACE_CHAR''. 62 Commands get rewritten in the terminal sessions. This re-rewrite function helps 63 displaying command string in log output. 64 65 @param cmd: command that has to be rewritten for log output 66 @type cmd: C{str} 67 68 @return: the command with ,,X2GO_SPACE_CHAR'' re-replaced by blanks 69 @rtype: C{str} 70 71 """ 72 # X2Go run command replace X2GO_SPACE_CHAR string with blanks 73 if cmd: 74 cmd = cmd.replace("X2GO_SPACE_CHAR", " ") 75 return cmd
76
77 -def _rewrite_password(cmd, user=None, password=None):
78 """\ 79 In command strings Python X2Go replaces some macros with actual values: 80 81 - X2GO_USER -> the user name under which the user is authenticated via SSH 82 - X2GO_PASSWORD -> the password being used for SSH authentication 83 84 Both macros can be used to on-the-fly authenticate via RDP. 85 86 @param cmd: command that is to be sent to an X2Go server script 87 @type cmd: C{str} 88 @param user: the SSH authenticated user name 89 @type password: the password being used for SSH authentication 90 91 @return: the command with macros replaced 92 @rtype: C{str} 93 94 """ 95 # if there is a ,,-u X2GO_USER'' parameter in RDP options then we will replace 96 # it by our X2Go session password 97 if cmd and user: 98 cmd = cmd.replace('X2GO_USER', user) 99 # if there is a ,,-p X2GO_PASSWORD'' parameter in RDP options then we will replace 100 # it by our X2Go session password 101 if cmd and password: 102 cmd = cmd.replace('X2GO_PASSWORD', password) 103 return cmd
104
105 106 -class X2goControlSessionSTDOUT(paramiko.SSHClient):
107 """\ 108 In the Python X2Go concept, X2Go sessions fall into two parts: a control session and one to many terminal sessions. 109 110 The control session handles the SSH based communication between server and client. It is mainly derived from 111 C{paramiko.SSHClient} and adds on X2Go related functionality. 112 113 """ 114 associated_terminals = None 115
116 - def __init__(self, 117 profile_name='UNKNOWN', 118 add_to_known_hosts=False, 119 known_hosts=None, 120 forward_sshagent=False, 121 terminal_backend=_X2goTerminalSession, 122 info_backend=_X2goServerSessionInfo, 123 list_backend=_X2goServerSessionList, 124 proxy_backend=_X2goProxy, 125 client_rootdir=os.path.join(defaults.LOCAL_HOME, defaults.X2GO_CLIENT_ROOTDIR), 126 sessions_rootdir=os.path.join(defaults.LOCAL_HOME, defaults.X2GO_SESSIONS_ROOTDIR), 127 ssh_rootdir=os.path.join(defaults.LOCAL_HOME, defaults.X2GO_SSH_ROOTDIR), 128 logger=None, loglevel=log.loglevel_DEFAULT, 129 published_applications_no_submenus=0, 130 **kwargs):
131 """\ 132 Initialize an X2Go control session. For each connected session profile there will be one SSH-based 133 control session and one to many terminal sessions that all server-client-communicate via this one common control 134 session. 135 136 A control session normally gets set up by an L{X2goSession} instance. Do not use it directly!!! 137 138 @param profile_name: the profile name of the session profile this control session works for 139 @type profile_name: C{str} 140 @param add_to_known_hosts: Auto-accept server host validity? 141 @type add_to_known_hosts: C{bool} 142 @param known_hosts: the underlying Paramiko/SSH systems C{known_hosts} file 143 @type known_hosts: C{str} 144 @param forward_sshagent: forward SSH agent authentication requests to the X2Go client-side 145 @type forward_sshagent: C{bool} 146 @param terminal_backend: X2Go terminal session backend to use 147 @type terminal_backend: C{class} 148 @param info_backend: backend for handling storage of server session information 149 @type info_backend: C{X2goServerSessionInfo*} instance 150 @param list_backend: backend for handling storage of session list information 151 @type list_backend: C{X2goServerSessionList*} instance 152 @param proxy_backend: backend for handling the X-proxy connections 153 @type proxy_backend: C{X2goProxy*} instance 154 @param client_rootdir: client base dir (default: ~/.x2goclient) 155 @type client_rootdir: C{str} 156 @param sessions_rootdir: sessions base dir (default: ~/.x2go) 157 @type sessions_rootdir: C{str} 158 @param ssh_rootdir: ssh base dir (default: ~/.ssh) 159 @type ssh_rootdir: C{str} 160 @param published_applications_no_submenus: published applications menus with less items than C{published_applications_no_submenus} 161 are rendered without submenus 162 @type published_applications_no_submenus: C{int} 163 @param logger: you can pass an L{X2goLogger} object to the 164 L{X2goControlSessionSTDOUT} constructor 165 @type logger: L{X2goLogger} instance 166 @param loglevel: if no L{X2goLogger} object has been supplied a new one will be 167 constructed with the given loglevel 168 @type loglevel: C{int} 169 @param kwargs: catch any non-defined parameters in C{kwargs} 170 @type kwargs: C{dict} 171 172 """ 173 self.associated_terminals = {} 174 self.terminated_terminals = [] 175 176 self.profile_name = profile_name 177 self.add_to_known_hosts = add_to_known_hosts 178 self.known_hosts = known_hosts 179 self.forward_sshagent = forward_sshagent 180 181 self.hostname = None 182 self.port = None 183 184 self.sshproxy_session = None 185 186 self._session_auth_rsakey = None 187 self._remote_home = None 188 self._remote_group = {} 189 self._remote_username = None 190 self._remote_peername = None 191 192 self._server_features = None 193 194 if logger is None: 195 self.logger = log.X2goLogger(loglevel=loglevel) 196 else: 197 self.logger = copy.deepcopy(logger) 198 self.logger.tag = __NAME__ 199 200 self._terminal_backend = terminal_backend 201 self._info_backend = info_backend 202 self._list_backend = list_backend 203 self._proxy_backend = proxy_backend 204 205 self.client_rootdir = client_rootdir 206 self.sessions_rootdir = sessions_rootdir 207 self.ssh_rootdir = ssh_rootdir 208 209 self._published_applications_menu = {} 210 211 self.agent_chan = None 212 self.agent_handler = None 213 214 paramiko.SSHClient.__init__(self) 215 if self.add_to_known_hosts: 216 self.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 217 218 self.session_died = False 219 220 self.published_applications_no_submenus = published_applications_no_submenus 221 self._already_querying_published_applications = threading.Lock() 222 223 self._transport_lock = threading.Lock()
224
225 - def get_hostname(self):
226 """\ 227 Get the hostname as stored in the properties of this control session. 228 229 @return: the hostname of the connected X2Go server 230 @rtype: C{str} 231 232 """ 233 return self.hostname
234
235 - def get_port(self):
236 """\ 237 Get the port number of the SSH connection as stored in the properties of this control session. 238 239 @return: the server-side port number of the control session's SSH connection 240 @rtype: C{str} 241 242 """ 243 return self.port
244
245 - def load_session_host_keys(self):
246 """\ 247 Load known SSH host keys from the C{known_hosts} file. 248 249 If the file does not exist, create it first. 250 251 """ 252 if self.known_hosts is not None: 253 utils.touch_file(self.known_hosts) 254 self.load_host_keys(self.known_hosts)
255
256 - def __del__(self):
257 """\ 258 On instance descruction, do a proper session disconnect from the server. 259 260 """ 261 self.disconnect()
262
263 - def _x2go_sftp_put(self, local_path, remote_path):
264 """ 265 Put a local file on the remote server via sFTP. 266 267 During sFTP operations, remote command execution gets blocked. 268 269 @param local_path: full local path name of the file to be put on the server 270 @type local_path: C{str} 271 @param remote_path: full remote path name of the server-side target location, path names have to be Unix-compliant 272 @type remote_path: C{str} 273 274 @raise X2goControlSessionException: if the SSH connection dropped out 275 276 """ 277 ssh_transport = self.get_transport() 278 self._transport_lock.acquire() 279 if ssh_transport and ssh_transport.is_authenticated(): 280 self.logger('sFTP-put: %s -> %s:%s' % (os.path.normpath(local_path), self.remote_peername(), remote_path), loglevel=log.loglevel_DEBUG) 281 self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport) 282 try: 283 self.sftp_client.put(os.path.normpath(local_path), remote_path) 284 except (x2go_exceptions.SSHException, socket.error, IOError): 285 # react to connection dropped error for SSH connections 286 self.session_died = True 287 self._transport_lock.release() 288 raise x2go_exceptions.X2goControlSessionException('The SSH connection was dropped during an sFTP put action.') 289 self.sftp_client = None 290 self._transport_lock.release()
291
292 - def _x2go_sftp_write(self, remote_path, content):
293 """ 294 Create a text file on the remote server via sFTP. 295 296 During sFTP operations, remote command execution gets blocked. 297 298 @param remote_path: full remote path name of the server-side target location, path names have to be Unix-compliant 299 @type remote_path: C{str} 300 @param content: a text file, multi-line files use Unix-link EOL style 301 @type content: C{str} 302 303 @raise X2goControlSessionException: if the SSH connection dropped out 304 305 """ 306 ssh_transport = self.get_transport() 307 self._transport_lock.acquire() 308 if ssh_transport and ssh_transport.is_authenticated(): 309 self.logger('sFTP-write: opening remote file %s on host %s for writing' % (remote_path, self.remote_peername()), loglevel=log.loglevel_DEBUG) 310 self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport) 311 try: 312 remote_fileobj = self.sftp_client.open(remote_path, 'w') 313 self.logger('sFTP-write: writing content: %s' % content, loglevel=log.loglevel_DEBUG_SFTPXFER) 314 remote_fileobj.write(content) 315 remote_fileobj.close() 316 except (x2go_exceptions.SSHException, socket.error, IOError): 317 self.session_died = True 318 self._transport_lock.release() 319 self.logger('sFTP-write: opening remote file %s on host %s failed' % (remote_path, self.remote_peername()), loglevel=log.loglevel_WARN) 320 raise x2go_exceptions.X2goControlSessionException('The SSH connection was dropped during an sFTP write action.') 321 self.sftp_client = None 322 self._transport_lock.release()
323
324 - def _x2go_sftp_remove(self, remote_path):
325 """ 326 Remote a remote file from the server via sFTP. 327 328 During sFTP operations, remote command execution gets blocked. 329 330 @param remote_path: full remote path name of the server-side file to be removed, path names have to be Unix-compliant 331 @type remote_path: C{str} 332 333 @raise X2goControlSessionException: if the SSH connection dropped out 334 335 """ 336 ssh_transport = self.get_transport() 337 self._transport_lock.acquire() 338 if ssh_transport and ssh_transport.is_authenticated(): 339 self.logger('sFTP-write: removing remote file %s on host %s' % (remote_path, self.remote_peername()), loglevel=log.loglevel_DEBUG) 340 self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport) 341 try: 342 self.sftp_client.remove(remote_path) 343 except (x2go_exceptions.SSHException, socket.error, IOError): 344 self.session_died = True 345 self._transport_lock.release() 346 self.logger('sFTP-write: removing remote file %s on host %s failed' % (remote_path, self.remote_peername()), loglevel=log.loglevel_WARN) 347 raise x2go_exceptions.X2goControlSessionException('The SSH connection was dropped during an sFTP remove action.') 348 self.sftp_client = None 349 self._transport_lock.release()
350
351 - def _x2go_exec_command(self, cmd_line, loglevel=log.loglevel_INFO, timeout=20, **kwargs):
352 """ 353 Execute an X2Go server-side command via SSH. 354 355 During SSH command executions, sFTP operations get blocked. 356 357 @param cmd_line: the command to be executed on the remote server 358 @type cmd_line: C{str} or C{list} 359 @param loglevel: use this loglevel for reporting about remote command execution 360 @type loglevel: C{int} 361 @param timeout: if commands take longer than C{<timeout>} to be executed, consider the control session connection 362 to have died. 363 @type timeout: C{int} 364 @param kwargs: parameters that get passed through to the C{paramiko.SSHClient.exec_command()} method. 365 @type kwargs: C{dict} 366 367 @return: C{True} if the command could be successfully executed on the remote X2Go server 368 @rtype: C{bool} 369 370 @raise X2goControlSessionException: if the command execution failed (due to a lost connection) 371 372 """ 373 if type(cmd_line) == types.ListType: 374 cmd = " ".join(cmd_line) 375 else: 376 cmd = cmd_line 377 378 cmd = 'sh -c \"%s\"' % cmd 379 380 if self.session_died: 381 self.logger("control session seams to be dead, not executing command ,,%s'' on X2Go server %s" % (_rerewrite_blanks(cmd), self.profile_name,), loglevel=loglevel) 382 return (cStringIO.StringIO(), cStringIO.StringIO(), cStringIO.StringIO('failed to execute command')) 383 384 self._transport_lock.acquire() 385 386 _retval = None 387 388 ssh_transport = self.get_transport() 389 if ssh_transport and ssh_transport.is_authenticated(): 390 391 timer = gevent.Timeout(timeout) 392 timer.start() 393 try: 394 self.logger("executing command on X2Go server ,,%s'': %s" % (self.profile_name, _rerewrite_blanks(cmd)), loglevel=loglevel) 395 _retval = self.exec_command(_rewrite_password(cmd, user=self.get_transport().get_username(), password=self._session_password), **kwargs) 396 except AttributeError: 397 self.session_died = True 398 self._transport_lock.release() 399 if self.sshproxy_session: 400 self.sshproxy_session.stop_thread() 401 raise x2go_exceptions.X2goControlSessionException('the X2Go control session has died unexpectedly') 402 except EOFError: 403 self.session_died = True 404 self._transport_lock.release() 405 if self.sshproxy_session: 406 self.sshproxy_session.stop_thread() 407 raise x2go_exceptions.X2goControlSessionException('the X2Go control session has died unexpectedly') 408 except x2go_exceptions.SSHException: 409 self.session_died = True 410 self._transport_lock.release() 411 if self.sshproxy_session: 412 self.sshproxy_session.stop_thread() 413 raise x2go_exceptions.X2goControlSessionException('the X2Go control session has died unexpectedly') 414 except gevent.timeout.Timeout: 415 self.session_died = True 416 self._transport_lock.release() 417 if self.sshproxy_session: 418 self.sshproxy_session.stop_thread() 419 raise x2go_exceptions.X2goControlSessionException('the X2Go control session command timed out') 420 except socket.error: 421 self.session_died = True 422 self._transport_lock.release() 423 if self.sshproxy_session: 424 self.sshproxy_session.stop_thread() 425 raise x2go_exceptions.X2goControlSessionException('the X2Go control session has died unexpectedly') 426 finally: 427 timer.cancel() 428 429 else: 430 self._transport_lock.release() 431 raise x2go_exceptions.X2goControlSessionException('the X2Go control session is not connected') 432 433 self._transport_lock.release() 434 return _retval
435 436 @property
437 - def _x2go_server_features(self):
438 """\ 439 Render a list of server-side X2Go features. Results get cached once there has been one successfull query. 440 441 """ 442 if self._server_features is None: 443 (stdin, stdout, stderr) = self._x2go_exec_command('which x2gofeaturelist >/dev/null && x2gofeaturelist') 444 self._server_features = stdout.read().split('\n') 445 self.logger('server-side X2Go features are: %s' % self._server_features, loglevel=log.loglevel_DEBUG) 446 return self._server_features 447 else: 448 return self._server_features
449
450 - def query_server_features(self, force=False):
451 """\ 452 Do a query for the server-side list of X2Go features. 453 454 @param force: do not use the cached feature list, really ask the server (again) 455 @type force: C{bool} 456 457 @return: list of X2Go feature names 458 @rtype: C{list} 459 460 """ 461 if force: 462 self._server_features = None 463 return self._x2go_server_features
464 get_server_features = query_server_features 465 466 @property
467 - def _x2go_remote_home(self):
468 """\ 469 Retrieve and cache the remote home directory location. 470 471 """ 472 if self._remote_home is None: 473 (stdin, stdout, stderr) = self._x2go_exec_command('echo $HOME') 474 stdout_r = stdout.read() 475 if stdout_r: 476 self._remote_home = stdout_r.split()[0] 477 self.logger('remote user\' home directory: %s' % self._remote_home, loglevel=log.loglevel_DEBUG) 478 return self._remote_home 479 else: 480 return self._remote_home
481
482 - def _x2go_remote_group(self, group):
483 """\ 484 Retrieve and cache the members of a server-side POSIX group. 485 486 @param group: remote POSIX group name 487 @type group: C{str} 488 489 @return: list of POSIX group members 490 @rtype: C{list} 491 492 """ 493 if not self._remote_group.has_key(group): 494 (stdin, stdout, stderr) = self._x2go_exec_command('getent group %s | cut -d":" -f4' % group) 495 self._remote_group[group] = stdout.read().split('\n')[0].split(',') 496 self.logger('remote %s group: %s' % (group, self._remote_group[group]), loglevel=log.loglevel_DEBUG) 497 return self._remote_group[group] 498 else: 499 return self._remote_group[group]
500
501 - def is_x2gouser(self, username):
502 """\ 503 Is the remote user allowed to launch X2Go sessions? 504 505 FIXME: this method is currently non-functional. 506 507 @param username: remote user name 508 @type username: C{str} 509 510 @return: C{True} if the remote user is allowed to launch X2Go sessions 511 @rtype: C{bool} 512 513 """ 514 ### 515 ### FIXME: 516 ### 517 # discussion about server-side access restriction based on posix group membership or similar currently 518 # in process (as of 20110517, mg) 519 #return username in self._x2go_remote_group('x2gousers') 520 return True
521
522 - def is_sshfs_available(self):
523 """\ 524 Check if the remote user is allowed to use SSHFS mounts. 525 526 @return: C{True} if the user is allowed to connect client-side shares to the X2Go session 527 @rtype: C{bool} 528 529 """ 530 if self.remote_username() in self._x2go_remote_group('fuse'): 531 return True 532 return False
533
534 - def remote_username(self):
535 """\ 536 Returns (and caches) the control session's remote username. 537 538 @return: SSH transport's user name 539 @rtype: C{str} 540 541 @raise X2goControlSessionException: on SSH connection loss 542 543 """ 544 if self._remote_username is None: 545 if self.get_transport() is not None: 546 try: 547 self._remote_username = self.get_transport().get_username() 548 except: 549 self.session_died = True 550 raise x2go_exceptions.X2goControlSessionException('Lost connection to X2Go server') 551 return self._remote_username
552
553 - def remote_peername(self):
554 """\ 555 Returns (and caches) the control session's remote host (name or ip). 556 557 @return: SSH transport's peer name 558 @rtype: C{tuple} 559 560 @raise X2goControlSessionException: on SSH connection loss 561 562 """ 563 if self._remote_peername is None: 564 if self.get_transport() is not None: 565 try: 566 self._remote_peername = self.get_transport().getpeername() 567 except: 568 self.session_died = True 569 raise x2go_exceptions.X2goControlSessionException('Lost connection to X2Go server') 570 return self._remote_peername
571 572 @property
573 - def _x2go_session_auth_rsakey(self):
574 """\ 575 Generate (and cache) a temporary RSA host key for the lifetime of this control session. 576 577 """ 578 if self._session_auth_rsakey is None: 579 self._session_auth_rsakey = paramiko.RSAKey.generate(defaults.RSAKEY_STRENGTH) 580 return self._session_auth_rsakey
581
582 - def set_profile_name(self, profile_name):
583 """\ 584 Manipulate the control session's profile name. 585 586 @param profile_name: new profile name for this control session 587 @type profile_name: C{str} 588 589 """ 590 self.profile_name = profile_name
591
592 - def check_host(self, hostname, port=22):
593 """\ 594 Wraps around a Paramiko/SSH host key check. 595 596 @param hostname: the remote X2Go server's hostname 597 @type hostname: C{str} 598 @param port: the SSH port of the remote X2Go server 599 @type port: C{int} 600 601 @return: C{True} if the host key check succeeded, C{False} otherwise 602 @rtype: C{bool} 603 604 """ 605 # trailing whitespace tolerance 606 hostname = hostname.strip() 607 608 # force into IPv4 for localhost connections 609 if hostname in ('localhost', 'localhost.localdomain'): 610 hostname = '127.0.0.1' 611 612 return checkhosts.check_ssh_host_key(self, hostname, port=port)
613
614 - def connect(self, hostname, port=22, username='', password='', pkey=None, 615 key_filename=None, timeout=None, allow_agent=False, look_for_keys=False, 616 use_sshproxy=False, sshproxy_host='', sshproxy_port=22, sshproxy_user='', sshproxy_password='', sshproxy_force_password_auth=False, 617 sshproxy_key_filename='', sshproxy_pkey=None, sshproxy_look_for_keys=False, sshproxy_allow_agent=False, 618 sshproxy_tunnel='', 619 forward_sshagent=None, 620 session_instance=None, 621 add_to_known_hosts=False, force_password_auth=False):
622 """\ 623 Connect to an X2Go server and authenticate to it. This method is directly 624 inherited from the C{paramiko.SSHClient} class. The features of the Paramiko 625 SSH client connect method are recited here. The parameters C{add_to_known_hosts}, 626 C{force_password_auth}, C{session_instance} and all SSH proxy related parameters 627 have been added as X2Go specific parameters 628 629 The server's host key is checked against the system host keys 630 (see C{load_system_host_keys}) and any local host keys (C{load_host_keys}). 631 If the server's hostname is not found in either set of host keys, the missing host 632 key policy is used (see C{set_missing_host_key_policy}). The default policy is 633 to reject the key and raise an C{SSHException}. 634 635 Authentication is attempted in the following order of priority: 636 637 - The C{pkey} or C{key_filename} passed in (if any) 638 - Any key we can find through an SSH agent 639 - Any "id_rsa" or "id_dsa" key discoverable in C{~/.ssh/} 640 - Plain username/password auth, if a password was given 641 642 If a private key requires a password to unlock it, and a password is 643 passed in, that password will be used to attempt to unlock the key. 644 645 @param hostname: the server to connect to 646 @type hostname: C{str} 647 @param port: the server port to connect to 648 @type port: C{int} 649 @param username: the username to authenticate as (defaults to the 650 current local username) 651 @type username: C{str} 652 @param password: a password to use for authentication or for unlocking 653 a private key 654 @type password: C{str} 655 @param key_filename: the filename, or list of filenames, of optional 656 private key(s) to try for authentication 657 @type key_filename: C{str} or list(str) 658 @param pkey: an optional private key to use for authentication 659 @type pkey: C{PKey} 660 @param forward_sshagent: forward SSH agent authentication requests to the X2Go client-side 661 (will update the class property of the same name) 662 @type forward_sshagent: C{bool} 663 @param timeout: an optional timeout (in seconds) for the TCP connect 664 @type timeout: float 665 @param look_for_keys: set to C{True} to enable searching for discoverable 666 private key files in C{~/.ssh/} 667 @type look_for_keys: C{bool} 668 @param allow_agent: set to C{True} to enable connecting to a local SSH agent 669 for acquiring authentication information 670 @type allow_agent: C{bool} 671 @param add_to_known_hosts: non-paramiko option, if C{True} paramiko.AutoAddPolicy() 672 is used as missing-host-key-policy. If set to C{False} paramiko.RejectPolicy() 673 is used 674 @type add_to_known_hosts: C{bool} 675 @param force_password_auth: non-paramiko option, disable pub/priv key authentication 676 completely, even if the C{pkey} or the C{key_filename} parameter is given 677 @type force_password_auth: C{bool} 678 @param session_instance: an instance L{X2goSession} using this L{X2goControlSessionSTDOUT} 679 instance. 680 @type session_instance: C{obj} 681 @param use_sshproxy: connect through an SSH proxy 682 @type use_sshproxy: C{True} if an SSH proxy is to be used for tunneling the connection 683 @param sshproxy_host: hostname of the SSH proxy server 684 @type sshproxy_host: C{str} 685 @param sshproxy_port: port of the SSH proxy server 686 @type sshproxy_port: C{int} 687 @param sshproxy_user: username that we use for authenticating against C{<sshproxy_host>} 688 @type sshproxy_user: C{str} 689 @param sshproxy_password: a password to use for SSH proxy authentication or for unlocking 690 a private key 691 @type sshproxy_password: C{str} 692 @param sshproxy_force_password_auth: enforce using a given C{sshproxy_password} even if a key(file) is given 693 @type sshproxy_force_password_auth: C{bool} 694 @param sshproxy_key_filename: local file location of the private key file 695 @type sshproxy_key_filename: C{str} 696 @param sshproxy_pkey: an optional private key to use for SSH proxy authentication 697 @type sshproxy_pkey: C{PKey} 698 @param sshproxy_look_for_keys: set to C{True} to enable connecting to a local SSH agent 699 for acquiring authentication information (for SSH proxy authentication) 700 @type sshproxy_look_for_keys: C{bool} 701 @param sshproxy_allow_agent: set to C{True} to enable connecting to a local SSH agent 702 for acquiring authentication information (for SSH proxy authentication) 703 @type sshproxy_allow_agent: C{bool} 704 @param sshproxy_tunnel: the SSH proxy tunneling parameters, format is: <local-address>:<local-port>:<remote-address>:<remote-port> 705 @type sshproxy_tunnel: C{str} 706 707 @return: C{True} if an authenticated SSH transport could be retrieved by this method 708 @rtype: C{bool} 709 710 @raise BadHostKeyException: if the server's host key could not be 711 verified 712 @raise AuthenticationException: if authentication failed 713 @raise SSHException: if there was any other error connecting or 714 establishing an SSH session 715 @raise socket.error: if a socket error occurred while connecting 716 @raise X2goSSHProxyException: any SSH proxy exception is passed through while establishing the SSH proxy connection and tunneling setup 717 @raise X2goSSHAuthenticationException: any SSH proxy authentication exception is passed through while establishing the SSH proxy connection and tunneling setup 718 @raise X2goRemoteHomeException: if the remote home directory does not exist or is not accessible 719 720 """ 721 if use_sshproxy and sshproxy_host and sshproxy_user: 722 try: 723 self.sshproxy_session = sshproxy.X2goSSHProxy(known_hosts=self.known_hosts, 724 sshproxy_host=sshproxy_host, 725 sshproxy_port=sshproxy_port, 726 sshproxy_user=sshproxy_user, 727 sshproxy_password=sshproxy_password, 728 sshproxy_force_password_auth=sshproxy_force_password_auth, 729 sshproxy_key_filename=sshproxy_key_filename, 730 sshproxy_pkey=sshproxy_pkey, 731 sshproxy_look_for_keys=sshproxy_look_for_keys, 732 sshproxy_allow_agent=sshproxy_allow_agent, 733 sshproxy_tunnel=sshproxy_tunnel, 734 session_instance=session_instance, 735 logger=self.logger, 736 ) 737 738 except: 739 if self.sshproxy_session: 740 self.sshproxy_session.stop_thread() 741 self.sshproxy_session = None 742 raise 743 744 if self.sshproxy_session is not None: 745 self.sshproxy_session.start() 746 747 # divert port to sshproxy_session's local forwarding port (it might have changed due to 748 # SSH connection errors 749 gevent.sleep(.1) 750 port = self.sshproxy_session.get_local_proxy_port() 751 752 if not add_to_known_hosts and session_instance: 753 self.set_missing_host_key_policy(checkhosts.X2goInteractiveAddPolicy(caller=self, session_instance=session_instance)) 754 755 if add_to_known_hosts: 756 self.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 757 758 # disable pub/priv key authentication if forced 759 if force_password_auth: 760 key_filename = None 761 pkey = None 762 763 # trailing whitespace tolerance in hostname 764 hostname = hostname.strip() 765 766 self.logger('connecting to [%s]:%s' % (hostname, port), loglevel=log.loglevel_NOTICE) 767 768 self.load_session_host_keys() 769 770 _hostname = hostname 771 # enforce IPv4 for localhost address 772 if _hostname in ('localhost', 'localhost.localdomain'): 773 _hostname = '127.0.0.1' 774 775 # update self.forward_sshagent via connect method parameter 776 if forward_sshagent is not None: 777 self.forward_sshagent = forward_sshagent 778 779 if key_filename or pkey or look_for_keys or allow_agent or (password and force_password_auth): 780 try: 781 if password and force_password_auth: 782 self.logger('trying keyboard-interactive SSH authentication with server', loglevel=log.loglevel_DEBUG) 783 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, pkey=None, password=password, 784 key_filename=None, timeout=timeout, allow_agent=False, 785 look_for_keys=False) 786 elif (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey: 787 self.logger('trying SSH pub/priv key authentication with server', loglevel=log.loglevel_DEBUG) 788 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, pkey=pkey, 789 key_filename=key_filename, timeout=timeout, allow_agent=allow_agent, 790 look_for_keys=look_for_keys) 791 else: 792 self.logger('trying SSH key discovery or agent authentication with server', loglevel=log.loglevel_DEBUG) 793 try: 794 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, pkey=None, 795 key_filename=None, timeout=timeout, allow_agent=allow_agent, 796 look_for_keys=look_for_keys) 797 except paramiko.SSHException, e: 798 if str(e) == 'No authentication methods available': 799 raise paramiko.AuthenticationException('Interactive password authentication required!') 800 else: 801 raise(e) 802 803 # since Paramiko 1.7.7.1 there is compression available, let's use it if present... 804 t = self.get_transport() 805 if x2go._paramiko.PARAMIKO_FEATURE['use-compression']: 806 t.use_compression(compress=True) 807 808 except paramiko.AuthenticationException, e: 809 self.close() 810 if password: 811 self.logger('next auth mechanism we\'ll try is keyboard-interactive authentication', loglevel=log.loglevel_DEBUG) 812 try: 813 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, 814 key_filename=None, pkey=None, timeout=timeout, allow_agent=False, look_for_keys=False) 815 except: 816 self.close() 817 if self.sshproxy_session: 818 self.sshproxy_session.stop_thread() 819 raise 820 else: 821 self.close() 822 if self.sshproxy_session: 823 self.sshproxy_session.stop_thread() 824 raise(e) 825 826 except: 827 self.close() 828 if self.sshproxy_session: 829 self.sshproxy_session.stop_thread() 830 raise 831 832 # if there is no private key (and no agent auth), we will use the given password, if any 833 else: 834 # create a random password if password is empty to trigger host key validity check 835 if not password: 836 password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)]) 837 self.logger('performing SSH keyboard-interactive authentication with server', loglevel=log.loglevel_DEBUG) 838 try: 839 paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, 840 timeout=timeout, allow_agent=False, look_for_keys=False) 841 except paramiko.AuthenticationException, e: 842 self.close() 843 if self.sshproxy_session: 844 self.sshproxy_session.stop_thread() 845 raise e 846 except: 847 self.close() 848 if self.sshproxy_session: 849 self.sshproxy_session.stop_thread() 850 raise 851 852 self.set_missing_host_key_policy(paramiko.RejectPolicy()) 853 854 self.hostname = hostname 855 self.port = port 856 857 # preparing reverse tunnels 858 ssh_transport = self.get_transport() 859 ssh_transport.reverse_tunnels = {} 860 861 # mark Paramiko/SSH transport as X2goControlSession 862 ssh_transport._x2go_session_marker = True 863 self._session_password = password 864 865 if ssh_transport is not None: 866 self.session_died = False 867 self.query_server_features(force=True) 868 if self.forward_sshagent: 869 if x2go._paramiko.PARAMIKO_FEATURE['forward-ssh-agent']: 870 self.agent_chan = ssh_transport.open_session() 871 self.agent_handler = paramiko.agent.AgentRequestHandler(self.agent_chan) 872 self.logger('Requesting SSH agent forwarding for control session of connected session profile %s' % self.profile_name, loglevel=log.loglevel_INFO) 873 else: 874 self.logger('SSH agent forwarding is not available in the Paramiko version used with this instance of Python X2Go', loglevel=log.loglevel_WARN) 875 876 self._remote_home = None 877 if not self.home_exists(): 878 raise x2go_exceptions.X2goRemoteHomeException('remote home directory does not exist') 879 880 return (self.get_transport() is not None)
881
882 - def dissociate(self, terminal_session):
883 """\ 884 Drop an associated terminal session. 885 886 @param terminal_session: the terminal session object to remove from the list of associated terminals 887 @type terminal_session: C{X2goTerminalSession*} 888 889 """ 890 for t_name in self.associated_terminals.keys(): 891 if self.associated_terminals[t_name] == terminal_session: 892 del self.associated_terminals[t_name] 893 if self.terminated_terminals.has_key(t_name): 894 del self.terminated_terminals[t_name]
895
896 - def disconnect(self):
897 """\ 898 Disconnect this control session from the remote server. 899 900 @return: report success or failure after having disconnected 901 @rtype: C{bool} 902 903 """ 904 if self.associated_terminals: 905 t_names = self.associated_terminals.keys() 906 for t_obj in self.associated_terminals.values(): 907 try: 908 if not self.session_died: 909 t_obj.suspend() 910 except x2go_exceptions.X2goTerminalSessionException: 911 pass 912 except x2go_exceptions.X2goControlSessionException: 913 self.session_died 914 t_obj.__del__() 915 for t_name in t_names: 916 try: 917 del self.associated_terminals[t_name] 918 except KeyError: 919 pass 920 921 self._remote_home = None 922 self._remote_group = {} 923 924 self._session_auth_rsakey = None 925 926 # in any case, release out internal transport lock 927 self._transport_lock.release() 928 929 # close SSH agent auth forwarding objects 930 if self.agent_handler is not None: 931 self.agent_handler.close() 932 933 if self.agent_chan is not None: 934 self.agent_chan.close() 935 936 retval = False 937 try: 938 if self.get_transport() is not None: 939 retval = self.get_transport().is_active() 940 try: 941 self.close() 942 except IOError: 943 pass 944 except AttributeError: 945 # if the Paramiko _transport object has not yet been initialized, ignore it 946 # but state that this method call did not close the SSH client, but was already closed 947 pass 948 949 # take down sshproxy_session no matter what happened to the control session itself 950 if self.sshproxy_session is not None: 951 self.sshproxy_session.stop_thread() 952 953 return retval
954
955 - def home_exists(self):
956 """\ 957 Test if the remote home directory exists. 958 959 @return: C{True} if the home directory exists, C{False} otherwise 960 @rtype: C{bool} 961 962 """ 963 (_stdin, _stdout, _stderr) = self._x2go_exec_command('stat -tL "%s"' % self._x2go_remote_home, loglevel=log.loglevel_DEBUG) 964 if _stdout.read(): 965 return True 966 return False
967 968
969 - def is_alive(self):
970 """\ 971 Test if the connection to the remote X2Go server is still alive. 972 973 @return: C{True} if the connection is still alive, C{False} otherwise 974 @rtype: C{bool} 975 976 """ 977 try: 978 if self._x2go_exec_command('echo', loglevel=log.loglevel_DEBUG): 979 return True 980 except x2go_exceptions.X2goControlSessionException: 981 self.session_died = True 982 return False
983
984 - def has_session_died(self):
985 """\ 986 Test if the connection to the remote X2Go server died on the way. 987 988 @return: C{True} if the connection has died, C{False} otherwise 989 @rtype: C{bool} 990 991 """ 992 return self.session_died
993
994 - def get_published_applications(self, lang=None, refresh=False, raw=False, very_raw=False, max_no_submenus=defaults.PUBAPP_MAX_NO_SUBMENUS):
995 """\ 996 Retrieve the menu tree of published applications from the remote X2Go server. 997 998 The C{raw} option lets this method return a C{list} of C{dict} elements. Each C{dict} elements has a 999 C{desktop} key containing a shortened version of the text output of a .desktop file and an C{icon} key 1000 which contains the desktop base64-encoded icon data. 1001 1002 The {very_raw} lets this method return the output of the C{x2gogetapps} script as is. 1003 1004 @param lang: locale/language identifier 1005 @type lang: C{str} 1006 @param refresh: force reload of the menu tree from X2Go server 1007 @type refresh: C{bool} 1008 @param raw: retrieve a raw output of the server list of published applications 1009 @type raw: C{bool} 1010 @param very_raw: retrieve a very raw output of the server list of published applications 1011 @type very_raw: C{bool} 1012 1013 @return: an i18n capable menu tree packed as a Python dictionary 1014 @rtype: C{list} 1015 1016 """ 1017 self._already_querying_published_applications.acquire() 1018 1019 if defaults.X2GOCLIENT_OS != 'Windows' and lang is None: 1020 lang = locale.getdefaultlocale()[0] 1021 elif lang is None: 1022 lang = 'en' 1023 1024 if 'X2GO_PUBLISHED_APPLICATIONS' in self.get_server_features(): 1025 if self._published_applications_menu is {} or \ 1026 not self._published_applications_menu.has_key(lang) or \ 1027 raw or very_raw or refresh or \ 1028 (self.published_applications_no_submenus != max_no_submenus): 1029 1030 self.published_applications_no_submenus = max_no_submenus 1031 1032 ### STAGE 1: retrieve menu from server 1033 1034 self.logger('querying server (%s) for list of published applications' % self.profile_name, loglevel=log.loglevel_NOTICE) 1035 (stdin, stdout, stderr) = self._x2go_exec_command('which x2gogetapps >/dev/null && x2gogetapps') 1036 _raw_output = stdout.read() 1037 1038 if very_raw: 1039 self.logger('published applications query for %s finished, return very raw output' % self.profile_name, loglevel=log.loglevel_NOTICE) 1040 self._already_querying_published_applications.release() 1041 return _raw_output 1042 1043 ### STAGE 2: dissect the text file retrieved from server, cut into single menu elements 1044 1045 _raw_menu_items = _raw_output.split('</desktop>\n') 1046 _raw_menu_items = [ i.replace('<desktop>\n', '') for i in _raw_menu_items ] 1047 _menu = [] 1048 for _raw_menu_item in _raw_menu_items: 1049 if '<icon>\n' in _raw_menu_item and '</icon>' in _raw_menu_item: 1050 _menu_item = _raw_menu_item.split('<icon>\n')[0] + _raw_menu_item.split('</icon>\n')[1] 1051 _icon_base64 = _raw_menu_item.split('<icon>\n')[1].split('</icon>\n')[0] 1052 else: 1053 _menu_item = _raw_menu_item 1054 _icon_base64 = None 1055 if _menu_item: 1056 _menu.append({ 'desktop': _menu_item, 'icon': _icon_base64, }) 1057 _menu_item = None 1058 _icon_base64 = None 1059 1060 if raw: 1061 self.logger('published applications query for %s finished, returning raw output' % self.profile_name, loglevel=log.loglevel_NOTICE) 1062 self._already_querying_published_applications.release() 1063 return _menu 1064 1065 if len(_menu) > max_no_submenus >= 0: 1066 _render_submenus = True 1067 else: 1068 _render_submenus = False 1069 1070 # STAGE 3: create menu structure in a Python dictionary 1071 1072 _category_map = { 1073 lang: { 1074 'Multimedia': [], 1075 'Development': [], 1076 'Education': [], 1077 'Games': [], 1078 'Graphics': [], 1079 'Internet': [], 1080 'Office': [], 1081 'System': [], 1082 'Utilities': [], 1083 'Other Applications': [], 1084 'TOP': [], 1085 } 1086 } 1087 _empty_menus = _category_map[lang].keys() 1088 1089 for item in _menu: 1090 1091 _menu_entry_name = '' 1092 _menu_entry_fallback_name = '' 1093 _menu_entry_comment = '' 1094 _menu_entry_fallback_comment = '' 1095 _menu_entry_exec = '' 1096 _menu_entry_cat = '' 1097 _menu_entry_shell = False 1098 1099 lang_regio = lang 1100 lang_only = lang_regio.split('_')[0] 1101 1102 for line in item['desktop'].split('\n'): 1103 if re.match('^Name\[%s\]=.*' % lang_regio, line) or re.match('Name\[%s\]=.*' % lang_only, line): 1104 _menu_entry_name = line.split("=")[1].strip() 1105 elif re.match('^Name=.*', line): 1106 _menu_entry_fallback_name = line.split("=")[1].strip() 1107 elif re.match('^Comment\[%s\]=.*' % lang_regio, line) or re.match('Comment\[%s\]=.*' % lang_only, line): 1108 _menu_entry_comment = line.split("=")[1].strip() 1109 elif re.match('^Comment=.*', line): 1110 _menu_entry_fallback_comment = line.split("=")[1].strip() 1111 elif re.match('^Exec=.*', line): 1112 _menu_entry_exec = line.split("=")[1].strip() 1113 elif re.match('^Terminal=.*(t|T)(r|R)(u|U)(e|E).*', line): 1114 _menu_entry_shell = True 1115 elif re.match('^Categories=.*', line): 1116 if 'X2Go-Top' in line: 1117 _menu_entry_cat = 'TOP' 1118 elif 'Audio' in line or 'Video' in line: 1119 _menu_entry_cat = 'Multimedia' 1120 elif 'Development' in line: 1121 _menu_entry_cat = 'Development' 1122 elif 'Education' in line: 1123 _menu_entry_cat = 'Education' 1124 elif 'Game' in line: 1125 _menu_entry_cat = 'Games' 1126 elif 'Graphics' in line: 1127 _menu_entry_cat = 'Graphics' 1128 elif 'Network' in line: 1129 _menu_entry_cat = 'Internet' 1130 elif 'Office' in line: 1131 _menu_entry_cat = 'Office' 1132 elif 'Settings' in line: 1133 continue 1134 elif 'System' in line: 1135 _menu_entry_cat = 'System' 1136 elif 'Utility' in line: 1137 _menu_entry_cat = 'Utilities' 1138 else: 1139 _menu_entry_cat = 'Other Applications' 1140 1141 if not _menu_entry_exec: 1142 continue 1143 else: 1144 # FIXME: strip off any noted options (%f, %F, %u, %U, ...), this can be more intelligent 1145 _menu_entry_exec = _menu_entry_exec.replace('%f', '').replace('%F','').replace('%u','').replace('%U','') 1146 if _menu_entry_shell: 1147 _menu_entry_exec = "x-terminal-emulator -e '%s'" % _menu_entry_exec 1148 1149 if not _menu_entry_cat: 1150 _menu_entry_cat = 'Other Applications' 1151 1152 if not _render_submenus: 1153 _menu_entry_cat = 'TOP' 1154 1155 if _menu_entry_cat in _empty_menus: 1156 _empty_menus.remove(_menu_entry_cat) 1157 1158 if not _menu_entry_name: _menu_entry_name = _menu_entry_fallback_name 1159 if not _menu_entry_comment: _menu_entry_comment = _menu_entry_fallback_comment 1160 if not _menu_entry_comment: _menu_entry_comment = _menu_entry_name 1161 1162 _menu_entry_icon = item['icon'] 1163 1164 _category_map[lang][_menu_entry_cat].append( 1165 { 1166 'name': _menu_entry_name, 1167 'comment': _menu_entry_comment, 1168 'exec': _menu_entry_exec, 1169 'icon': _menu_entry_icon, 1170 } 1171 ) 1172 1173 for _cat in _empty_menus: 1174 del _category_map[lang][_cat] 1175 1176 for _cat in _category_map[lang].keys(): 1177 _sorted = sorted(_category_map[lang][_cat], key=lambda k: k['name']) 1178 _category_map[lang][_cat] = _sorted 1179 1180 self._published_applications_menu.update(_category_map) 1181 self.logger('published applications query for %s finished, return menu tree' % self.profile_name, loglevel=log.loglevel_NOTICE) 1182 1183 else: 1184 # FIXME: ignoring the absence of the published applications feature for now, handle it appropriately later 1185 pass 1186 1187 self._already_querying_published_applications.release() 1188 return self._published_applications_menu
1189
1190 - def start(self, **kwargs):
1191 """\ 1192 Start a new X2Go session. 1193 1194 The L{X2goControlSessionSTDOUT.start()} method accepts any parameter 1195 that can be passed to any of the C{X2goTerminalSession} backend class 1196 constructors. 1197 1198 @param kwargs: parameters that get passed through to the control session's 1199 L{resume()} method, only the C{session_name} parameter will get removed 1200 before pass-through 1201 @type kwargs: C{dict} 1202 1203 @return: return value of the cascaded L{resume()} method, denoting the success or failure 1204 of the session startup 1205 @rtype: C{bool} 1206 1207 """ 1208 if 'session_name' in kwargs.keys(): 1209 del kwargs['session_name'] 1210 return self.resume(**kwargs)
1211
1212 - def resume(self, session_name=None, session_instance=None, session_list=None, **kwargs):
1213 """\ 1214 Resume a running/suspended X2Go session. 1215 1216 The L{X2goControlSessionSTDOUT.resume()} method accepts any parameter 1217 that can be passed to any of the C{X2goTerminalSession*} backend class constructors. 1218 1219 @return: True if the session could be successfully resumed 1220 @rtype: C{bool} 1221 1222 @raise X2goUserException: if the remote user is not allowed to launch/resume X2Go sessions. 1223 1224 """ 1225 if not self.is_x2gouser(self.get_transport().get_username()): 1226 raise x2go_exceptions.X2goUserException('remote user %s is not allowed to run X2Go commands' % self.get_transport().get_username()) 1227 1228 try: 1229 if session_name is not None: 1230 if session_list: 1231 session_info = session_list[session_name] 1232 else: 1233 session_info = self.list_sessions()[session_name] 1234 else: 1235 session_info = None 1236 except KeyError: 1237 _success = False 1238 1239 _terminal = self._terminal_backend(self, 1240 profile_name=self.profile_name, 1241 session_info=session_info, 1242 info_backend=self._info_backend, 1243 list_backend=self._list_backend, 1244 proxy_backend=self._proxy_backend, 1245 client_rootdir=self.client_rootdir, 1246 session_instance=session_instance, 1247 sessions_rootdir=self.sessions_rootdir, 1248 **kwargs) 1249 1250 _success = False 1251 try: 1252 if session_name is not None: 1253 _success = _terminal.resume() 1254 else: 1255 _success = _terminal.start() 1256 except x2go_exceptions.X2goTerminalSessionException: 1257 _success = False 1258 1259 if _success: 1260 while not _terminal.ok(): 1261 gevent.sleep(.2) 1262 1263 if _terminal.ok(): 1264 self.associated_terminals[_terminal.get_session_name()] = _terminal 1265 self.get_transport().reverse_tunnels[_terminal.get_session_name()] = { 1266 'sshfs': (0, None), 1267 'snd': (0, None), 1268 } 1269 1270 return _terminal or None 1271 1272 return None
1273
1274 - def share_desktop(self, desktop=None, user=None, display=None, share_mode=0, **kwargs):
1275 """\ 1276 Share another already running desktop session. Desktop sharing can be run 1277 in two different modes: view-only and full-access mode. 1278 1279 @param desktop: desktop ID of a sharable desktop in format C{<user>@<display>} 1280 @type desktop: C{str} 1281 @param user: user name and display number can be given separately, here give the 1282 name of the user who wants to share a session with you 1283 @type user: C{str} 1284 @param display: user name and display number can be given separately, here give the 1285 number of the display that a user allows you to be shared with 1286 @type display: C{str} 1287 @param share_mode: desktop sharing mode, 0 stands for VIEW-ONLY, 1 for FULL-ACCESS mode 1288 @type share_mode: C{int} 1289 1290 @return: True if the session could be successfully shared 1291 @rtype: C{bool} 1292 1293 @raise X2goDesktopSharingException: if C{username} and C{dislpay} do not relate to a 1294 sharable desktop session 1295 1296 """ 1297 if desktop: 1298 user = desktop.split('@')[0] 1299 display = desktop.split('@')[1] 1300 if not (user and display): 1301 raise x2go_exceptions.X2goDesktopSharingException('Need user name and display number of sharable desktop.') 1302 1303 cmd = '%sXSHAD%sXSHAD%s' % (share_mode, user, display) 1304 1305 kwargs['cmd'] = cmd 1306 kwargs['session_type'] = 'shared' 1307 1308 return self.start(**kwargs)
1309
1310 - def list_desktops(self, raw=False, maxwait=20):
1311 """\ 1312 List all desktop-like sessions of current user (or of users that have 1313 granted desktop sharing) on the connected server. 1314 1315 @param raw: if C{True}, the raw output of the server-side X2Go command 1316 C{x2golistdesktops} is returned. 1317 @type raw: C{bool} 1318 1319 @return: a list of X2Go desktops available for sharing 1320 @rtype: C{list} 1321 1322 @raise X2goTimeOutException: on command execution timeouts, with the server-side C{x2golistdesktops} 1323 command this can sometimes happen. Make sure you ignore these time-outs and to try again 1324 1325 """ 1326 if raw: 1327 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistdesktops") 1328 return stdout.read(), stderr.read() 1329 1330 else: 1331 1332 # this _success loop will catch errors in case the x2golistsessions output is corrupt 1333 # this should not be needed and is a workaround for the current X2Go server implementation 1334 1335 timeout = gevent.Timeout(maxwait) 1336 timeout.start() 1337 try: 1338 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistdesktops") 1339 _stdout_read = stdout.read() 1340 _listdesktops = _stdout_read.split('\n') 1341 except gevent.timeout.Timeout: 1342 # if we do not get a reply here after <maxwait> seconds we will raise a time out, we have to 1343 # make sure that we catch this at places where we want to ignore timeouts (e.g. in the 1344 # desktop list cache) 1345 raise x2go_exceptions.X2goTimeOutException('x2golistdesktop command timed out') 1346 finally: 1347 timeout.cancel() 1348 1349 return _listdesktops
1350
1351 - def list_mounts(self, session_name, raw=False, maxwait=20):
1352 """\ 1353 List all mounts for a given session of the current user on the connected server. 1354 1355 @param session_name: name of a session to query a list of mounts for 1356 @type session_name: C{str} 1357 @param raw: if C{True}, the raw output of the server-side X2Go command 1358 C{x2golistmounts} is returned. 1359 @type raw: C{bool} 1360 @param maxwait: stop processing C{x2golistmounts} after C{<maxwait>} seconds 1361 @type maxwait: C{int} 1362 1363 @return: a list of client-side mounts for X2Go session C{<session_name>} on the server 1364 @rtype: C{list} 1365 1366 @raise X2goTimeOutException: on command execution timeouts, queries with the server-side 1367 C{x2golistmounts} query should normally be processed quickly, a time-out may hint that the 1368 control session has lost its connection to the X2Go server 1369 1370 """ 1371 if raw: 1372 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistmounts %s" % session_name) 1373 return stdout.read(), stderr.read() 1374 1375 else: 1376 1377 # this _success loop will catch errors in case the x2golistmounts output is corrupt 1378 1379 timeout = gevent.Timeout(maxwait) 1380 timeout.start() 1381 try: 1382 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistmounts %s" % session_name) 1383 _stdout_read = stdout.read() 1384 _listmounts = {session_name: [ line for line in _stdout_read.split('\n') if line ] } 1385 except gevent.timeout.Timeout: 1386 # if we do not get a reply here after <maxwait> seconds we will raise a time out, we have to 1387 # make sure that we catch this at places where we want to ignore timeouts 1388 raise x2go_exceptions.X2goTimeOutException('x2golistmounts command timed out') 1389 finally: 1390 timeout.cancel() 1391 1392 return _listmounts
1393
1394 - def list_sessions(self, raw=False):
1395 """\ 1396 List all sessions of current user on the connected server. 1397 1398 @param raw: if C{True}, the raw output of the server-side X2Go command 1399 C{x2golistsessions} is returned. 1400 @type raw: C{bool} 1401 1402 @return: normally an instance of a C{X2goServerSessionList*} backend is returned. However, 1403 if the raw argument is set, the plain text output of the server-side C{x2golistsessions} 1404 command is returned 1405 @rtype: C{X2goServerSessionList} instance or str 1406 1407 @raise X2goControlSessionException: on command execution timeouts, if this happens the control session will 1408 be interpreted as disconnected due to connection loss 1409 """ 1410 if raw: 1411 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistsessions") 1412 return stdout.read(), stderr.read() 1413 1414 else: 1415 1416 # this _success loop will catch errors in case the x2golistsessions output is corrupt 1417 # this should not be needed and is a workaround for the current X2Go server implementation 1418 _listsessions = {} 1419 _success = False 1420 _count = 0 1421 _maxwait = 20 1422 1423 # we will try this 20 times before giving up... we might simply catch the x2golistsessions 1424 # output in the middle of creating a session in the database... 1425 while not _success and _count < _maxwait: 1426 _count += 1 1427 try: 1428 (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistsessions") 1429 _stdout_read = stdout.read() 1430 _listsessions = self._list_backend(_stdout_read, info_backend=self._info_backend).sessions 1431 _success = True 1432 except KeyError: 1433 gevent.sleep(1) 1434 except IndexError: 1435 gevent.sleep(1) 1436 except ValueError: 1437 gevent.sleep(1) 1438 1439 if _count >= _maxwait: 1440 self.session_died = True 1441 raise x2go_exceptions.X2goControlSessionException('x2golistsessions command failed after we have tried 20 times') 1442 1443 # update internal variables when list_sessions() is called 1444 for _session_name, _terminal in self.associated_terminals.items(): 1445 if _session_name in _listsessions.keys(): 1446 # update the whole session_info object within the terminal session 1447 if hasattr(self.associated_terminals[_session_name], 'session_info') and not self.associated_terminals[_session_name].is_session_info_protected(): 1448 self.associated_terminals[_session_name].session_info.update(_listsessions[_session_name]) 1449 else: 1450 try: del self.associated_terminals[_session_name] 1451 except KeyError: pass 1452 self.terminated_terminals.append(_session_name) 1453 if _terminal.is_suspended(): 1454 try: del self.associated_terminals[_session_name] 1455 except KeyError: pass 1456 1457 1458 return _listsessions
1459
1460 - def clean_sessions(self, destroy_terminals=True, published_applications=False):
1461 """\ 1462 Find X2Go terminals that have previously been started by the 1463 connected user on the remote X2Go server and terminate them. 1464 1465 @param destroy_terminals: destroy the terminal session instances after cleanup 1466 @type destroy_terminals: C{bool} 1467 @param published_applications: also clean up published applications providing sessions 1468 @type published_applications: C{bool} 1469 1470 """ 1471 session_list = self.list_sessions() 1472 if published_applications: 1473 session_names = session_list.keys() 1474 else: 1475 session_names = [ _sn for _sn in session_list.keys() if not session_list[_sn].is_published_applications_provider() ] 1476 for session_name in session_names: 1477 self.terminate(session_name=session_name, destroy_terminals=destroy_terminals)
1478
1479 - def is_connected(self):
1480 """\ 1481 Returns C{True} if this control session is connected to the remote server (that 1482 is: if it has a valid Paramiko/SSH transport object). 1483 1484 @return: X2Go session connected? 1485 @rtype: C{bool} 1486 1487 """ 1488 return self.get_transport() is not None and self.get_transport().is_authenticated()
1489
1490 - def is_running(self, session_name):
1491 """\ 1492 Returns C{True} if the given X2Go session is in running state, 1493 C{False} else. 1494 1495 @param session_name: X2Go name of the session to be queried 1496 @type session_name: C{str} 1497 1498 @return: X2Go session running? If C{<session_name>} is not listable by the L{list_sessions()} method then C{None} is returned 1499 @rtype: C{bool} or C{None} 1500 1501 """ 1502 session_infos = self.list_sessions() 1503 if session_name in session_infos.keys(): 1504 return session_infos[session_name].is_running() 1505 return None
1506
1507 - def is_suspended(self, session_name):
1508 """\ 1509 Returns C{True} if the given X2Go session is in suspended state, 1510 C{False} else. 1511 1512 @return: X2Go session suspended? If C{<session_name>} is not listable by the L{list_sessions()} method then C{None} is returned 1513 @rtype: C{bool} or C{None} 1514 1515 """ 1516 session_infos = self.list_sessions() 1517 if session_name in session_infos.keys(): 1518 return session_infos[session_name].is_suspended() 1519 return None
1520
1521 - def has_terminated(self, session_name):
1522 """\ 1523 Returns C{True} if the X2Go session with name C{<session_name>} has been seen 1524 by this control session and--in the meantime--has been terminated. 1525 1526 If C{<session_name>} has not been seen, yet, the method will return C{None}. 1527 1528 @return: X2Go session has terminated? 1529 @rtype: C{bool} or C{None} 1530 1531 """ 1532 session_infos = self.list_sessions() 1533 if session_name in self.terminated_terminals: 1534 return True 1535 if session_name not in session_infos.keys() and session_name in self.associated_terminals.keys(): 1536 # do a post-mortem tidy up 1537 self.terminate(session_name) 1538 return True 1539 if self.is_suspended(session_name) or self.is_running(session_name): 1540 return False 1541 1542 return None
1543
1544 - def suspend(self, session_name):
1545 """\ 1546 Suspend X2Go session with name C{<session_name>} on the connected 1547 server. 1548 1549 @param session_name: X2Go name of the session to be suspended 1550 @type session_name: C{str} 1551 1552 @return: C{True} if the session could be successfully suspended 1553 @rtype: C{bool} 1554 1555 """ 1556 _ret = False 1557 _session_names = [ t.get_session_name() for t in self.associated_terminals.values() ] 1558 if session_name in _session_names: 1559 1560 self.logger('suspending associated terminal session: %s' % session_name, loglevel=log.loglevel_DEBUG) 1561 (stdin, stdout, stderr) = self._x2go_exec_command("x2gosuspend-session %s" % session_name, loglevel=log.loglevel_DEBUG) 1562 stdout.read() 1563 stderr.read() 1564 if self.associated_terminals.has_key(session_name): 1565 if self.associated_terminals[session_name] is not None: 1566 self.associated_terminals[session_name].__del__() 1567 try: del self.associated_terminals[session_name] 1568 except KeyError: pass 1569 _ret = True 1570 1571 else: 1572 1573 self.logger('suspending non-associated terminal session: %s' % session_name, loglevel=log.loglevel_DEBUG) 1574 (stdin, stdout, stderr) = self._x2go_exec_command("x2gosuspend-session %s" % session_name, loglevel=log.loglevel_DEBUG) 1575 stdout.read() 1576 stderr.read() 1577 _ret = True 1578 1579 return _ret
1580
1581 - def terminate(self, session_name, destroy_terminals=True):
1582 """\ 1583 Terminate X2Go session with name C{<session_name>} on the connected 1584 server. 1585 1586 @param session_name: X2Go name of the session to be terminated 1587 @type session_name: C{str} 1588 1589 @return: C{True} if the session could be successfully terminated 1590 @rtype: C{bool} 1591 1592 """ 1593 1594 _ret = False 1595 _session_names = [ t.get_session_name() for t in self.associated_terminals.values() ] 1596 if session_name in _session_names: 1597 1598 self.logger('terminating associated session: %s' % session_name, loglevel=log.loglevel_DEBUG) 1599 (stdin, stdout, stderr) = self._x2go_exec_command("x2goterminate-session %s" % session_name, loglevel=log.loglevel_DEBUG) 1600 stdout.read() 1601 stderr.read() 1602 if self.associated_terminals.has_key(session_name): 1603 if self.associated_terminals[session_name] is not None and destroy_terminals: 1604 self.associated_terminals[session_name].__del__() 1605 try: del self.associated_terminals[session_name] 1606 except KeyError: pass 1607 self.terminated_terminals.append(session_name) 1608 _ret = True 1609 1610 else: 1611 1612 self.logger('terminating non-associated session: %s' % session_name, loglevel=log.loglevel_DEBUG) 1613 (stdin, stdout, stderr) = self._x2go_exec_command("x2goterminate-session %s" % session_name, loglevel=log.loglevel_DEBUG) 1614 stdout.read() 1615 stderr.read() 1616 _ret = True 1617 1618 return _ret
1619