| Adam Israel | a2a2d89 | 2017-03-30 21:39:26 -0400 | [diff] [blame] | 1 | ## |
| 2 | # Copyright 2016 Canonical Ltd. |
| 3 | # All rights reserved. |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 6 | # not use this file except in compliance with the License. You may obtain |
| 7 | # a copy of the License at |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 14 | # License for the specific language governing permissions and limitations |
| 15 | # under the License. |
| 16 | ## |
| 17 | |
| 18 | from charmhelpers.core.hookenv import ( |
| 19 | config, |
| 20 | ) |
| 21 | import io |
| 22 | import paramiko |
| 23 | |
| 24 | from subprocess import ( |
| 25 | Popen, |
| 26 | CalledProcessError, |
| 27 | PIPE, |
| 28 | ) |
| 29 | |
| 30 | |
| 31 | def _run(cmd, env=None): |
| 32 | """ Run a command, either on the local machine or remotely via SSH. """ |
| 33 | if isinstance(cmd, str): |
| 34 | cmd = cmd.split(' ') if ' ' in cmd else [cmd] |
| 35 | |
| 36 | cfg = config() |
| 37 | if all(k in cfg for k in ['ssh-hostname', 'ssh-username', |
| 38 | 'ssh-password', 'ssh-private-key']): |
| 39 | host = cfg['ssh-hostname'] |
| 40 | user = cfg['ssh-username'] |
| 41 | passwd = cfg['ssh-password'] |
| 42 | key = cfg['ssh-private-key'] |
| 43 | |
| 44 | if host and user and (passwd or key): |
| 45 | return ssh(cmd, host, user, passwd, key) |
| 46 | |
| 47 | p = Popen(cmd, |
| 48 | env=env, |
| 49 | shell=True, |
| 50 | stdout=PIPE, |
| 51 | stderr=PIPE) |
| 52 | stdout, stderr = p.communicate() |
| 53 | retcode = p.poll() |
| 54 | if retcode > 0: |
| 55 | raise CalledProcessError(returncode=retcode, |
| 56 | cmd=cmd, |
| 57 | output=stderr.decode("utf-8").strip()) |
| 58 | return (stdout.decode('utf-8').strip(), stderr.decode('utf-8').strip()) |
| 59 | |
| 60 | |
| 61 | def get_ssh_client(host, user, password=None, key=None): |
| 62 | """Return a connected Paramiko ssh object""" |
| 63 | |
| 64 | client = paramiko.SSHClient() |
| 65 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) |
| 66 | |
| 67 | pkey = None |
| 68 | if key: |
| 69 | f = io.StringIO(key) |
| 70 | pkey = paramiko.RSAKey.from_private_key(f) |
| 71 | |
| 72 | ########################################################################### |
| 73 | # There is a bug in some versions of OpenSSH 4.3 (CentOS/RHEL 5) where # |
| 74 | # the server may not send the SSH_MSG_USERAUTH_BANNER message except when # |
| 75 | # responding to an auth_none request. For example, paramiko will attempt # |
| 76 | # to use password authentication when a password is set, but the server # |
| 77 | # could deny that, instead requesting keyboard-interactive. The hack to # |
| 78 | # workaround this is to attempt a reconnect, which will receive the right # |
| 79 | # banner, and authentication can proceed. See the following for more info # |
| 80 | # https://github.com/paramiko/paramiko/issues/432 # |
| 81 | # https://github.com/paramiko/paramiko/pull/438 # |
| 82 | ########################################################################### |
| 83 | |
| 84 | try: |
| 85 | client.connect(host, port=22, username=user, |
| 86 | password=password, pkey=pkey) |
| 87 | except paramiko.ssh_exception.SSHException as e: |
| 88 | if 'Error reading SSH protocol banner' == str(e): |
| 89 | # Once more, with feeling |
| 90 | client.connect(host, port=22, username=user, |
| 91 | password=password, pkey=pkey) |
| 92 | pass |
| 93 | else: |
| 94 | raise paramiko.ssh_exception.SSHException(e) |
| 95 | |
| 96 | return client |
| 97 | |
| 98 | |
| 99 | def sftp(local_file, remote_file, host, user, password=None, key=None): |
| 100 | """Copy a local file to a remote host""" |
| 101 | client = get_ssh_client(host, user, password, key) |
| 102 | |
| 103 | # Create an sftp connection from the underlying transport |
| 104 | sftp = paramiko.SFTPClient.from_transport(client.get_transport()) |
| 105 | sftp.put(local_file, remote_file) |
| 106 | client.close() |
| 107 | |
| 108 | |
| 109 | def ssh(cmd, host, user, password=None, key=None): |
| 110 | """ Run an arbitrary command over SSH. """ |
| 111 | client = get_ssh_client(host, user, password, key) |
| 112 | |
| 113 | cmds = ' '.join(cmd) |
| 114 | stdin, stdout, stderr = client.exec_command(cmds, get_pty=True) |
| 115 | retcode = stdout.channel.recv_exit_status() |
| 116 | client.close() # @TODO re-use connections |
| 117 | if retcode > 0: |
| 118 | output = stderr.read().strip() |
| 119 | raise CalledProcessError(returncode=retcode, cmd=cmd, |
| 120 | output=output) |
| 121 | return ( |
| 122 | stdout.read().decode('utf-8').strip(), |
| 123 | stderr.read().decode('utf-8').strip() |
| 124 | ) |