Passed
Push — develop ( f1fe9e...5c5de8 )
by
unknown
06:59 queued 03:36
created

ParamikoSSHClient.mkdir()   A

Complexity

Conditions 1

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
1
# Licensed to the Apache Software Foundation (ASF) under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import os
17
import posixpath
18
from StringIO import StringIO
19
import time
20
21
import eventlet
22
from oslo_config import cfg
23
24
import paramiko
25
from paramiko.ssh_exception import SSHException
26
27
# Depending on your version of Paramiko, it may cause a deprecation
28
# warning on Python 2.6.
29
# Ref: https://bugs.launchpad.net/paramiko/+bug/392973
30
31
from st2common.log import logging
32
from st2common.util.misc import strip_shell_chars
33
from st2common.util.shell import quote_unix
34
from st2common.constants.runners import DEFAULT_SSH_PORT, REMOTE_RUNNER_PRIVATE_KEY_HEADER
35
36
__all__ = [
37
    'ParamikoSSHClient',
38
39
    'SSHCommandTimeoutError'
40
]
41
42
43
class SSHCommandTimeoutError(Exception):
44
    """
45
    Exception which is raised when an SSH command times out.
46
    """
47
48
    def __init__(self, cmd, timeout, stdout=None, stderr=None):
49
        """
50
        :param stdout: Stdout which was consumed until the timeout occured.
51
        :type stdout: ``str``
52
53
        :param stdout: Stderr which was consumed until the timeout occured.
54
        :type stderr: ``str``
55
        """
56
        self.cmd = cmd
57
        self.timeout = timeout
58
        self.stdout = stdout
59
        self.stderr = stderr
60
        message = 'Command didn\'t finish in %s seconds' % (timeout)
61
        super(SSHCommandTimeoutError, self).__init__(message)
62
63
    def __repr__(self):
64
        return ('<SSHCommandTimeoutError: cmd="%s",timeout=%s)>' %
65
                (self.cmd, self.timeout))
66
67
    def __str__(self):
68
        return self.message
69
70
71
class ParamikoSSHClient(object):
72
    """
73
    A SSH Client powered by Paramiko.
74
    """
75
76
    # Maximum number of bytes to read at once from a socket
77
    CHUNK_SIZE = 1024
78
79
    # How long to sleep while waiting for command to finish to prevent busy waiting
80
    SLEEP_DELAY = 0.2
81
82
    # Connect socket timeout
83
    CONNECT_TIMEOUT = 60
84
85
    def __init__(self, hostname, port=DEFAULT_SSH_PORT, username=None, password=None,
86
                 bastion_host=None, key_files=None, key_material=None, timeout=None,
87
                 passphrase=None, handle_stdout_line_func=None, handle_stderr_line_func=None):
88
        """
89
        Authentication is always attempted in the following order:
90
91
        - The key passed in (if key is provided)
92
        - Any key we can find through an SSH agent (only if no password and
93
          key is provided)
94
        - Any "id_rsa" or "id_dsa" key discoverable in ~/.ssh/ (only if no
95
          password and key is provided)
96
        - Plain username/password auth, if a password was given (if password is
97
          provided)
98
        """
99
        self.hostname = hostname
100
        self.port = port
101
        self.username = username
102
        self.password = password
103
        self.key_files = key_files
104
        self.timeout = timeout or ParamikoSSHClient.CONNECT_TIMEOUT
105
        self.key_material = key_material
106
        self.bastion_host = bastion_host
107
        self.passphrase = passphrase
108
        self._handle_stdout_line_func = handle_stdout_line_func
109
        self._handle_stderr_line_func = handle_stderr_line_func
110
111
        self.ssh_config_file = os.path.expanduser(
112
            cfg.CONF.ssh_runner.ssh_config_file_path or
113
            '~/.ssh/config'
114
        )
115
        self.logger = logging.getLogger(__name__)
116
117
        self.client = None
118
        self.sftp_client = None
119
120
        self.bastion_client = None
121
        self.bastion_socket = None
122
123
    def connect(self):
124
        """
125
        Connect to the remote node over SSH.
126
127
        :return: True if the connection has been successfully established,
128
                 False otherwise.
129
        :rtype: ``bool``
130
        """
131
        if self.bastion_host:
132
            self.logger.debug('Bastion host specified, connecting')
133
            self.bastion_client = self._connect(host=self.bastion_host)
134
            transport = self.bastion_client.get_transport()
135
            real_addr = (self.hostname, self.port)
136
            # fabric uses ('', 0) for direct-tcpip, this duplicates that behaviour
137
            # see https://github.com/fabric/fabric/commit/c2a9bbfd50f560df6c6f9675603fb405c4071cad
138
            local_addr = ('', 0)
139
            self.bastion_socket = transport.open_channel('direct-tcpip', real_addr, local_addr)
140
141
        self.client = self._connect(host=self.hostname, socket=self.bastion_socket)
142
        return True
143
144
    def put(self, local_path, remote_path, mode=None, mirror_local_mode=False):
145
        """
146
        Upload a file to the remote node.
147
148
        :type local_path: ``st``
149
        :param local_path: File path on the local node.
150
151
        :type remote_path: ``str``
152
        :param remote_path: File path on the remote node.
153
154
        :type mode: ``int``
155
        :param mode: Permissions mode for the file. E.g. 0744.
156
157
        :type mirror_local_mode: ``int``
158
        :param mirror_local_mode: Should remote file mirror local mode.
159
160
        :return: Attributes of the remote file.
161
        :rtype: :class:`posix.stat_result` or ``None``
162
        """
163
164
        if not local_path or not remote_path:
165
            raise Exception('Need both local_path and remote_path. local: %s, remote: %s' %
166
                            local_path, remote_path)
167
        local_path = quote_unix(local_path)
168
        remote_path = quote_unix(remote_path)
169
170
        extra = {'_local_path': local_path, '_remote_path': remote_path, '_mode': mode,
171
                 '_mirror_local_mode': mirror_local_mode}
172
        self.logger.debug('Uploading file', extra=extra)
173
174
        if not os.path.exists(local_path):
175
            raise Exception('Path %s does not exist locally.' % local_path)
176
177
        rattrs = self.sftp.put(local_path, remote_path)
178
179
        if mode or mirror_local_mode:
180
            local_mode = mode
181
            if not mode or mirror_local_mode:
182
                local_mode = os.stat(local_path).st_mode
183
184
            # Cast to octal integer in case of string
185
            if isinstance(local_mode, basestring):
186
                local_mode = int(local_mode, 8)
187
            local_mode = local_mode & 07777
188
            remote_mode = rattrs.st_mode
189
            # Only bitshift if we actually got an remote_mode
190
            if remote_mode is not None:
191
                remote_mode = (remote_mode & 07777)
192
            if local_mode != remote_mode:
193
                self.sftp.chmod(remote_path, local_mode)
194
195
        return rattrs
196
197
    def put_dir(self, local_path, remote_path, mode=None, mirror_local_mode=False):
198
        """
199
        Upload a dir to the remote node.
200
201
        :type local_path: ``str``
202
        :param local_path: Dir path on the local node.
203
204
        :type remote_path: ``str``
205
        :param remote_path: Base dir path on the remote node.
206
207
        :type mode: ``int``
208
        :param mode: Permissions mode for the file. E.g. 0744.
209
210
        :type mirror_local_mode: ``int``
211
        :param mirror_local_mode: Should remote file mirror local mode.
212
213
        :return: List of files created on remote node.
214
        :rtype: ``list`` of ``str``
215
        """
216
217
        extra = {'_local_path': local_path, '_remote_path': remote_path, '_mode': mode,
218
                 '_mirror_local_mode': mirror_local_mode}
219
        self.logger.debug('Uploading dir', extra=extra)
220
221
        if os.path.basename(local_path):
222
            strip = os.path.dirname(local_path)
223
        else:
224
            strip = os.path.dirname(os.path.dirname(local_path))
225
226
        remote_paths = []
227
228
        for context, dirs, files in os.walk(local_path):
229
            rcontext = context.replace(strip, '', 1)
230
            # normalize pathname separators with POSIX separator
231
            rcontext = rcontext.replace(os.sep, '/')
232
            rcontext = rcontext.lstrip('/')
233
            rcontext = posixpath.join(remote_path, rcontext)
234
235
            if not self.exists(rcontext):
236
                self.sftp.mkdir(rcontext)
237
238
            for d in dirs:
239
                n = posixpath.join(rcontext, d)
240
                if not self.exists(n):
241
                    self.sftp.mkdir(n)
242
243
            for f in files:
244
                local_path = os.path.join(context, f)
245
                n = posixpath.join(rcontext, f)
246
                # Note that quote_unix is done by put anyways.
247
                p = self.put(local_path=local_path, remote_path=n,
248
                             mirror_local_mode=mirror_local_mode, mode=mode)
249
                remote_paths.append(p)
250
251
        return remote_paths
252
253
    def exists(self, remote_path):
254
        """
255
        Validate whether a remote file or directory exists.
256
257
        :param remote_path: Path to remote file.
258
        :type remote_path: ``str``
259
260
        :rtype: ``bool``
261
        """
262
        try:
263
            self.sftp.lstat(remote_path).st_mode
264
        except IOError:
265
            return False
266
267
        return True
268
269
    def mkdir(self, dir_path):
270
        """
271
        Create a directory on remote box.
272
273
        :param dir_path: Path to remote directory to be created.
274
        :type dir_path: ``str``
275
276
        :return: Returns nothing if successful else raises IOError exception.
277
278
        :rtype: ``None``
279
        """
280
281
        dir_path = quote_unix(dir_path)
282
        extra = {'_dir_path': dir_path}
283
        self.logger.debug('mkdir', extra=extra)
284
        return self.sftp.mkdir(dir_path)
285
286
    def delete_file(self, path):
287
        """
288
        Delete a file on remote box.
289
290
        :param path: Path to remote file to be deleted.
291
        :type path: ``str``
292
293
        :return: True if the file has been successfully deleted, False
294
                 otherwise.
295
        :rtype: ``bool``
296
        """
297
298
        path = quote_unix(path)
299
        extra = {'_path': path}
300
        self.logger.debug('Deleting file', extra=extra)
301
        self.sftp.unlink(path)
302
        return True
303
304
    def delete_dir(self, path, force=False, timeout=None):
305
        """
306
        Delete a dir on remote box.
307
308
        :param path: Path to remote dir to be deleted.
309
        :type path: ``str``
310
311
        :param force: Optional Forcefully remove dir.
312
        :type force: ``bool``
313
314
        :param timeout: Optional Time to wait for dir to be deleted. Only relevant for force.
315
        :type timeout: ``int``
316
317
        :return: True if the file has been successfully deleted, False
318
                 otherwise.
319
        :rtype: ``bool``
320
        """
321
322
        path = quote_unix(path)
323
        extra = {'_path': path}
324
        if force:
325
            command = 'rm -rf %s' % path
326
            extra['_command'] = command
327
            extra['_force'] = force
328
            self.logger.debug('Deleting dir', extra=extra)
329
            return self.run(command, timeout=timeout)
330
331
        self.logger.debug('Deleting dir', extra=extra)
332
        return self.sftp.rmdir(path)
333
334
    def run(self, cmd, timeout=None, quote=False, call_line_handler_func=False):
335
        """
336
        Note: This function is based on paramiko's exec_command()
337
        method.
338
339
        :param timeout: How long to wait (in seconds) for the command to finish (optional).
340
        :type timeout: ``float``
341
342
        :param call_line_handler_func: True to call handle_stdout_line_func function for each line
343
                                       of received stdout and handle_stderr_line_func for each
344
                                       line of stderr.
345
        :type call_line_handler_func: ``bool``
346
        """
347
348
        if quote:
349
            cmd = quote_unix(cmd)
350
351
        extra = {'_cmd': cmd}
352
        self.logger.info('Executing command', extra=extra)
353
354
        # Use the system default buffer size
355
        bufsize = -1
356
357
        transport = self.client.get_transport()
358
        chan = transport.open_session()
359
360
        start_time = time.time()
361
        if cmd.startswith('sudo'):
362
            # Note that fabric does this as well. If you set pty, stdout and stderr
363
            # streams will be combined into one.
364
            chan.get_pty()
365
        chan.exec_command(cmd)
366
367
        stdout = StringIO()
368
        stderr = StringIO()
369
370
        # Create a stdin file and immediately close it to prevent any
371
        # interactive script from hanging the process.
372
        stdin = chan.makefile('wb', bufsize)
373
        stdin.close()
374
375
        # Receive all the output
376
        # Note #1: This is used instead of chan.makefile approach to prevent
377
        # buffering issues and hanging if the executed command produces a lot
378
        # of output.
379
        #
380
        # Note #2: If you are going to remove "ready" checks inside the loop
381
        # you are going to have a bad time. Trying to consume from a channel
382
        # which is not ready will block for indefinitely.
383
        exit_status_ready = chan.exit_status_ready()
384
385
        if exit_status_ready:
386
            stdout_data = self._consume_stdout(chan=chan,
387
                                               call_line_handler_func=call_line_handler_func)
388
            stdout_data = stdout_data.getvalue()
389
390
            stderr_data = self._consume_stderr(chan=chan,
391
                                               call_line_handler_func=call_line_handler_func)
392
            stderr_data = stderr_data.getvalue()
393
394
            stdout.write(stdout_data)
395
            stderr.write(stderr_data)
396
397
        while not exit_status_ready:
398
            current_time = time.time()
399
            elapsed_time = (current_time - start_time)
400
401
            if timeout and (elapsed_time > timeout):
402
                # TODO: Is this the right way to clean up?
403
                chan.close()
404
405
                stdout = strip_shell_chars(stdout.getvalue())
406
                stderr = strip_shell_chars(stderr.getvalue())
407
                raise SSHCommandTimeoutError(cmd=cmd, timeout=timeout, stdout=stdout,
408
                                             stderr=stderr)
409
410
            stdout_data = self._consume_stdout(chan=chan,
411
                                               call_line_handler_func=call_line_handler_func)
412
            stdout_data = stdout_data.getvalue()
413
414
            stderr_data = self._consume_stderr(chan=chan,
415
                                               call_line_handler_func=call_line_handler_func)
416
            stderr_data = stderr_data.getvalue()
417
418
            stdout.write(stdout_data)
419
            stderr.write(stderr_data)
420
421
            # We need to check the exit status here, because the command could
422
            # print some output and exit during this sleep below.
423
            exit_status_ready = chan.exit_status_ready()
424
425
            if exit_status_ready:
426
                break
427
428
            # Short sleep to prevent busy waiting
429
            eventlet.sleep(self.SLEEP_DELAY)
430
        # print('Wait over. Channel must be ready for host: %s' % self.hostname)
431
432
        # Receive the exit status code of the command we ran.
433
        status = chan.recv_exit_status()
434
435
        stdout = strip_shell_chars(stdout.getvalue())
436
        stderr = strip_shell_chars(stderr.getvalue())
437
438
        extra = {'_status': status, '_stdout': stdout, '_stderr': stderr}
439
        self.logger.debug('Command finished', extra=extra)
440
441
        return [stdout, stderr, status]
442
443
    def close(self):
444
        self.logger.debug('Closing server connection')
445
446
        self.client.close()
447
448
        if self.sftp_client:
449
            self.sftp_client.close()
450
451
        if self.bastion_client:
452
            self.bastion_client.close()
453
454
        return True
455
456
    @property
457
    def sftp(self):
458
        """
459
        Method which lazily establishes SFTP connection if one is not established yet when this
460
        variable is accessed.
461
        """
462
        if not self.sftp_client:
463
            self.sftp_client = self.client.open_sftp()
464
465
        return self.sftp_client
466
467
    def _consume_stdout(self, chan, call_line_handler_func=False):
468
        """
469
        Try to consume stdout data from chan if it's receive ready.
470
        """
471
472
        out = bytearray()
473
        stdout = StringIO()
474
475
        if chan.recv_ready():
476
            data = chan.recv(self.CHUNK_SIZE)
477
            out += data
478
479
            while data:
480
                ready = chan.recv_ready()
481
482
                if not ready:
483
                    break
484
485
                data = chan.recv(self.CHUNK_SIZE)
486
                out += data
487
488
        stdout.write(self._get_decoded_data(out))
489
490
        if self._handle_stdout_line_func and call_line_handler_func:
491
            data = strip_shell_chars(stdout.getvalue())
492
            lines = data.split('\n')
493
            lines = [line for line in lines if line]
494
495
            for line in lines:
496
                # Note: If this function performs network operating no sleep is
497
                # needed, otherwise if a long blocking operating is performed,
498
                # sleep is recommended to yield and prevent from busy looping
499
                self._handle_stdout_line_func(line=line + '\n')
500
501
            stdout.seek(0)
502
503
        return stdout
504
505
    def _consume_stderr(self, chan, call_line_handler_func=False):
506
        """
507
        Try to consume stderr data from chan if it's receive ready.
508
        """
509
510
        out = bytearray()
511
        stderr = StringIO()
512
513
        if chan.recv_stderr_ready():
514
            data = chan.recv_stderr(self.CHUNK_SIZE)
515
            out += data
516
517
            while data:
518
                ready = chan.recv_stderr_ready()
519
520
                if not ready:
521
                    break
522
523
                data = chan.recv_stderr(self.CHUNK_SIZE)
524
                out += data
525
526
        stderr.write(self._get_decoded_data(out))
527
528
        if self._handle_stderr_line_func and call_line_handler_func:
529
            data = strip_shell_chars(stderr.getvalue())
530
            lines = data.split('\n')
531
            lines = [line for line in lines if line]
532
533
            for line in lines:
534
                # Note: If this function performs network operating no sleep is
535
                # needed, otherwise if a long blocking operating is performed,
536
                # sleep is recommended to yield and prevent from busy looping
537
                self._handle_stderr_line_func(line=line + '\n')
538
539
            stderr.seek(0)
540
541
        return stderr
542
543
    def _get_decoded_data(self, data):
544
        try:
545
            return data.decode('utf-8')
546
        except:
547
            self.logger.exception('Non UTF-8 character found in data: %s', data)
548
            raise
549
550
    def _get_pkey_object(self, key_material, passphrase):
551
        """
552
        Try to detect private key type and return paramiko.PKey object.
553
        """
554
555
        for cls in [paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey]:
556
            try:
557
                key = cls.from_private_key(StringIO(key_material), password=passphrase)
558
            except paramiko.ssh_exception.SSHException:
559
                # Invalid key, try other key type
560
                pass
561
            else:
562
                return key
563
564
        # If a user passes in something which looks like file path we throw a more friendly
565
        # exception letting the user know we expect the contents a not a path.
566
        # Note: We do it here and not up the stack to avoid false positives.
567
        contains_header = REMOTE_RUNNER_PRIVATE_KEY_HEADER in key_material.lower()
568
        if not contains_header and (key_material.count('/') >= 1 or key_material.count('\\') >= 1):
569
            msg = ('"private_key" parameter needs to contain private key data / content and not '
570
                   'a path')
571
        elif passphrase:
572
            msg = 'Invalid passphrase or invalid/unsupported key type'
573
        else:
574
            msg = 'Invalid or unsupported key type'
575
576
        raise paramiko.ssh_exception.SSHException(msg)
577
578
    def _connect(self, host, socket=None):
579
        """
580
        Order of precedence for SSH connection parameters:
581
582
        1. If user supplies parameters via action parameters, we use them to connect.
583
        2. For parameters not supplied via action parameters, if there is an entry
584
           for host in SSH config file, we use those. Note that this is a merge operation.
585
        3. If user does not supply certain action parameters (username and key file location)
586
           and there is no entry for host in SSH config file, we use values supplied in
587
           st2 config file for those parameters.
588
589
        :type host: ``str``
590
        :param host: Host to connect to
591
592
        :type socket: :class:`paramiko.Channel` or an opened :class:`socket.socket`
593
        :param socket: If specified, won't open a socket for communication to the specified host
594
                       and will use this instead
595
596
        :return: A connected SSHClient
597
        :rtype: :class:`paramiko.SSHClient`
598
        """
599
600
        conninfo = {'hostname': host,
601
                    'allow_agent': False,
602
                    'look_for_keys': False,
603
                    'timeout': self.timeout}
604
605
        ssh_config_file_info = {}
606
        if cfg.CONF.ssh_runner.use_ssh_config:
607
            ssh_config_file_info = self._get_ssh_config_for_host(host)
608
609
        self.username = (self.username or ssh_config_file_info.get('user', None) or
610
                         cfg.CONF.system_user.user)
611
        self.port = self.port or ssh_config_file_info.get('port' or None) or DEFAULT_SSH_PORT
612
613
        # If both key file and key material are provided as action parameters,
614
        # throw an error informing user only one is required.
615
        if self.key_files and self.key_material:
616
            msg = ('key_files and key_material arguments are mutually exclusive. Supply only one.')
617
            raise ValueError(msg)
618
619
        # If neither key material nor password is provided, only then we look at key file and decide
620
        # if we want to use the user supplied one or the one in SSH config.
621
        if not self.key_material and not self.password:
622
            self.key_files = (self.key_files or ssh_config_file_info.get('identityfile', None) or
623
                              cfg.CONF.system_user.ssh_key_file)
624
625
        if self.passphrase and not (self.key_files or self.key_material):
626
            raise ValueError('passphrase should accompany private key material')
627
628
        credentials_provided = self.password or self.key_files or self.key_material
629
630
        if not credentials_provided:
631
            msg = ('Either password or key file location or key material should be supplied ' +
632
                   'for action. You can also add an entry for host %s in SSH config file %s.' %
633
                   (host, self.ssh_config_file))
634
            raise ValueError(msg)
635
636
        conninfo['username'] = self.username
637
        conninfo['port'] = self.port
638
639
        if self.password:
640
            conninfo['password'] = self.password
641
642
        if self.key_files:
643
            conninfo['key_filename'] = self.key_files
644
645
            passphrase_reqd = self._is_key_file_needs_passphrase(self.key_files)
646
            if passphrase_reqd and not self.passphrase:
647
                msg = ('Private key file %s is passphrase protected. Supply a passphrase.' %
648
                       self.key_files)
649
                raise paramiko.ssh_exception.PasswordRequiredException(msg)
650
651
            if self.passphrase:
652
                # Optional passphrase for unlocking the private key
653
                conninfo['password'] = self.passphrase
654
655
        if self.key_material:
656
            conninfo['pkey'] = self._get_pkey_object(key_material=self.key_material,
657
                                                     passphrase=self.passphrase)
658
659
        if not self.password and not (self.key_files or self.key_material):
660
            conninfo['allow_agent'] = True
661
            conninfo['look_for_keys'] = True
662
663
        extra = {'_hostname': host, '_port': self.port,
664
                 '_username': self.username, '_timeout': self.timeout}
665
        self.logger.debug('Connecting to server', extra=extra)
666
667
        socket = socket or ssh_config_file_info.get('sock', None)
668
        if socket:
669
            conninfo['sock'] = socket
670
671
        client = paramiko.SSHClient()
672
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
673
674
        extra = {'_conninfo': conninfo}
675
        self.logger.debug('Connection info', extra=extra)
676
        try:
677
            client.connect(**conninfo)
678
        except SSHException as e:
679
            paramiko_msg = e.message
680
681
            if conninfo.get('password', None):
682
                conninfo['password'] = '<redacted>'
683
684
            msg = ('Error connecting to host %s ' % host +
685
                   'with connection parameters %s.' % conninfo +
686
                   'Paramiko error: %s.' % paramiko_msg)
687
            raise SSHException(msg)
688
689
        return client
690
691
    def _get_ssh_config_for_host(self, host):
692
        ssh_config_info = {}
693
        ssh_config_parser = paramiko.SSHConfig()
694
695
        try:
696
            with open(self.ssh_config_file) as f:
697
                ssh_config_parser.parse(f)
698
        except IOError as e:
699
            raise Exception('Error accessing ssh config file %s. Code: %s Reason %s' %
700
                            (self.ssh_config_file, e.errno, e.strerror))
701
702
        ssh_config = ssh_config_parser.lookup(host)
703
        self.logger.info('Parsed SSH config file contents: %s', ssh_config)
704
        if ssh_config:
705
            for k in ('hostname', 'user', 'port'):
706
                if k in ssh_config:
707
                    ssh_config_info[k] = ssh_config[k]
708
709
            if 'identityfile' in ssh_config:
710
                key_file = ssh_config['identityfile']
711
                if type(key_file) is list:
712
                    key_file = key_file[0]
713
714
                ssh_config_info['identityfile'] = key_file
715
716
            if 'proxycommand' in ssh_config:
717
                ssh_config_info['sock'] = paramiko.ProxyCommand(ssh_config['proxycommand'])
718
719
        return ssh_config_info
720
721
    @staticmethod
722
    def _is_key_file_needs_passphrase(file):
723
        for cls in [paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey]:
724
            try:
725
                cls.from_private_key_file(file, password=None)
726
            except paramiko.ssh_exception.PasswordRequiredException:
727
                return True
728
            except paramiko.ssh_exception.SSHException:
729
                continue
730
731
        return False
732
733
    def __repr__(self):
734
        return ('<ParamikoSSHClient hostname=%s,port=%s,username=%s,id=%s>' %
735
                (self.hostname, self.port, self.username, id(self)))
736