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