Source code for gerritssh.borrowed.ssh

# The MIT License
#
# Copyright 2012 Sony Mobile Communications. All rights reserved.
# Copyright 2014 Keith Derrick
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

"""
Gerrit SSH Client.

A thin wrapper/extension of Paramiko's SSHClient class, adding logic
to parse the standard configuration file from ~/.ssh.

It also provides lightweight threading protection to allow a single
connection to be used by multiple threads.

"""

from os.path import abspath, expanduser, isfile
import socket
from threading import Event, Lock

from paramiko import SSHClient, SSHConfig
from paramiko.ssh_exception import SSHException


[docs]class SSHCommandResult(object): """ Represents the results of a command run over SSH. The three attributes are channels representing the three standard streams. :param stdin: The channel's input channel :param stdout: Standard output channel :param stderr: The error output channel """ def __init__(self, command, stdin, stdout, stderr): self.command = command self.stdin = stdin self.stdout = stdout self.stderr = stderr def __repr__(self): return "<SSHCommandResult [%s]>" % self.command
[docs]class GerritSSHClient(SSHClient): """ Gerrit SSH Client, extending the paramiko SSH Client. :param hostname: The host to connect to :param username: The optional user name to use on connection :param port: The optional port to use :param keyfile_name: The optional key file to use """ def __init__(self, hostname, username=None, port=None, keyfile_name=None): """ Initialize and connect to SSH. """ super(GerritSSHClient, self).__init__() self.hostname = hostname self.username = username self.key_filename = keyfile_name self.port = port self.__connected = Event() self.lock = Lock() def _configure(self): """ Configure the ssh parameters from the config file. The Port and username are extracted from .ssh/config, unless overridden by arguments to the constructor. If username and or port are provided to `__init__`, they will override the values found in the configuration file. :raise: SSHException under the following conditions: * No configuration file is found * It does not contain an entry for the Host. * It references a keyfile which does not exist * The port number is non-numeric or negative * Values for port and username can not be determined """ configfile = expanduser("~/.ssh/config") if not isfile(configfile): raise SSHException("ssh config file '%s' does not exist" % configfile) config = SSHConfig() config.parse(open(configfile)) data = config.lookup(self.hostname) if not data: raise SSHException("No ssh config for host %s" % self.hostname) self.hostname = data.get('hostname', None) if not self.username: self.username = data.get('user', None) if self.key_filename: self.key_filename = abspath(expanduser(self.key_filename)) elif 'identityfile' in data: self.key_filename = abspath(expanduser(data['identityfile'][0])) if self.key_filename and not isfile(self.key_filename): raise SSHException("Identity file '%s' does not exist" % self.key_filename) if self.port is None: try: self.port = int(data.get('port', '29418')) except ValueError: raise SSHException("Invalid port: %s" % data['port']) config_data = (self.hostname, self.port, self.username) if not all(config_data): raise SSHException("Missing configuration data in %s" % configfile) def _do_connect(self): """ Actually connect to the remote. :raise: SSHException if connection fails. """ self.load_system_host_keys() if self.username is None or self.port is None: self._configure() try: self.connect(hostname=self.hostname, port=self.port, username=self.username, key_filename=self.key_filename) except socket.error as e: raise SSHException("Failed to connect to server: %s" % e) def _connect(self): """ Connect to the remote if not already _connected. """ if not self.connected: try: self.lock.acquire() # Another thread may have _connected while we were # waiting to acquire the lock if not self.connected: self._do_connect() self.__connected.set() except SSHException: raise finally: self.lock.release()
[docs] def execute(self, command): """ Run the given command. Ensure we're _connected to the remote server, and run `command`. :return: the results as an `SSHCommandResult`. :raise: `ValueError` if `command` is not a string, or `SSHException` if command execution fails. """ if not isinstance(command, str): raise ValueError("command must be a string") self._connect() try: stdin, stdout, stderr = self.exec_command(command, bufsize=1, timeout=None, get_pty=False) except SSHException as err: raise SSHException("Command execution error: %s" % err) return SSHCommandResult(command, stdin, stdout, stderr)
@property
[docs] def connected(self): ''' Does the client have an open conection? :return: True if the client is connected ''' return self.__connected.is_set()
[docs] def disconnect(self): ''' Close any open connection :return: self to allow chaining ''' self.lock.acquire() try: if self.connected: self.close() self.__connected.clear() finally: self.lock.release() return self