| ## |
| # Copyright 2016 Canonical Ltd. |
| # All rights reserved. |
| # |
| # Licensed 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. |
| ## |
| |
| from charmhelpers.core.hookenv import ( |
| config, |
| ) |
| import io |
| import paramiko |
| |
| from subprocess import ( |
| Popen, |
| CalledProcessError, |
| PIPE, |
| ) |
| |
| |
| def _run(cmd, env=None): |
| """ Run a command, either on the local machine or remotely via SSH. """ |
| if isinstance(cmd, str): |
| cmd = cmd.split(' ') if ' ' in cmd else [cmd] |
| |
| cfg = config() |
| if all(k in cfg for k in ['ssh-hostname', 'ssh-username', |
| 'ssh-password', 'ssh-private-key']): |
| host = cfg['ssh-hostname'] |
| user = cfg['ssh-username'] |
| passwd = cfg['ssh-password'] |
| key = cfg['ssh-private-key'] |
| |
| if host and user and (passwd or key): |
| return ssh(cmd, host, user, passwd, key) |
| |
| p = Popen(cmd, |
| env=env, |
| shell=True, |
| stdout=PIPE, |
| stderr=PIPE) |
| stdout, stderr = p.communicate() |
| retcode = p.poll() |
| if retcode > 0: |
| raise CalledProcessError(returncode=retcode, |
| cmd=cmd, |
| output=stderr.decode("utf-8").strip()) |
| return (stdout.decode('utf-8').strip(), stderr.decode('utf-8').strip()) |
| |
| |
| def get_ssh_client(host, user, password=None, key=None): |
| """Return a connected Paramiko ssh object""" |
| |
| client = paramiko.SSHClient() |
| client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) |
| |
| pkey = None |
| if key: |
| f = io.StringIO(key) |
| pkey = paramiko.RSAKey.from_private_key(f) |
| |
| ########################################################################### |
| # There is a bug in some versions of OpenSSH 4.3 (CentOS/RHEL 5) where # |
| # the server may not send the SSH_MSG_USERAUTH_BANNER message except when # |
| # responding to an auth_none request. For example, paramiko will attempt # |
| # to use password authentication when a password is set, but the server # |
| # could deny that, instead requesting keyboard-interactive. The hack to # |
| # workaround this is to attempt a reconnect, which will receive the right # |
| # banner, and authentication can proceed. See the following for more info # |
| # https://github.com/paramiko/paramiko/issues/432 # |
| # https://github.com/paramiko/paramiko/pull/438 # |
| ########################################################################### |
| |
| try: |
| client.connect(host, port=22, username=user, |
| password=password, pkey=pkey) |
| except paramiko.ssh_exception.SSHException as e: |
| if 'Error reading SSH protocol banner' == str(e): |
| # Once more, with feeling |
| client.connect(host, port=22, username=user, |
| password=password, pkey=pkey) |
| pass |
| else: |
| raise paramiko.ssh_exception.SSHException(e) |
| |
| return client |
| |
| |
| def sftp(local_file, remote_file, host, user, password=None, key=None): |
| """Copy a local file to a remote host""" |
| client = get_ssh_client(host, user, password, key) |
| |
| # Create an sftp connection from the underlying transport |
| sftp = paramiko.SFTPClient.from_transport(client.get_transport()) |
| sftp.put(local_file, remote_file) |
| client.close() |
| |
| |
| def ssh(cmd, host, user, password=None, key=None): |
| """ Run an arbitrary command over SSH. """ |
| client = get_ssh_client(host, user, password, key) |
| |
| cmds = ' '.join(cmd) |
| stdin, stdout, stderr = client.exec_command(cmds, get_pty=True) |
| retcode = stdout.channel.recv_exit_status() |
| client.close() # @TODO re-use connections |
| if retcode > 0: |
| output = stderr.read().strip() |
| raise CalledProcessError(returncode=retcode, cmd=cmd, |
| output=output) |
| return ( |
| stdout.read().decode('utf-8').strip(), |
| stderr.read().decode('utf-8').strip() |
| ) |