| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """ |
| Wraps multiple ways to communicate over SSH |
| """ |
| have_paramiko = False |
| |
| try: |
| import paramiko |
| have_paramiko = True |
| except ImportError: |
| pass |
| |
| # Depending on your version of Paramiko, it may cause a deprecation |
| # warning on Python 2.6. |
| # Ref: https://bugs.launchpad.net/paramiko/+bug/392973 |
| |
| import os |
| import subprocess |
| import logging |
| |
| from os.path import split as psplit |
| from os.path import join as pjoin |
| |
| |
| class BaseSSHClient(object): |
| """ |
| Base class representing a connection over SSH/SCP to a remote node. |
| """ |
| |
| def __init__(self, hostname, port=22, username='root', password=None, |
| key=None, timeout=None): |
| """ |
| @type hostname: C{str} |
| @keyword hostname: Hostname or IP address to connect to. |
| |
| @type port: C{int} |
| @keyword port: TCP port to communicate on, defaults to 22. |
| |
| @type username: C{str} |
| @keyword username: Username to use, defaults to root. |
| |
| @type password: C{str} |
| @keyword password: Password to authenticate with. |
| |
| @type key: C{list} |
| @keyword key: Private SSH keys to authenticate with. |
| """ |
| self.hostname = hostname |
| self.port = port |
| self.username = username |
| self.password = password |
| self.key = key |
| self.timeout = timeout |
| |
| def connect(self): |
| """ |
| Connect to the remote node over SSH. |
| |
| @return: True if the connection has been successfuly established, False |
| otherwise. |
| @rtype: C{bool} |
| """ |
| raise NotImplementedError( |
| 'connect not implemented for this ssh client') |
| |
| def put(self, path, contents=None, chmod=None, mode='w'): |
| """ |
| Upload a file to the remote node. |
| |
| @type path: C{str} |
| @keyword path: File path on the remote node. |
| |
| @type contents: C{str} |
| @keyword contents: File Contents. |
| |
| @type chmod: C{int} |
| @keyword chmod: chmod file to this after creation. |
| |
| @type mode: C{str} |
| @keyword mode: Mode in which the file is opened. |
| |
| @return: Full path to the location where a file has been saved. |
| @rtype: C{str} |
| """ |
| raise NotImplementedError( |
| 'put not implemented for this ssh client') |
| |
| def delete(self, path): |
| """ |
| Delete/Unlink a file on the remote node. |
| |
| @type path: C{str} |
| @keyword path: File path on the remote node. |
| |
| @return: True if the file has been successfuly deleted, False |
| otherwise. |
| @rtype: C{bool} |
| """ |
| raise NotImplementedError( |
| 'delete not implemented for this ssh client') |
| |
| def run(self, cmd): |
| """ |
| Run a command on a remote node. |
| |
| @type cmd: C{str} |
| @keyword cmd: Command to run. |
| |
| @return C{list} of [stdout, stderr, exit_status] |
| """ |
| raise NotImplementedError( |
| 'run not implemented for this ssh client') |
| |
| def close(self): |
| """ |
| Shutdown connection to the remote node. |
| |
| @return: True if the connection has been successfuly closed, False |
| otherwise. |
| @rtype: C{bool} |
| """ |
| raise NotImplementedError( |
| 'close not implemented for this ssh client') |
| |
| |
| class ParamikoSSHClient(BaseSSHClient): |
| |
| """ |
| A SSH Client powered by Paramiko. |
| """ |
| def __init__(self, hostname, port=22, username='root', password=None, |
| key=None, timeout=None): |
| super(ParamikoSSHClient, self).__init__(hostname, port, username, |
| password, key, timeout) |
| self.client = paramiko.SSHClient() |
| self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) |
| |
| def connect(self): |
| conninfo = {'hostname': self.hostname, |
| 'port': self.port, |
| 'username': self.username, |
| 'allow_agent': False, |
| 'look_for_keys': False} |
| |
| if self.password: |
| conninfo['password'] = self.password |
| elif self.key: |
| conninfo['key_filename'] = self.key |
| else: |
| conninfo['allow_agent'] = True |
| conninfo['look_for_keys'] = True |
| |
| if self.timeout: |
| conninfo['timeout'] = self.timeout |
| |
| self.client.connect(**conninfo) |
| return True |
| |
| def put(self, path, contents=None, chmod=None, mode='w'): |
| sftp = self.client.open_sftp() |
| # less than ideal, but we need to mkdir stuff otherwise file() fails |
| head, tail = psplit(path) |
| |
| if path[0] == "/": |
| sftp.chdir("/") |
| else: |
| # Relative path - start from a home directory (~) |
| sftp.chdir('.') |
| |
| for part in head.split("/"): |
| if part != "": |
| try: |
| sftp.mkdir(part) |
| except IOError: |
| # so, there doesn't seem to be a way to |
| # catch EEXIST consistently *sigh* |
| pass |
| sftp.chdir(part) |
| |
| cwd = sftp.getcwd() |
| |
| ak = sftp.file(tail, mode=mode) |
| ak.write(contents) |
| if chmod is not None: |
| ak.chmod(chmod) |
| ak.close() |
| sftp.close() |
| |
| if path[0] == '/': |
| file_path = path |
| else: |
| file_path = pjoin(cwd, path) |
| |
| return file_path |
| |
| def delete(self, path): |
| sftp = self.client.open_sftp() |
| sftp.unlink(path) |
| sftp.close() |
| return True |
| |
| def run(self, cmd): |
| # based on exec_command() |
| bufsize = -1 |
| t = self.client.get_transport() |
| chan = t.open_session() |
| chan.exec_command(cmd) |
| stdin = chan.makefile('wb', bufsize) |
| stdout = chan.makefile('rb', bufsize) |
| stderr = chan.makefile_stderr('rb', bufsize) |
| #stdin, stdout, stderr = self.client.exec_command(cmd) |
| stdin.close() |
| status = chan.recv_exit_status() |
| so = stdout.read() |
| se = stderr.read() |
| return [so, se, status] |
| |
| def close(self): |
| self.client.close() |
| return True |
| |
| |
| class ShellOutSSHClient(BaseSSHClient): |
| """ |
| This client shells out to "ssh" binary to run commands on the remote |
| server. |
| |
| Note: This client should not be used in production. |
| """ |
| |
| def __init__(self, hostname, port=22, username='root', password=None, |
| key=None, timeout=None): |
| super(ShellOutSSHClient, self).__init__(hostname, port, username, |
| password, key, timeout) |
| if self.password: |
| raise ValueError('ShellOutSSHClient only supports key auth') |
| |
| child = subprocess.Popen(['ssh'], stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| child.communicate() |
| |
| if child.returncode == 127: |
| raise ValueError('ssh client is not available') |
| |
| self.logger = self._get_and_setup_logger() |
| |
| def connect(self): |
| """ |
| This client doesn't support persistent connections establish a new |
| connection every time "run" method is called. |
| """ |
| return True |
| |
| def run(self, cmd): |
| return self._run_remote_shell_command([cmd]) |
| |
| def put(self, path, contents=None, chmod=None, mode='w'): |
| if mode == 'w': |
| redirect = '>' |
| elif mode == 'a': |
| redirect = '>>' |
| else: |
| raise ValueError('Invalid mode: ' + mode) |
| |
| cmd = ['echo "%s" %s %s' % (contents, redirect, path)] |
| self._run_remote_shell_command(cmd) |
| return path |
| |
| def delete(self, path): |
| cmd = ['rm', '-rf', path] |
| self._run_remote_shell_command(cmd) |
| return True |
| |
| def close(self): |
| return True |
| |
| def _get_and_setup_logger(self): |
| logger = logging.getLogger('libcloud.compute.ssh') |
| path = os.getenv('LIBCLOUD_DEBUG') |
| |
| if path: |
| handler = logging.FileHandler(path) |
| logger.addHandler(handler) |
| logger.setLevel(logging.DEBUG) |
| |
| return logger |
| |
| def _get_base_ssh_command(self): |
| cmd = ['ssh'] |
| |
| if self.key: |
| cmd += ['-i', self.key] |
| |
| if self.timeout: |
| cmd += ['-oConnectTimeout=%s' % (self.timeout)] |
| |
| cmd += ['%s@%s' % (self.username, self.hostname)] |
| |
| return cmd |
| |
| def _run_remote_shell_command(self, cmd): |
| """ |
| Run a command on a remote server. |
| |
| @param cmd: Command to run. |
| @type cmd: C{list} of C{str} |
| |
| @return: Command stdout, stderr and status code. |
| @rtype: C{tuple} |
| """ |
| base_cmd = self._get_base_ssh_command() |
| full_cmd = base_cmd + [' '.join(cmd)] |
| |
| self.logger.debug('Executing command: "%s"' % (' '.join(full_cmd))) |
| |
| child = subprocess.Popen(full_cmd, stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| stdout, stderr = child.communicate() |
| return (stdout, stderr, child.returncode) |
| |
| |
| class MockSSHClient(BaseSSHClient): |
| pass |
| |
| |
| SSHClient = ParamikoSSHClient |
| if not have_paramiko: |
| SSHClient = MockSSHClient |