• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2019 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain 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,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import backoff
18import os
19import logging
20import paramiko
21import socket
22import time
23
24from acts import utils
25from acts.controllers.fuchsia_lib.base_lib import DeviceOffline
26from acts.libs.proc import job
27
28logging.getLogger("paramiko").setLevel(logging.WARNING)
29# paramiko-ng will throw INFO messages when things get disconnect or cannot
30# connect perfectly the first time.  In this library those are all handled by
31# either retrying and/or throwing an exception for the appropriate case.
32# Therefore, in order to reduce confusion in the logs the log level is set to
33# WARNING.
34
35
36def get_private_key(ip_address, ssh_config):
37    """Tries to load various ssh key types.
38
39    Args:
40        ip_address: IP address of ssh server.
41        ssh_config: ssh_config location for the ssh server.
42    Returns:
43        The ssh private key
44    """
45    exceptions = []
46    try:
47        logging.debug('Trying to load SSH key type: ed25519')
48        return paramiko.ed25519key.Ed25519Key(
49            filename=get_ssh_key_for_host(ip_address, ssh_config))
50    except paramiko.SSHException as e:
51        exceptions.append(e)
52        logging.debug('Failed loading SSH key type: ed25519')
53
54    try:
55        logging.debug('Trying to load SSH key type: rsa')
56        return paramiko.RSAKey.from_private_key_file(
57            filename=get_ssh_key_for_host(ip_address, ssh_config))
58    except paramiko.SSHException as e:
59        exceptions.append(e)
60        logging.debug('Failed loading SSH key type: rsa')
61
62    raise Exception('No valid ssh key type found', exceptions)
63
64
65@backoff.on_exception(
66    backoff.constant,
67    (paramiko.ssh_exception.SSHException,
68     paramiko.ssh_exception.AuthenticationException, socket.timeout,
69     socket.error, ConnectionRefusedError, ConnectionResetError),
70    interval=1.5,
71    max_tries=4)
72def create_ssh_connection(ip_address,
73                          ssh_username,
74                          ssh_config,
75                          ssh_port=22,
76                          connect_timeout=10,
77                          auth_timeout=10,
78                          banner_timeout=10):
79    """Creates and ssh connection to a Fuchsia device
80
81    Args:
82        ip_address: IP address of ssh server.
83        ssh_username: Username for ssh server.
84        ssh_config: ssh_config location for the ssh server.
85        connect_timeout: Timeout value for connecting to ssh_server.
86        auth_timeout: Timeout value to wait for authentication.
87        banner_timeout: Timeout to wait for ssh banner.
88
89    Returns:
90        A paramiko ssh object
91    """
92    if not utils.can_ping(job, ip_address):
93        raise DeviceOffline("Device %s is not reachable via "
94                            "the network." % ip_address)
95    ssh_key = get_private_key(ip_address=ip_address, ssh_config=ssh_config)
96    ssh_client = paramiko.SSHClient()
97    ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
98    ssh_client.connect(hostname=ip_address,
99                       username=ssh_username,
100                       allow_agent=False,
101                       pkey=ssh_key,
102                       port=ssh_port,
103                       timeout=connect_timeout,
104                       auth_timeout=auth_timeout,
105                       banner_timeout=banner_timeout)
106    ssh_client.get_transport().set_keepalive(1)
107    return ssh_client
108
109
110def ssh_is_connected(ssh_client):
111    """Checks to see if the SSH connection is alive.
112    Args:
113        ssh_client: A paramiko SSH client instance.
114    Returns:
115          True if connected, False or None if not connected.
116    """
117    return ssh_client and ssh_client.get_transport().is_active()
118
119
120def get_ssh_key_for_host(host, ssh_config_file):
121    """Gets the SSH private key path from a supplied ssh_config_file and the
122       host.
123    Args:
124        host (str): The ip address or host name that SSH will connect to.
125        ssh_config_file (str): Path to the ssh_config_file that will be used
126            to connect to the host.
127
128    Returns:
129        path: A path to the private key for the SSH connection.
130    """
131    ssh_config = paramiko.SSHConfig()
132    user_config_file = os.path.expanduser(ssh_config_file)
133    if os.path.exists(user_config_file):
134        with open(user_config_file) as f:
135            ssh_config.parse(f)
136    user_config = ssh_config.lookup(host)
137
138    if 'identityfile' not in user_config:
139        raise ValueError('Could not find identity file in %s.' % ssh_config)
140
141    path = os.path.expanduser(user_config['identityfile'][0])
142    if not os.path.exists(path):
143        raise FileNotFoundError('Specified IdentityFile %s for %s in %s not '
144                                'existing anymore.' % (path, host, ssh_config))
145    return path
146
147
148class SshResults:
149    """Class representing the results from a SSH command to mimic the output
150    of the job.Result class in ACTS.  This is to reduce the changes needed from
151    swapping the ssh connection in ACTS to paramiko.
152
153    Attributes:
154        stdin: The file descriptor to the input channel of the SSH connection.
155        stdout: The file descriptor to the stdout of the SSH connection.
156        stderr: The file descriptor to the stderr of the SSH connection.
157        exit_status: The file descriptor of the SSH command.
158    """
159    def __init__(self, stdin, stdout, stderr, exit_status):
160        self._raw_stdout = stdout.read()
161        self._stdout = self._raw_stdout.decode('utf-8', errors='replace')
162        self._stderr = stderr.read().decode('utf-8', errors='replace')
163        self._exit_status = exit_status.recv_exit_status()
164
165    @property
166    def stdout(self):
167        return self._stdout
168
169    @property
170    def raw_stdout(self):
171        return self._raw_stdout
172
173    @property
174    def stderr(self):
175        return self._stderr
176
177    @property
178    def exit_status(self):
179        return self._exit_status
180