Statistics
| Branch: | Tag: | Revision:

root / ncclient / transport / ssh.py @ 4a3d4804

History | View | Annotate | Download (12.1 kB)

1 d095a59e Shikhar Bhushan
# Copyright 2009 Shikhar Bhushan
2 d095a59e Shikhar Bhushan
#
3 d095a59e Shikhar Bhushan
# Licensed under the Apache License, Version 2.0 (the "License");
4 d095a59e Shikhar Bhushan
# you may not use this file except in compliance with the License.
5 d095a59e Shikhar Bhushan
# You may obtain a copy of the License at
6 d095a59e Shikhar Bhushan
#
7 d095a59e Shikhar Bhushan
#    http://www.apache.org/licenses/LICENSE-2.0
8 d095a59e Shikhar Bhushan
#
9 d095a59e Shikhar Bhushan
# Unless required by applicable law or agreed to in writing, software
10 d095a59e Shikhar Bhushan
# distributed under the License is distributed on an "AS IS" BASIS,
11 d095a59e Shikhar Bhushan
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 d095a59e Shikhar Bhushan
# See the License for the specific language governing permissions and
13 d095a59e Shikhar Bhushan
# limitations under the License.
14 d095a59e Shikhar Bhushan
15 d095a59e Shikhar Bhushan
import os
16 d095a59e Shikhar Bhushan
import socket
17 c34a55c8 Shikhar Bhushan
import getpass
18 d095a59e Shikhar Bhushan
from binascii import hexlify
19 d095a59e Shikhar Bhushan
from cStringIO import StringIO
20 d095a59e Shikhar Bhushan
from select import select
21 d095a59e Shikhar Bhushan
22 d095a59e Shikhar Bhushan
import paramiko
23 d095a59e Shikhar Bhushan
24 c35cebbf Shikhar Bhushan
from errors import AuthenticationError, SessionCloseError, SSHError, SSHUnknownHostError
25 d095a59e Shikhar Bhushan
from session import Session
26 d095a59e Shikhar Bhushan
27 41e2ed46 Shikhar Bhushan
import logging
28 4bc8021f Shikhar Bhushan
logger = logging.getLogger("ncclient.transport.ssh")
29 41e2ed46 Shikhar Bhushan
30 d095a59e Shikhar Bhushan
BUF_SIZE = 4096
31 030b950d Shikhar Bhushan
MSG_DELIM = "]]>]]>"
32 d095a59e Shikhar Bhushan
TICK = 0.1
33 d095a59e Shikhar Bhushan
34 9a9af391 Shikhar Bhushan
def default_unknown_host_cb(host, fingerprint):
35 19e7c7f6 Shikhar Bhushan
    """An unknown host callback returns `True` if it finds the key acceptable, and `False` if not.
36 4f650d54 Shikhar Bhushan

37 19e7c7f6 Shikhar Bhushan
    This default callback always returns `False`, which would lead to :meth:`connect` raising a :exc:`SSHUnknownHost` exception.
38 4bc8021f Shikhar Bhushan
    
39 4bc8021f Shikhar Bhushan
    Supply another valid callback if you need to verify the host key programatically.
40 216bb34c Shikhar Bhushan

41 19e7c7f6 Shikhar Bhushan
    *host* is the hostname that needs to be verified
42 4f650d54 Shikhar Bhushan

43 19e7c7f6 Shikhar Bhushan
    *fingerprint* is a hex string representing the host key fingerprint, colon-delimited e.g. `"4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21"`
44 4f650d54 Shikhar Bhushan
    """
45 4f650d54 Shikhar Bhushan
    return False
46 4f650d54 Shikhar Bhushan
47 9a9af391 Shikhar Bhushan
def _colonify(fp):
48 9a9af391 Shikhar Bhushan
    finga = fp[:2]
49 9a9af391 Shikhar Bhushan
    for idx  in range(2, len(fp), 2):
50 9a9af391 Shikhar Bhushan
        finga += ":" + fp[idx:idx+2]
51 9a9af391 Shikhar Bhushan
    return finga
52 4f650d54 Shikhar Bhushan
53 d095a59e Shikhar Bhushan
class SSHSession(Session):
54 4f650d54 Shikhar Bhushan
55 4f650d54 Shikhar Bhushan
    "Implements a :rfc:`4742` NETCONF session over SSH."
56 4f650d54 Shikhar Bhushan
57 583c11f6 Shikhar Bhushan
    def __init__(self, capabilities):
58 583c11f6 Shikhar Bhushan
        Session.__init__(self, capabilities)
59 d095a59e Shikhar Bhushan
        self._host_keys = paramiko.HostKeys()
60 d095a59e Shikhar Bhushan
        self._transport = None
61 d095a59e Shikhar Bhushan
        self._connected = False
62 d095a59e Shikhar Bhushan
        self._channel = None
63 d095a59e Shikhar Bhushan
        self._buffer = StringIO() # for incoming data
64 d095a59e Shikhar Bhushan
        # parsing-related, see _parse()
65 4f650d54 Shikhar Bhushan
        self._parsing_state = 0
66 d095a59e Shikhar Bhushan
        self._parsing_pos = 0
67 9a9af391 Shikhar Bhushan
    
68 d095a59e Shikhar Bhushan
    def _parse(self):
69 19e7c7f6 Shikhar Bhushan
        "Messages ae delimited by MSG_DELIM. The buffer could have grown by a maximum of BUF_SIZE bytes everytime this method is called. Retains state across method calls and if a byte has been read it will not be considered again."
70 d095a59e Shikhar Bhushan
        delim = MSG_DELIM
71 d095a59e Shikhar Bhushan
        n = len(delim) - 1
72 d095a59e Shikhar Bhushan
        expect = self._parsing_state
73 d095a59e Shikhar Bhushan
        buf = self._buffer
74 d095a59e Shikhar Bhushan
        buf.seek(self._parsing_pos)
75 d095a59e Shikhar Bhushan
        while True:
76 d095a59e Shikhar Bhushan
            x = buf.read(1)
77 d095a59e Shikhar Bhushan
            if not x: # done reading
78 d095a59e Shikhar Bhushan
                break
79 d095a59e Shikhar Bhushan
            elif x == delim[expect]: # what we expected
80 d095a59e Shikhar Bhushan
                expect += 1 # expect the next delim char
81 d095a59e Shikhar Bhushan
            else:
82 0c608b53 Shikhar Bhushan
                expect = 0
83 d095a59e Shikhar Bhushan
                continue
84 d095a59e Shikhar Bhushan
            # loop till last delim char expected, break if other char encountered
85 d095a59e Shikhar Bhushan
            for i in range(expect, n):
86 d095a59e Shikhar Bhushan
                x = buf.read(1)
87 d095a59e Shikhar Bhushan
                if not x: # done reading
88 d095a59e Shikhar Bhushan
                    break
89 d095a59e Shikhar Bhushan
                if x == delim[expect]: # what we expected
90 d095a59e Shikhar Bhushan
                    expect += 1 # expect the next delim char
91 d095a59e Shikhar Bhushan
                else:
92 d095a59e Shikhar Bhushan
                    expect = 0 # reset
93 d095a59e Shikhar Bhushan
                    break
94 d095a59e Shikhar Bhushan
            else: # if we didn't break out of the loop, full delim was parsed
95 d095a59e Shikhar Bhushan
                msg_till = buf.tell() - n
96 d095a59e Shikhar Bhushan
                buf.seek(0)
97 41e2ed46 Shikhar Bhushan
                logger.debug('parsed new message')
98 41e2ed46 Shikhar Bhushan
                self._dispatch_message(buf.read(msg_till).strip())
99 d095a59e Shikhar Bhushan
                buf.seek(n+1, os.SEEK_CUR)
100 d095a59e Shikhar Bhushan
                rest = buf.read()
101 d095a59e Shikhar Bhushan
                buf = StringIO()
102 d095a59e Shikhar Bhushan
                buf.write(rest)
103 d095a59e Shikhar Bhushan
                buf.seek(0)
104 4ba5e843 Shikhar Bhushan
                expect = 0
105 d095a59e Shikhar Bhushan
        self._buffer = buf
106 d095a59e Shikhar Bhushan
        self._parsing_state = expect
107 d095a59e Shikhar Bhushan
        self._parsing_pos = self._buffer.tell()
108 4f650d54 Shikhar Bhushan
109 216bb34c Shikhar Bhushan
    def load_known_hosts(self, filename=None):
110 19e7c7f6 Shikhar Bhushan
        """Load host keys from an openssh :file:`known_hosts`-style file. Can be called multiple times.
111 216bb34c Shikhar Bhushan

112 19e7c7f6 Shikhar Bhushan
        If *filename* is not specified, looks in the default locations i.e. :file:`~/.ssh/known_hosts` and :file:`~/ssh/known_hosts` for Windows.
113 216bb34c Shikhar Bhushan
        """
114 d095a59e Shikhar Bhushan
        if filename is None:
115 d095a59e Shikhar Bhushan
            filename = os.path.expanduser('~/.ssh/known_hosts')
116 d095a59e Shikhar Bhushan
            try:
117 216bb34c Shikhar Bhushan
                self._host_keys.load(filename)
118 d095a59e Shikhar Bhushan
            except IOError:
119 4ba5e843 Shikhar Bhushan
                # for windows
120 4ba5e843 Shikhar Bhushan
                filename = os.path.expanduser('~/ssh/known_hosts')
121 4ba5e843 Shikhar Bhushan
                try:
122 216bb34c Shikhar Bhushan
                    self._host_keys.load(filename)
123 4ba5e843 Shikhar Bhushan
                except IOError:
124 4ba5e843 Shikhar Bhushan
                    pass
125 216bb34c Shikhar Bhushan
        else:
126 216bb34c Shikhar Bhushan
            self._host_keys.load(filename)
127 4f650d54 Shikhar Bhushan
128 d095a59e Shikhar Bhushan
    def close(self):
129 d095a59e Shikhar Bhushan
        if self._transport.is_active():
130 d095a59e Shikhar Bhushan
            self._transport.close()
131 d095a59e Shikhar Bhushan
        self._connected = False
132 4f650d54 Shikhar Bhushan
133 19e7c7f6 Shikhar Bhushan
    # REMEMBER to update transport.rst if sig. changes, since it is hardcoded there
134 19e7c7f6 Shikhar Bhushan
    def connect(self, host, port=830, timeout=None, unknown_host_cb=default_unknown_host_cb,
135 19e7c7f6 Shikhar Bhushan
                username=None, password=None, key_filename=None, allow_agent=True, look_for_keys=True):
136 19e7c7f6 Shikhar Bhushan
        """Connect via SSH and initialize the NETCONF session. First attempts the publickey authentication method and then password authentication.
137 4f650d54 Shikhar Bhushan

138 19e7c7f6 Shikhar Bhushan
        To disable attempting publickey authentication altogether, call with *allow_agent* and *look_for_keys* as `False`.
139 4f650d54 Shikhar Bhushan

140 d215b592 Shikhar Bhushan
        *host* is the hostname or IP address to connect to
141 4f650d54 Shikhar Bhushan

142 d215b592 Shikhar Bhushan
        *port* is by default 830, but some devices use the default SSH port of 22 so this may need to be specified
143 4f650d54 Shikhar Bhushan

144 d215b592 Shikhar Bhushan
        *timeout* is an optional timeout for socket connect
145 4f650d54 Shikhar Bhushan

146 d215b592 Shikhar Bhushan
        *unknown_host_cb* is called when the server host key is not recognized. It takes two arguments, the hostname and the fingerprint (see the signature of :func:`default_unknown_host_cb`)
147 4f650d54 Shikhar Bhushan

148 d215b592 Shikhar Bhushan
        *username* is the username to use for SSH authentication
149 4f650d54 Shikhar Bhushan

150 d215b592 Shikhar Bhushan
        *password* is the password used if using password authentication, or the passphrase to use for unlocking keys that require it
151 4f650d54 Shikhar Bhushan

152 d215b592 Shikhar Bhushan
        *key_filename* is a filename where a the private key to be used can be found
153 4f650d54 Shikhar Bhushan

154 d215b592 Shikhar Bhushan
        *allow_agent* enables querying SSH agent (if found) for keys
155 4f650d54 Shikhar Bhushan

156 d215b592 Shikhar Bhushan
        *look_for_keys* enables looking in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`)
157 4f650d54 Shikhar Bhushan
        """
158 bb700ea5 Shikhar Bhushan
        if username is None:
159 68ac4439 Shikhar Bhushan
            username = getpass.getuser()
160 68ac4439 Shikhar Bhushan
        
161 bb700ea5 Shikhar Bhushan
        sock = None
162 bb700ea5 Shikhar Bhushan
        for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
163 bb700ea5 Shikhar Bhushan
            af, socktype, proto, canonname, sa = res
164 bb700ea5 Shikhar Bhushan
            try:
165 bb700ea5 Shikhar Bhushan
                sock = socket.socket(af, socktype, proto)
166 bb700ea5 Shikhar Bhushan
                sock.settimeout(timeout)
167 bb700ea5 Shikhar Bhushan
            except socket.error:
168 bb700ea5 Shikhar Bhushan
                continue
169 bb700ea5 Shikhar Bhushan
            try:
170 bb700ea5 Shikhar Bhushan
                sock.connect(sa)
171 bb700ea5 Shikhar Bhushan
            except socket.error:
172 bb700ea5 Shikhar Bhushan
                sock.close()
173 bb700ea5 Shikhar Bhushan
                continue
174 bb700ea5 Shikhar Bhushan
            break
175 d095a59e Shikhar Bhushan
        else:
176 495b9bf7 Shikhar Bhushan
            raise SSHError("Could not open socket to %s:%s" % (host, port))
177 bb700ea5 Shikhar Bhushan
178 d095a59e Shikhar Bhushan
        t = self._transport = paramiko.Transport(sock)
179 d095a59e Shikhar Bhushan
        t.set_log_channel(logger.name)
180 4f650d54 Shikhar Bhushan
181 d095a59e Shikhar Bhushan
        try:
182 d095a59e Shikhar Bhushan
            t.start_client()
183 d095a59e Shikhar Bhushan
        except paramiko.SSHException:
184 d095a59e Shikhar Bhushan
            raise SSHError('Negotiation failed')
185 4f650d54 Shikhar Bhushan
186 d095a59e Shikhar Bhushan
        # host key verification
187 d095a59e Shikhar Bhushan
        server_key = t.get_remote_server_key()
188 216bb34c Shikhar Bhushan
        known_host = self._host_keys.check(host, server_key)
189 216bb34c Shikhar Bhushan
190 9a9af391 Shikhar Bhushan
        fingerprint = _colonify(hexlify(server_key.get_fingerprint()))
191 4f650d54 Shikhar Bhushan
192 216bb34c Shikhar Bhushan
        if not known_host and not unknown_host_cb(host, fingerprint):
193 216bb34c Shikhar Bhushan
            raise SSHUnknownHostError(host, fingerprint)
194 4f650d54 Shikhar Bhushan
195 d095a59e Shikhar Bhushan
        if key_filename is None:
196 d095a59e Shikhar Bhushan
            key_filenames = []
197 d095a59e Shikhar Bhushan
        elif isinstance(key_filename, basestring):
198 d095a59e Shikhar Bhushan
            key_filenames = [ key_filename ]
199 d095a59e Shikhar Bhushan
        else:
200 d095a59e Shikhar Bhushan
            key_filenames = key_filename
201 4f650d54 Shikhar Bhushan
202 d095a59e Shikhar Bhushan
        self._auth(username, password, key_filenames, allow_agent, look_for_keys)
203 4f650d54 Shikhar Bhushan
204 d095a59e Shikhar Bhushan
        self._connected = True # there was no error authenticating
205 4f650d54 Shikhar Bhushan
206 d095a59e Shikhar Bhushan
        c = self._channel = self._transport.open_session()
207 030b950d Shikhar Bhushan
        c.set_name("netconf")
208 030b950d Shikhar Bhushan
        c.invoke_subsystem("netconf")
209 4f650d54 Shikhar Bhushan
210 d095a59e Shikhar Bhushan
        self._post_connect()
211 9a9af391 Shikhar Bhushan
    
212 d095a59e Shikhar Bhushan
    # on the lines of paramiko.SSHClient._auth()
213 d095a59e Shikhar Bhushan
    def _auth(self, username, password, key_filenames, allow_agent,
214 d095a59e Shikhar Bhushan
              look_for_keys):
215 d095a59e Shikhar Bhushan
        saved_exception = None
216 4f650d54 Shikhar Bhushan
217 d095a59e Shikhar Bhushan
        for key_filename in key_filenames:
218 d095a59e Shikhar Bhushan
            for cls in (paramiko.RSAKey, paramiko.DSSKey):
219 d095a59e Shikhar Bhushan
                try:
220 d095a59e Shikhar Bhushan
                    key = cls.from_private_key_file(key_filename, password)
221 030b950d Shikhar Bhushan
                    logger.debug("Trying key %s from %s" %
222 d095a59e Shikhar Bhushan
                              (hexlify(key.get_fingerprint()), key_filename))
223 d095a59e Shikhar Bhushan
                    self._transport.auth_publickey(username, key)
224 d095a59e Shikhar Bhushan
                    return
225 d095a59e Shikhar Bhushan
                except Exception as e:
226 d095a59e Shikhar Bhushan
                    saved_exception = e
227 d095a59e Shikhar Bhushan
                    logger.debug(e)
228 4f650d54 Shikhar Bhushan
229 d095a59e Shikhar Bhushan
        if allow_agent:
230 d095a59e Shikhar Bhushan
            for key in paramiko.Agent().get_keys():
231 d095a59e Shikhar Bhushan
                try:
232 030b950d Shikhar Bhushan
                    logger.debug("Trying SSH agent key %s" %
233 d095a59e Shikhar Bhushan
                                 hexlify(key.get_fingerprint()))
234 d095a59e Shikhar Bhushan
                    self._transport.auth_publickey(username, key)
235 d095a59e Shikhar Bhushan
                    return
236 d095a59e Shikhar Bhushan
                except Exception as e:
237 d095a59e Shikhar Bhushan
                    saved_exception = e
238 d095a59e Shikhar Bhushan
                    logger.debug(e)
239 4f650d54 Shikhar Bhushan
240 d095a59e Shikhar Bhushan
        keyfiles = []
241 d095a59e Shikhar Bhushan
        if look_for_keys:
242 030b950d Shikhar Bhushan
            rsa_key = os.path.expanduser("~/.ssh/id_rsa")
243 030b950d Shikhar Bhushan
            dsa_key = os.path.expanduser("~/.ssh/id_dsa")
244 d095a59e Shikhar Bhushan
            if os.path.isfile(rsa_key):
245 d095a59e Shikhar Bhushan
                keyfiles.append((paramiko.RSAKey, rsa_key))
246 d095a59e Shikhar Bhushan
            if os.path.isfile(dsa_key):
247 d095a59e Shikhar Bhushan
                keyfiles.append((paramiko.DSSKey, dsa_key))
248 d095a59e Shikhar Bhushan
            # look in ~/ssh/ for windows users:
249 030b950d Shikhar Bhushan
            rsa_key = os.path.expanduser("~/ssh/id_rsa")
250 030b950d Shikhar Bhushan
            dsa_key = os.path.expanduser("~/ssh/id_dsa")
251 d095a59e Shikhar Bhushan
            if os.path.isfile(rsa_key):
252 d095a59e Shikhar Bhushan
                keyfiles.append((paramiko.RSAKey, rsa_key))
253 d095a59e Shikhar Bhushan
            if os.path.isfile(dsa_key):
254 d095a59e Shikhar Bhushan
                keyfiles.append((paramiko.DSSKey, dsa_key))
255 4f650d54 Shikhar Bhushan
256 d095a59e Shikhar Bhushan
        for cls, filename in keyfiles:
257 d095a59e Shikhar Bhushan
            try:
258 d095a59e Shikhar Bhushan
                key = cls.from_private_key_file(filename, password)
259 030b950d Shikhar Bhushan
                logger.debug("Trying discovered key %s in %s" %
260 d095a59e Shikhar Bhushan
                          (hexlify(key.get_fingerprint()), filename))
261 d095a59e Shikhar Bhushan
                self._transport.auth_publickey(username, key)
262 d095a59e Shikhar Bhushan
                return
263 d095a59e Shikhar Bhushan
            except Exception as e:
264 d095a59e Shikhar Bhushan
                saved_exception = e
265 d095a59e Shikhar Bhushan
                logger.debug(e)
266 4f650d54 Shikhar Bhushan
267 d095a59e Shikhar Bhushan
        if password is not None:
268 d095a59e Shikhar Bhushan
            try:
269 d095a59e Shikhar Bhushan
                self._transport.auth_password(username, password)
270 d095a59e Shikhar Bhushan
                return
271 d095a59e Shikhar Bhushan
            except Exception as e:
272 d095a59e Shikhar Bhushan
                saved_exception = e
273 d095a59e Shikhar Bhushan
                logger.debug(e)
274 4f650d54 Shikhar Bhushan
275 d095a59e Shikhar Bhushan
        if saved_exception is not None:
276 541247ba Shikhar Bhushan
            # need pep-3134 to do this right
277 a7cb58ce Shikhar Bhushan
            raise AuthenticationError(repr(saved_exception))
278 4f650d54 Shikhar Bhushan
279 030b950d Shikhar Bhushan
        raise AuthenticationError("No authentication methods available")
280 4f650d54 Shikhar Bhushan
281 d095a59e Shikhar Bhushan
    def run(self):
282 d095a59e Shikhar Bhushan
        chan = self._channel
283 d095a59e Shikhar Bhushan
        q = self._q
284 d095a59e Shikhar Bhushan
        try:
285 d095a59e Shikhar Bhushan
            while True:
286 19e7c7f6 Shikhar Bhushan
                # select on a paramiko ssh channel object does not ever return it in the writable list, so channels don't exactly emulate the socket api
287 d095a59e Shikhar Bhushan
                r, w, e = select([chan], [], [], TICK)
288 19e7c7f6 Shikhar Bhushan
                # will wakeup evey TICK seconds to check if something to send, more if something to read (due to select returning chan in readable list)
289 d095a59e Shikhar Bhushan
                if r:
290 d095a59e Shikhar Bhushan
                    data = chan.recv(BUF_SIZE)
291 d095a59e Shikhar Bhushan
                    if data:
292 d095a59e Shikhar Bhushan
                        self._buffer.write(data)
293 d095a59e Shikhar Bhushan
                        self._parse()
294 d095a59e Shikhar Bhushan
                    else:
295 94265508 Shikhar Bhushan
                        raise SessionCloseError(self._buffer.getvalue())
296 d095a59e Shikhar Bhushan
                if not q.empty() and chan.send_ready():
297 030b950d Shikhar Bhushan
                    logger.debug("Sending message")
298 d095a59e Shikhar Bhushan
                    data = q.get() + MSG_DELIM
299 d095a59e Shikhar Bhushan
                    while data:
300 d095a59e Shikhar Bhushan
                        n = chan.send(data)
301 d095a59e Shikhar Bhushan
                        if n <= 0:
302 94265508 Shikhar Bhushan
                            raise SessionCloseError(self._buffer.getvalue(), data)
303 d095a59e Shikhar Bhushan
                        data = data[n:]
304 d095a59e Shikhar Bhushan
        except Exception as e:
305 030b950d Shikhar Bhushan
            logger.debug("Broke out of main loop, error=%r", e)
306 1d540e60 Shikhar Bhushan
            self.close()
307 564bee4f Shikhar Bhushan
            self._dispatch_error(e)
308 4f650d54 Shikhar Bhushan
309 d095a59e Shikhar Bhushan
    @property
310 d095a59e Shikhar Bhushan
    def transport(self):
311 c15671aa Shikhar Bhushan
        "Underlying `paramiko.Transport <http://www.lag.net/paramiko/docs/paramiko.Transport-class.html>`_ object. This makes it possible to call methods like :meth:`~paramiko.Transport.set_keepalive` on it."
312 d095a59e Shikhar Bhushan
        return self._transport