Statistics
| Branch: | Tag: | Revision:

root / ncclient / transport / ssh.py @ 583c11f6

History | View | Annotate | Download (10.6 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 d095a59e Shikhar Bhushan
from binascii import hexlify
18 d095a59e Shikhar Bhushan
from cStringIO import StringIO
19 d095a59e Shikhar Bhushan
from select import select
20 d095a59e Shikhar Bhushan
21 d095a59e Shikhar Bhushan
import paramiko
22 d095a59e Shikhar Bhushan
23 c35cebbf Shikhar Bhushan
from errors import AuthenticationError, SessionCloseError, SSHError, SSHUnknownHostError
24 d095a59e Shikhar Bhushan
from session import Session
25 d095a59e Shikhar Bhushan
26 41e2ed46 Shikhar Bhushan
import logging
27 41e2ed46 Shikhar Bhushan
logger = logging.getLogger('ncclient.transport.ssh')
28 41e2ed46 Shikhar Bhushan
29 d095a59e Shikhar Bhushan
BUF_SIZE = 4096
30 d095a59e Shikhar Bhushan
MSG_DELIM = ']]>]]>'
31 d095a59e Shikhar Bhushan
TICK = 0.1
32 d095a59e Shikhar Bhushan
33 d095a59e Shikhar Bhushan
class SSHSession(Session):
34 e91a5349 Shikhar Bhushan
    
35 583c11f6 Shikhar Bhushan
    "A NETCONF SSH session, per :rfc: 4742"
36 583c11f6 Shikhar Bhushan
    
37 583c11f6 Shikhar Bhushan
    def __init__(self, capabilities):
38 583c11f6 Shikhar Bhushan
        Session.__init__(self, capabilities)
39 d095a59e Shikhar Bhushan
        self._host_keys = paramiko.HostKeys()
40 4ba5e843 Shikhar Bhushan
        self._system_host_keys = paramiko.HostKeys()
41 d095a59e Shikhar Bhushan
        self._transport = None
42 d095a59e Shikhar Bhushan
        self._connected = False
43 d095a59e Shikhar Bhushan
        self._channel = None
44 1d540e60 Shikhar Bhushan
        self._expecting_close = False
45 d095a59e Shikhar Bhushan
        self._buffer = StringIO() # for incoming data
46 d095a59e Shikhar Bhushan
        # parsing-related, see _parse()
47 d095a59e Shikhar Bhushan
        self._parsing_state = 0 
48 d095a59e Shikhar Bhushan
        self._parsing_pos = 0
49 d095a59e Shikhar Bhushan
    
50 d095a59e Shikhar Bhushan
    def _parse(self):
51 d095a59e Shikhar Bhushan
        '''Messages ae delimited by MSG_DELIM. The buffer could have grown by a
52 d095a59e Shikhar Bhushan
        maximum of BUF_SIZE bytes everytime this method is called. Retains state
53 d095a59e Shikhar Bhushan
        across method calls and if a byte has been read it will not be considered
54 d095a59e Shikhar Bhushan
        again.
55 d095a59e Shikhar Bhushan
        '''
56 d095a59e Shikhar Bhushan
        delim = MSG_DELIM
57 d095a59e Shikhar Bhushan
        n = len(delim) - 1
58 d095a59e Shikhar Bhushan
        expect = self._parsing_state
59 d095a59e Shikhar Bhushan
        buf = self._buffer
60 d095a59e Shikhar Bhushan
        buf.seek(self._parsing_pos)
61 d095a59e Shikhar Bhushan
        while True:
62 d095a59e Shikhar Bhushan
            x = buf.read(1)
63 d095a59e Shikhar Bhushan
            if not x: # done reading
64 d095a59e Shikhar Bhushan
                break
65 d095a59e Shikhar Bhushan
            elif x == delim[expect]: # what we expected
66 d095a59e Shikhar Bhushan
                expect += 1 # expect the next delim char
67 d095a59e Shikhar Bhushan
            else:
68 d095a59e Shikhar Bhushan
                continue
69 d095a59e Shikhar Bhushan
            # loop till last delim char expected, break if other char encountered
70 d095a59e Shikhar Bhushan
            for i in range(expect, n):
71 d095a59e Shikhar Bhushan
                x = buf.read(1)
72 d095a59e Shikhar Bhushan
                if not x: # done reading
73 d095a59e Shikhar Bhushan
                    break
74 d095a59e Shikhar Bhushan
                if x == delim[expect]: # what we expected
75 d095a59e Shikhar Bhushan
                    expect += 1 # expect the next delim char
76 d095a59e Shikhar Bhushan
                else:
77 d095a59e Shikhar Bhushan
                    expect = 0 # reset
78 d095a59e Shikhar Bhushan
                    break
79 d095a59e Shikhar Bhushan
            else: # if we didn't break out of the loop, full delim was parsed
80 d095a59e Shikhar Bhushan
                msg_till = buf.tell() - n
81 d095a59e Shikhar Bhushan
                buf.seek(0)
82 41e2ed46 Shikhar Bhushan
                logger.debug('parsed new message')
83 41e2ed46 Shikhar Bhushan
                self._dispatch_message(buf.read(msg_till).strip())
84 d095a59e Shikhar Bhushan
                buf.seek(n+1, os.SEEK_CUR)
85 d095a59e Shikhar Bhushan
                rest = buf.read()
86 d095a59e Shikhar Bhushan
                buf = StringIO()
87 d095a59e Shikhar Bhushan
                buf.write(rest)
88 d095a59e Shikhar Bhushan
                buf.seek(0)
89 4ba5e843 Shikhar Bhushan
                expect = 0
90 d095a59e Shikhar Bhushan
        self._buffer = buf
91 d095a59e Shikhar Bhushan
        self._parsing_state = expect
92 d095a59e Shikhar Bhushan
        self._parsing_pos = self._buffer.tell()
93 d095a59e Shikhar Bhushan
    
94 d095a59e Shikhar Bhushan
    def load_system_host_keys(self, filename=None):
95 d095a59e Shikhar Bhushan
        if filename is None:
96 d095a59e Shikhar Bhushan
            filename = os.path.expanduser('~/.ssh/known_hosts')
97 d095a59e Shikhar Bhushan
            try:
98 d095a59e Shikhar Bhushan
                self._system_host_keys.load(filename)
99 d095a59e Shikhar Bhushan
            except IOError:
100 4ba5e843 Shikhar Bhushan
                # for windows
101 4ba5e843 Shikhar Bhushan
                filename = os.path.expanduser('~/ssh/known_hosts')
102 4ba5e843 Shikhar Bhushan
                try:
103 4ba5e843 Shikhar Bhushan
                    self._system_host_keys.load(filename)
104 4ba5e843 Shikhar Bhushan
                except IOError:
105 4ba5e843 Shikhar Bhushan
                    pass
106 d095a59e Shikhar Bhushan
            return
107 d095a59e Shikhar Bhushan
        self._system_host_keys.load(filename)
108 d095a59e Shikhar Bhushan
    
109 d095a59e Shikhar Bhushan
    def load_host_keys(self, filename):
110 d095a59e Shikhar Bhushan
        self._host_keys.load(filename)
111 d095a59e Shikhar Bhushan
112 d095a59e Shikhar Bhushan
    def add_host_key(self, key):
113 d095a59e Shikhar Bhushan
        self._host_keys.add(key)
114 d095a59e Shikhar Bhushan
    
115 d095a59e Shikhar Bhushan
    def save_host_keys(self, filename):
116 d095a59e Shikhar Bhushan
        f = open(filename, 'w')
117 583c11f6 Shikhar Bhushan
        for host, keys in self._host_keys.iteritems():
118 d095a59e Shikhar Bhushan
            for keytype, key in keys.iteritems():
119 583c11f6 Shikhar Bhushan
                f.write('%s %s %s\n' % (host, keytype, key.get_base64()))
120 d095a59e Shikhar Bhushan
        f.close()    
121 d095a59e Shikhar Bhushan
    
122 d095a59e Shikhar Bhushan
    def close(self):
123 583c11f6 Shikhar Bhushan
        self._expecting_close = True
124 d095a59e Shikhar Bhushan
        if self._transport.is_active():
125 d095a59e Shikhar Bhushan
            self._transport.close()
126 d095a59e Shikhar Bhushan
        self._connected = False
127 d095a59e Shikhar Bhushan
    
128 583c11f6 Shikhar Bhushan
    def connect(self, host, port=830, timeout=None,
129 d095a59e Shikhar Bhushan
                unknown_host_cb=None, username=None, password=None,
130 d095a59e Shikhar Bhushan
                key_filename=None, allow_agent=True, look_for_keys=True):
131 d095a59e Shikhar Bhushan
        assert(username is not None)
132 d095a59e Shikhar Bhushan
        
133 d095a59e Shikhar Bhushan
        for (family, socktype, proto, canonname, sockaddr) in \
134 583c11f6 Shikhar Bhushan
        socket.getaddrinfo(host, port):
135 583c11f6 Shikhar Bhushan
            if socktype == socket.SOCK_STREAM:
136 d095a59e Shikhar Bhushan
                af = family
137 d095a59e Shikhar Bhushan
                addr = sockaddr
138 d095a59e Shikhar Bhushan
                break
139 d095a59e Shikhar Bhushan
        else:
140 583c11f6 Shikhar Bhushan
            raise SSHError('No suitable address family for %s' % host)
141 d095a59e Shikhar Bhushan
        sock = socket.socket(af, socket.SOCK_STREAM)
142 d095a59e Shikhar Bhushan
        sock.settimeout(timeout)
143 d095a59e Shikhar Bhushan
        sock.connect(addr)
144 d095a59e Shikhar Bhushan
        t = self._transport = paramiko.Transport(sock)
145 d095a59e Shikhar Bhushan
        t.set_log_channel(logger.name)
146 d095a59e Shikhar Bhushan
        
147 d095a59e Shikhar Bhushan
        try:
148 d095a59e Shikhar Bhushan
            t.start_client()
149 d095a59e Shikhar Bhushan
        except paramiko.SSHException:
150 d095a59e Shikhar Bhushan
            raise SSHError('Negotiation failed')
151 d095a59e Shikhar Bhushan
        
152 d095a59e Shikhar Bhushan
        # host key verification
153 d095a59e Shikhar Bhushan
        server_key = t.get_remote_server_key()
154 583c11f6 Shikhar Bhushan
        known_host = self._host_keys.check(host, server_key) or \
155 583c11f6 Shikhar Bhushan
                        self._system_host_keys.check(host, server_key)
156 d095a59e Shikhar Bhushan
        
157 d095a59e Shikhar Bhushan
        if unknown_host_cb is None:
158 d095a59e Shikhar Bhushan
            unknown_host_cb = lambda *args: False
159 583c11f6 Shikhar Bhushan
        if not known_host and not unknown_host_cb(host, server_key):
160 583c11f6 Shikhar Bhushan
                raise SSHUnknownHostError(host, server_key)
161 d095a59e Shikhar Bhushan
        
162 d095a59e Shikhar Bhushan
        if key_filename is None:
163 d095a59e Shikhar Bhushan
            key_filenames = []
164 d095a59e Shikhar Bhushan
        elif isinstance(key_filename, basestring):
165 d095a59e Shikhar Bhushan
            key_filenames = [ key_filename ]
166 d095a59e Shikhar Bhushan
        else:
167 d095a59e Shikhar Bhushan
            key_filenames = key_filename
168 d095a59e Shikhar Bhushan
        
169 d095a59e Shikhar Bhushan
        self._auth(username, password, key_filenames, allow_agent, look_for_keys)
170 d095a59e Shikhar Bhushan
        
171 d095a59e Shikhar Bhushan
        self._connected = True # there was no error authenticating
172 d095a59e Shikhar Bhushan
        
173 d095a59e Shikhar Bhushan
        c = self._channel = self._transport.open_session()
174 d095a59e Shikhar Bhushan
        c.set_name('netconf')
175 41e2ed46 Shikhar Bhushan
        c.invoke_subsystem('netconf')
176 d095a59e Shikhar Bhushan
        
177 d095a59e Shikhar Bhushan
        self._post_connect()
178 d095a59e Shikhar Bhushan
    
179 d095a59e Shikhar Bhushan
    # on the lines of paramiko.SSHClient._auth()
180 d095a59e Shikhar Bhushan
    def _auth(self, username, password, key_filenames, allow_agent,
181 d095a59e Shikhar Bhushan
              look_for_keys):
182 d095a59e Shikhar Bhushan
        saved_exception = None
183 d095a59e Shikhar Bhushan
        
184 d095a59e Shikhar Bhushan
        for key_filename in key_filenames:
185 d095a59e Shikhar Bhushan
            for cls in (paramiko.RSAKey, paramiko.DSSKey):
186 d095a59e Shikhar Bhushan
                try:
187 d095a59e Shikhar Bhushan
                    key = cls.from_private_key_file(key_filename, password)
188 d095a59e Shikhar Bhushan
                    logger.debug('Trying key %s from %s' %
189 d095a59e Shikhar Bhushan
                              (hexlify(key.get_fingerprint()), key_filename))
190 d095a59e Shikhar Bhushan
                    self._transport.auth_publickey(username, key)
191 d095a59e Shikhar Bhushan
                    return
192 d095a59e Shikhar Bhushan
                except Exception as e:
193 d095a59e Shikhar Bhushan
                    saved_exception = e
194 d095a59e Shikhar Bhushan
                    logger.debug(e)
195 d095a59e Shikhar Bhushan
        
196 d095a59e Shikhar Bhushan
        if allow_agent:
197 d095a59e Shikhar Bhushan
            for key in paramiko.Agent().get_keys():
198 d095a59e Shikhar Bhushan
                try:
199 d095a59e Shikhar Bhushan
                    logger.debug('Trying SSH agent key %s' %
200 d095a59e Shikhar Bhushan
                                 hexlify(key.get_fingerprint()))
201 d095a59e Shikhar Bhushan
                    self._transport.auth_publickey(username, key)
202 d095a59e Shikhar Bhushan
                    return
203 d095a59e Shikhar Bhushan
                except Exception as e:
204 d095a59e Shikhar Bhushan
                    saved_exception = e
205 d095a59e Shikhar Bhushan
                    logger.debug(e)
206 d095a59e Shikhar Bhushan
        
207 d095a59e Shikhar Bhushan
        keyfiles = []
208 d095a59e Shikhar Bhushan
        if look_for_keys:
209 d095a59e Shikhar Bhushan
            rsa_key = os.path.expanduser('~/.ssh/id_rsa')
210 d095a59e Shikhar Bhushan
            dsa_key = os.path.expanduser('~/.ssh/id_dsa')
211 d095a59e Shikhar Bhushan
            if os.path.isfile(rsa_key):
212 d095a59e Shikhar Bhushan
                keyfiles.append((paramiko.RSAKey, rsa_key))
213 d095a59e Shikhar Bhushan
            if os.path.isfile(dsa_key):
214 d095a59e Shikhar Bhushan
                keyfiles.append((paramiko.DSSKey, dsa_key))
215 d095a59e Shikhar Bhushan
            # look in ~/ssh/ for windows users:
216 d095a59e Shikhar Bhushan
            rsa_key = os.path.expanduser('~/ssh/id_rsa')
217 d095a59e Shikhar Bhushan
            dsa_key = os.path.expanduser('~/ssh/id_dsa')
218 d095a59e Shikhar Bhushan
            if os.path.isfile(rsa_key):
219 d095a59e Shikhar Bhushan
                keyfiles.append((paramiko.RSAKey, rsa_key))
220 d095a59e Shikhar Bhushan
            if os.path.isfile(dsa_key):
221 d095a59e Shikhar Bhushan
                keyfiles.append((paramiko.DSSKey, dsa_key))
222 d095a59e Shikhar Bhushan
        
223 d095a59e Shikhar Bhushan
        for cls, filename in keyfiles:
224 d095a59e Shikhar Bhushan
            try:
225 d095a59e Shikhar Bhushan
                key = cls.from_private_key_file(filename, password)
226 d095a59e Shikhar Bhushan
                logger.debug('Trying discovered key %s in %s' %
227 d095a59e Shikhar Bhushan
                          (hexlify(key.get_fingerprint()), filename))
228 d095a59e Shikhar Bhushan
                self._transport.auth_publickey(username, key)
229 d095a59e Shikhar Bhushan
                return
230 d095a59e Shikhar Bhushan
            except Exception as e:
231 d095a59e Shikhar Bhushan
                saved_exception = e
232 d095a59e Shikhar Bhushan
                logger.debug(e)
233 d095a59e Shikhar Bhushan
        
234 d095a59e Shikhar Bhushan
        if password is not None:
235 d095a59e Shikhar Bhushan
            try:
236 d095a59e Shikhar Bhushan
                self._transport.auth_password(username, password)
237 d095a59e Shikhar Bhushan
                return
238 d095a59e Shikhar Bhushan
            except Exception as e:
239 d095a59e Shikhar Bhushan
                saved_exception = e
240 d095a59e Shikhar Bhushan
                logger.debug(e)
241 d095a59e Shikhar Bhushan
        
242 d095a59e Shikhar Bhushan
        if saved_exception is not None:
243 541247ba Shikhar Bhushan
            # need pep-3134 to do this right
244 4ba5e843 Shikhar Bhushan
            raise SSHAuthenticationError(repr(saved_exception))
245 d095a59e Shikhar Bhushan
        
246 4ba5e843 Shikhar Bhushan
        raise SSHAuthenticationError('No authentication methods available')
247 d095a59e Shikhar Bhushan
    
248 d095a59e Shikhar Bhushan
    def run(self):
249 d095a59e Shikhar Bhushan
        chan = self._channel
250 d095a59e Shikhar Bhushan
        chan.setblocking(0)
251 d095a59e Shikhar Bhushan
        q = self._q
252 d095a59e Shikhar Bhushan
        try:
253 d095a59e Shikhar Bhushan
            while True:
254 d095a59e Shikhar Bhushan
                # select on a paramiko ssh channel object does not ever return
255 d095a59e Shikhar Bhushan
                # it in the writable list, so it channel's don't exactly emulate 
256 d095a59e Shikhar Bhushan
                # the socket api
257 d095a59e Shikhar Bhushan
                r, w, e = select([chan], [], [], TICK)
258 d095a59e Shikhar Bhushan
                # will wakeup evey TICK seconds to check if something
259 d095a59e Shikhar Bhushan
                # to send, more if something to read (due to select returning
260 d095a59e Shikhar Bhushan
                # chan in readable list)
261 d095a59e Shikhar Bhushan
                if r:
262 d095a59e Shikhar Bhushan
                    data = chan.recv(BUF_SIZE)
263 d095a59e Shikhar Bhushan
                    if data:
264 d095a59e Shikhar Bhushan
                        self._buffer.write(data)
265 d095a59e Shikhar Bhushan
                        self._parse()
266 d095a59e Shikhar Bhushan
                    else:
267 94265508 Shikhar Bhushan
                        raise SessionCloseError(self._buffer.getvalue())
268 d095a59e Shikhar Bhushan
                if not q.empty() and chan.send_ready():
269 41e2ed46 Shikhar Bhushan
                    logger.debug('sending message')
270 d095a59e Shikhar Bhushan
                    data = q.get() + MSG_DELIM
271 d095a59e Shikhar Bhushan
                    while data:
272 d095a59e Shikhar Bhushan
                        n = chan.send(data)
273 d095a59e Shikhar Bhushan
                        if n <= 0:
274 94265508 Shikhar Bhushan
                            raise SessionCloseError(self._buffer.getvalue(), data)
275 d095a59e Shikhar Bhushan
                        data = data[n:]
276 d095a59e Shikhar Bhushan
        except Exception as e:
277 41e2ed46 Shikhar Bhushan
            logger.debug('broke out of main loop')
278 1d540e60 Shikhar Bhushan
            self.close()
279 1d540e60 Shikhar Bhushan
            if not (isinstance(e, SessionCloseError) and self._expecting_close):
280 1d540e60 Shikhar Bhushan
                self._dispatch_error(e)
281 d095a59e Shikhar Bhushan
    
282 d095a59e Shikhar Bhushan
    @property
283 d095a59e Shikhar Bhushan
    def transport(self):
284 d095a59e Shikhar Bhushan
        return self._transport
285 541247ba Shikhar Bhushan
    
286 541247ba Shikhar Bhushan
    @property
287 94803aaf Shikhar Bhushan
    def can_pipeline(self):
288 94803aaf Shikhar Bhushan
        if 'Cisco' in self._transport.remote_version:
289 94803aaf Shikhar Bhushan
            return False
290 94803aaf Shikhar Bhushan
        # elif ..
291 94803aaf Shikhar Bhushan
        return True