b247bcf2b52b015c1d84a15b35f439bb35b89ed2
[osm/SO.git] / charms / layers / sshproxy / lib / charms / sshproxy.py
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 )