2 # Copyright 2016 Canonical Ltd.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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
18 from charmhelpers
.core
.hookenv
import (
24 from subprocess
import (
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
]
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']
44 if host
and user
and (passwd
or key
):
45 return ssh(cmd
, host
, user
, passwd
, key
)
52 stdout
, stderr
= p
.communicate()
55 raise CalledProcessError(returncode
=retcode
,
57 output
=stderr
.decode("utf-8").strip())
58 return (stdout
.decode('utf-8').strip(), stderr
.decode('utf-8').strip())
61 def get_ssh_client(host
, user
, password
=None, key
=None):
62 """Return a connected Paramiko ssh object"""
64 client
= paramiko
.SSHClient()
65 client
.set_missing_host_key_policy(paramiko
.AutoAddPolicy())
70 pkey
= paramiko
.RSAKey
.from_private_key(f
)
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 ###########################################################################
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
)
94 raise paramiko
.ssh_exception
.SSHException(e
)
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
)
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
)
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
)
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
118 output
= stderr
.read().strip()
119 raise CalledProcessError(returncode
=retcode
, cmd
=cmd
,
122 stdout
.read().decode('utf-8').strip(),
123 stderr
.read().decode('utf-8').strip()