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