Initial commit for a Networking Proxy Manager
* Allows easy ping to a remote server * Allows easy connection to a remote server through a proxy server * Allows easy execution of commands on a remote server Change-Id: I0eb4d0beb7bb741cc283aae0120c0123aea122c8
This commit is contained in:
parent
ebfead8694
commit
6645144868
145
cloudcafe/networking/networks/common/proxy_mgr/ping_util.py
Normal file
145
cloudcafe/networking/networks/common/proxy_mgr/ping_util.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pexpect
|
||||||
|
|
||||||
|
from cafe.engine.ssh.models.ssh_response import ExecResponse
|
||||||
|
|
||||||
|
|
||||||
|
class PingMixin(object):
|
||||||
|
|
||||||
|
PING_COUNT = 5
|
||||||
|
PING_PACKET_LOSS_REGEX = r'\s*(?P<received>\d+)\s+received,'
|
||||||
|
LINUX_PROMPT_PATTERN = r'[$#>]\s*$'
|
||||||
|
|
||||||
|
def ping(self, target_ip, count=PING_COUNT, threshold=1):
|
||||||
|
"""
|
||||||
|
Ping a target IP (using remote proxy or local host)
|
||||||
|
|
||||||
|
@param target_ip: IP address to ping
|
||||||
|
@param count: Number of echo requests to issue
|
||||||
|
@param threshold: Number of echo requests required to determine success
|
||||||
|
|
||||||
|
@return: Boolean: Is target online?
|
||||||
|
|
||||||
|
"""
|
||||||
|
api = self._ping_from_here
|
||||||
|
if self.use_proxy:
|
||||||
|
api = self._ping_from_remote_client
|
||||||
|
|
||||||
|
output = api(target_ip, count)
|
||||||
|
return self._validate_ping_response(
|
||||||
|
ip=target_ip, response=output, threshold=threshold)
|
||||||
|
|
||||||
|
def _ping_from_remote_client(self, target_ip, count=PING_COUNT):
|
||||||
|
""" Ping target from a remote host. Connects to the proxy, and then
|
||||||
|
pings through proxy connection.
|
||||||
|
|
||||||
|
@param target_ip: The IP address of the target host
|
||||||
|
@param count: Number of pings to attempt
|
||||||
|
|
||||||
|
@return String of ping cmd output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get open client connection
|
||||||
|
connection = self.connect_to_proxy()
|
||||||
|
|
||||||
|
# Execute ping command on remote command
|
||||||
|
return self._ping_from_here(
|
||||||
|
target_ip=target_ip, count=count, connection=connection)
|
||||||
|
|
||||||
|
def _ping_from_here(
|
||||||
|
self, target_ip, count=PING_COUNT, connection=None):
|
||||||
|
"""
|
||||||
|
Ping target from the execution (local) host
|
||||||
|
|
||||||
|
@param target_ip: The IP address of the target
|
||||||
|
@param count: Number of pings to attempt
|
||||||
|
@param connection: Active pexpect session (from self.connect_to_proxy)
|
||||||
|
|
||||||
|
:return: ExecResponse containing data (stdin, stdout, stderr).
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Setup ping command
|
||||||
|
ping_cmd = 'ping -c {count} -v {ip}'.format(ip=target_ip, count=count)
|
||||||
|
|
||||||
|
# Build list of potential and expected output
|
||||||
|
expectations = OrderedDict([
|
||||||
|
(pexpect.TIMEOUT, None),
|
||||||
|
(self.LINUX_PROMPT_PATTERN, None)])
|
||||||
|
|
||||||
|
# Initialize output object (same used by remote SSH/ping cmd)
|
||||||
|
output = ExecResponse()
|
||||||
|
|
||||||
|
# Open process and start command
|
||||||
|
if connection is None:
|
||||||
|
connection = pexpect.spawn(ping_cmd)
|
||||||
|
else:
|
||||||
|
connection.sendline(ping_cmd)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
# Watch process output and match on specific criteria
|
||||||
|
try:
|
||||||
|
response = connection.expect(expectations.keys())
|
||||||
|
|
||||||
|
# TIMEOUT, break out of loop and indicate FAILURE
|
||||||
|
except pexpect.TIMEOUT:
|
||||||
|
err = 'Pinging target timed out. {0} --> {1}'
|
||||||
|
self.logger.error(err.format(
|
||||||
|
connection.before, connection.after))
|
||||||
|
output.stdout = connection.before
|
||||||
|
break
|
||||||
|
|
||||||
|
# CONNECTION CLOSED, save output and break out of loop.
|
||||||
|
except pexpect.EOF:
|
||||||
|
self.logger.debug('Reached END OF FILE')
|
||||||
|
output.stdout = connection.before
|
||||||
|
break
|
||||||
|
|
||||||
|
# If TIMEOUT returned by response, break out of loop and indicate
|
||||||
|
# FAILURE...
|
||||||
|
if response == 0:
|
||||||
|
err = 'Pinging target timed out. {0} --> {1}'
|
||||||
|
self.logger.error(err.format(
|
||||||
|
connection.before, connection.after))
|
||||||
|
output.stdout = connection.before
|
||||||
|
break
|
||||||
|
|
||||||
|
# Capture output from ping.
|
||||||
|
output.stdout = connection.before + connection.match.group()
|
||||||
|
break
|
||||||
|
|
||||||
|
connection.close()
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _validate_ping_response(self, ip, response, threshold=1):
|
||||||
|
"""
|
||||||
|
Validate ping response output.
|
||||||
|
|
||||||
|
:param ip: IP address of target
|
||||||
|
:param response: Output of ping cmd (should be a string)
|
||||||
|
:param threshold: Number of ping responses required to determine
|
||||||
|
success
|
||||||
|
|
||||||
|
:return: (Boolean) True = PING was successful
|
||||||
|
e.g. - received at least one echo reply
|
||||||
|
|
||||||
|
"""
|
||||||
|
msg = 'PING Response for {ip}:\n{resp}'.format(
|
||||||
|
ip=ip, resp=response.stdout)
|
||||||
|
self.logger.debug(msg)
|
||||||
|
|
||||||
|
# Check output for number of ping replies received
|
||||||
|
pings = re.search(self.PING_PACKET_LOSS_REGEX, response.stdout, re.I)
|
||||||
|
target_online = False
|
||||||
|
|
||||||
|
# If there were some number of pings replies received, make sure it is
|
||||||
|
# greater than 0 pings.
|
||||||
|
if pings is not None:
|
||||||
|
pings_received = int(pings.group('received'))
|
||||||
|
self.logger.info('Number of pings received: {rec}'.format(
|
||||||
|
rec=pings_received))
|
||||||
|
target_online = pings_received >= threshold
|
||||||
|
self.logger.info('Target online? {resp}'.format(resp=target_online))
|
||||||
|
return target_online
|
210
cloudcafe/networking/networks/common/proxy_mgr/proxy_mgr.py
Executable file
210
cloudcafe/networking/networks/common/proxy_mgr/proxy_mgr.py
Executable file
@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from cafe.common.reporting import cclogging
|
||||||
|
|
||||||
|
from cloudcafe.networking.networks.common.proxy_mgr.ping_util \
|
||||||
|
import PingMixin
|
||||||
|
from cloudcafe.networking.networks.common.proxy_mgr.ssh_util \
|
||||||
|
import SshMixin
|
||||||
|
|
||||||
|
# For available utility routines, please refer to the inherited mixins
|
||||||
|
|
||||||
|
|
||||||
|
class NoPasswordProvided(Exception):
|
||||||
|
def __init__(self):
|
||||||
|
self.message = ("Server Model Obj does not have the 'admin_pass' "
|
||||||
|
"attribute set")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkProxyMgr(PingMixin, SshMixin):
|
||||||
|
|
||||||
|
LINUX = 'linux'
|
||||||
|
WINDOWS = 'windows'
|
||||||
|
OS = [LINUX, WINDOWS]
|
||||||
|
|
||||||
|
DEFAULT_USER = 'root'
|
||||||
|
PROMPT_PATTERN = r'[$#>]\s*$'
|
||||||
|
|
||||||
|
STANDARD_CMD_DELAY = 0.5
|
||||||
|
|
||||||
|
def __init__(self, use_proxy=True, proxy_os=LINUX, ip_version=4,
|
||||||
|
logger=None, debug=False):
|
||||||
|
"""
|
||||||
|
Proxy Server Constructor
|
||||||
|
@param use_proxy: (Boolean) - Is there a proxy/bastion that should
|
||||||
|
execute commands or be used as a hop to another address?
|
||||||
|
True - Yes
|
||||||
|
False - No, execute cmds from the localhost.
|
||||||
|
@param proxy_os: (ENUM) - Support for multiple OSs. A hook for
|
||||||
|
future functionality. Only supports Linux currently.
|
||||||
|
@param ip_version: Version to use by default, if utilities differ
|
||||||
|
across IP versions.
|
||||||
|
@param logger: Logging functionality.
|
||||||
|
@param debug: (Boolean) Used for debugging system and mixin utiliies
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.use_proxy = use_proxy
|
||||||
|
|
||||||
|
self._proxy_svr = None
|
||||||
|
self._proxy_ip = None
|
||||||
|
self._proxy_os = proxy_os
|
||||||
|
self._ip_version = ip_version
|
||||||
|
self.logger = logger or cclogging.getLogger(
|
||||||
|
cclogging.get_object_namespace(self.__class__))
|
||||||
|
self.connection = None
|
||||||
|
self.debug = debug
|
||||||
|
self.session_password = None
|
||||||
|
self.prompt_pattern = self.PROMPT_PATTERN
|
||||||
|
self.last_response = None
|
||||||
|
|
||||||
|
# Track IPs (hops) currently connected to...
|
||||||
|
self._conn_path = []
|
||||||
|
|
||||||
|
# Delay between commands if iterating a list of commands
|
||||||
|
self._pexpect_cmd_delay = self.STANDARD_CMD_DELAY
|
||||||
|
|
||||||
|
def set_proxy_server(
|
||||||
|
self, server_obj, username=DEFAULT_USER, password=None):
|
||||||
|
"""
|
||||||
|
Saves server model representing the proxy server (compute model)
|
||||||
|
If obj does not contain the password, please provide it, it's difficult
|
||||||
|
to connect otherwise...
|
||||||
|
|
||||||
|
@param server_obj: compute model representation of server
|
||||||
|
@param username: User to log into proxy
|
||||||
|
@param password: password for compute VM
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Determine if the password is set
|
||||||
|
if password is not None:
|
||||||
|
server_obj.admin_pass = password
|
||||||
|
|
||||||
|
if (not hasattr(server_obj, 'admin_pass') or
|
||||||
|
getattr(server_obj, 'admin_pass', None) is None):
|
||||||
|
raise NoPasswordProvided()
|
||||||
|
|
||||||
|
server_obj.username = username
|
||||||
|
|
||||||
|
self._proxy_svr = server_obj
|
||||||
|
self._proxy_ip = getattr(self._proxy_svr.addresses.public,
|
||||||
|
'ipv{ver}'.format(ver=self._ip_version))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_server_address(self):
|
||||||
|
return self._proxy_ip
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_server_password(self):
|
||||||
|
return self._proxy_svr.admin_pass
|
||||||
|
|
||||||
|
@proxy_server_password.setter
|
||||||
|
def proxy_server_password(self, password):
|
||||||
|
self._proxy_svr.admin_pass = password
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_server_name(self):
|
||||||
|
return self._proxy_svr.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_server_user(self):
|
||||||
|
return self._proxy_svr.username
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_server_obj(self):
|
||||||
|
return self._proxy_svr
|
||||||
|
|
||||||
|
@proxy_server_obj.setter
|
||||||
|
def proxy_server_obj(self, obj):
|
||||||
|
self.set_proxy_server(server_obj=obj)
|
||||||
|
|
||||||
|
def display_conn_info(self, conn_info):
|
||||||
|
"""
|
||||||
|
Display connection info and all input/output at time of invocation,
|
||||||
|
plus input/output per command
|
||||||
|
|
||||||
|
@param conn_info: Populated SshResponse Object (proxy_mgr.ssh_util)
|
||||||
|
@return: None
|
||||||
|
|
||||||
|
"""
|
||||||
|
output = ("Connection:\n{conn}\nSTDOUT:\n{stdout}\nSTDIN:\n"
|
||||||
|
"{stdin}\nALL:\n{all}").format(
|
||||||
|
conn=conn_info, stdin=conn_info.stdin, stdout=conn_info.stdout,
|
||||||
|
all=conn_info.output)
|
||||||
|
|
||||||
|
per_command = "\n\nPER COMMAND:\n"
|
||||||
|
for cmd, out in conn_info.cmd_output.iteritems():
|
||||||
|
command_str = "CMD: '{0}'\n{1}\n\n".format(cmd, out)
|
||||||
|
per_command = '{0}\n{1}'.format(per_command, command_str)
|
||||||
|
|
||||||
|
self.logger.debug("{0}\n{1}".format(output, per_command))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _connect_to_local_proxy():
|
||||||
|
"""
|
||||||
|
Placeholder in case there is a different mechanism used to connect
|
||||||
|
to the local host (e.g. - subprocess() or popen())
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Basic test to verify functionality
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
def display_conn_info(conn_info):
|
||||||
|
print "Connection:", conn_info
|
||||||
|
print '\nSTDOUT\n', conn_info.stdout
|
||||||
|
print '\nSTDIN\n', conn_info.stdin
|
||||||
|
print '\n\nALL\n', conn_info.output
|
||||||
|
|
||||||
|
print "\n\nPER COMMAND:\n"
|
||||||
|
for cmd, out in conn_info.cmd_output.iteritems():
|
||||||
|
print "CMD: '{0}'\n{1}\n\n".format(cmd, out)
|
||||||
|
|
||||||
|
class Proxy(object):
|
||||||
|
def __init__(self, password=None, username=None, id_=None):
|
||||||
|
self.admin_pass = password
|
||||||
|
self.username = username
|
||||||
|
self.id = id_
|
||||||
|
|
||||||
|
# Flags to test specific scenarios (local/proxy, ping/ssh/both)
|
||||||
|
use_proxy = True
|
||||||
|
ssh = True
|
||||||
|
ping = True
|
||||||
|
|
||||||
|
# Test connect via localhost
|
||||||
|
# THESE VALUES NEED TO BE UPDATED TO USE EXISTING VM (UP TO USER)
|
||||||
|
proxy = NetworkProxyMgr(use_proxy=use_proxy, ip_version=4, logger=None,
|
||||||
|
debug=True, proxy_os=NetworkProxyMgr.LINUX)
|
||||||
|
|
||||||
|
username = 'root'
|
||||||
|
password = '<insert password>'
|
||||||
|
proxy._proxy_ip = '<insert proxy ip>'
|
||||||
|
target_ip = '<insert target ip>'
|
||||||
|
target_id = '<insert target host id>' # For tracking purposes only
|
||||||
|
|
||||||
|
proxy._proxy_svr = Proxy(
|
||||||
|
username=username, password=password, id_='<insert proxy host id>')
|
||||||
|
|
||||||
|
commands = ['ls -alF', 'hostname -I', 'exit']
|
||||||
|
|
||||||
|
if ping:
|
||||||
|
print "PING {0} --> {1}".format(target_ip, proxy.ping(target_ip))
|
||||||
|
|
||||||
|
if ssh:
|
||||||
|
response = proxy.ssh_to_target(
|
||||||
|
target_ip=target_ip, user=username, password=password,
|
||||||
|
proxy_user=username, proxy_pswd=password,
|
||||||
|
proxy_ip=proxy._proxy_ip, cmds=commands)
|
||||||
|
|
||||||
|
display_conn_info(response)
|
||||||
|
print "CONNS:", proxy._conn_path
|
62
cloudcafe/networking/networks/common/proxy_mgr/readme.txt
Normal file
62
cloudcafe/networking/networks/common/proxy_mgr/readme.txt
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
PROXY_MGR
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
--------------------
|
||||||
|
The purpose of the proxy_mgr is to provide a simplified interface to interact
|
||||||
|
with a remote host via a proxy (single hop for now, multi-hop in the future).
|
||||||
|
|
||||||
|
By instantiating the proxy manager and providing the proxy host information, a
|
||||||
|
user can ping or ssh/execute commands a remote system via the remote manager.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
# Given a server_model_obj representing the proxy and
|
||||||
|
# a remote host ip, username, & password.
|
||||||
|
|
||||||
|
proxy = NetworkProxyMgr(use_proxy=True)
|
||||||
|
proxy.set_proxy_server(server_obj=server_model_obj)
|
||||||
|
|
||||||
|
output = proxy.ping(target_ip)
|
||||||
|
response = proxy.ssh_to_target(
|
||||||
|
target_ip=target_ip, user=<username>, password=<password>,
|
||||||
|
cmds=<list of cmds>)
|
||||||
|
|
||||||
|
proxy.close_connections(response)
|
||||||
|
|
||||||
|
|
||||||
|
Basic Architecture
|
||||||
|
------------------------
|
||||||
|
For code organizational purposes, the proxy server information is defined in
|
||||||
|
the NetworkProxyMgr class, and the various utility classes (ping/ssh) are
|
||||||
|
maintained as dependent mixin classes. This is done to keep the code
|
||||||
|
organized, maintainable (e.g. - ping issues are in the ping mixin), and
|
||||||
|
additional utilities can be added with minimal effort.
|
||||||
|
|
||||||
|
|
||||||
|
Basic Methods and Basic Args (not complete list):
|
||||||
|
-------------------------------------------------
|
||||||
|
|
||||||
|
ping(target_ip, count, threshold) - Pings remote target.
|
||||||
|
Return: (Boolean) 'count' pings sent to target_ip, minimum threshold
|
||||||
|
of replies received.
|
||||||
|
|
||||||
|
connect_to_proxy()
|
||||||
|
Return: open pexpect console connection to the proxy.
|
||||||
|
|
||||||
|
can_ssh(target_ip, user, password)
|
||||||
|
Return: (Boolean) Was SSH connection be negotiated (via proxy).
|
||||||
|
|
||||||
|
ssh_to_target(target_ip, user, password, cmds)
|
||||||
|
Return: Response Object (described below) of SSH transaction.
|
||||||
|
|
||||||
|
|
||||||
|
Response Object:
|
||||||
|
------------------------
|
||||||
|
Basic storage object that contains:
|
||||||
|
stdin - all stdin in conversation
|
||||||
|
stdout - all stdout in conversation
|
||||||
|
stderr - all stderr in conversation
|
||||||
|
output - all stdin|out|err interlaced into conversation
|
||||||
|
cmd_output - ordered dictionary of key: cmd, value: output
|
||||||
|
errors - Any unexpected exceptions, etc.
|
||||||
|
connection - open pexpect connection (if closed, value = None)
|
490
cloudcafe/networking/networks/common/proxy_mgr/ssh_util.py
Normal file
490
cloudcafe/networking/networks/common/proxy_mgr/ssh_util.py
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
import pexpect
|
||||||
|
|
||||||
|
|
||||||
|
class SshUnableToConnect(Exception):
|
||||||
|
MSG = 'Unable to connect to: {ip} (Type: {t_type})'
|
||||||
|
|
||||||
|
def __init__(self, target_ip=None, target_type=None):
|
||||||
|
target_type = target_type or 'Not Specified'
|
||||||
|
args = {'ip': target_ip, 't_type': target_type}
|
||||||
|
self.message = self.MSG.format(**args)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
|
class MissingCredentials(SshUnableToConnect):
|
||||||
|
MSG = 'Missing credentials to connect to: {ip} (Type: {t_type})'
|
||||||
|
|
||||||
|
|
||||||
|
class SshResponse(object):
|
||||||
|
str_concat = '{0!s}\n{1!s}'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Supports pexpect connection info:
|
||||||
|
+ Contains open connection (if open)
|
||||||
|
+ Tracks various aspects of open connection: STDOUT, STDIN, STDERR
|
||||||
|
+ Tracks I/O per command (if commands issued over SSH connection)
|
||||||
|
+ errors = Any caught exceptions
|
||||||
|
|
||||||
|
+ stdin = all stdin
|
||||||
|
+ stdout = all stdout
|
||||||
|
+ output = interlaced stdin/stdout
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.stdout = ''
|
||||||
|
self.stdin = ''
|
||||||
|
self.stderr = ''
|
||||||
|
self.output = ''
|
||||||
|
self.cmd_output = OrderedDict()
|
||||||
|
self.errors = ''
|
||||||
|
self.connection = None
|
||||||
|
|
||||||
|
def _add(self, attribute, value):
|
||||||
|
prop = getattr(self, attribute, self.stdout)
|
||||||
|
setattr(self, attribute, self.str_concat.format(prop, value))
|
||||||
|
self.output = self.str_concat.format(self.output, value)
|
||||||
|
|
||||||
|
def add_to_stdin(self, value):
|
||||||
|
self._add('stdin', value)
|
||||||
|
|
||||||
|
def add_to_stdout(self, value):
|
||||||
|
self._add('stdout', value)
|
||||||
|
|
||||||
|
def add_to_stderr(self, value):
|
||||||
|
self._add('stderr', value)
|
||||||
|
|
||||||
|
def add_to_cmd_out(self, cmd, value):
|
||||||
|
self._add('stdout', value)
|
||||||
|
self.cmd_output[cmd] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def result(self):
|
||||||
|
return self.errors == ''
|
||||||
|
|
||||||
|
|
||||||
|
class SshMixin(object):
|
||||||
|
|
||||||
|
PSWD_PROMPT_REGEX = r'ssword\:\s*'
|
||||||
|
LINUX_PROMPT_REGEX = r'[$#>]\s*$'
|
||||||
|
SSH_KEY_REGEX = r'connecting\s+\(yes\/no\)\?\s*'
|
||||||
|
|
||||||
|
DEFAULT_CMD = 'ls -alF'
|
||||||
|
|
||||||
|
def connect_to_proxy(
|
||||||
|
self, user=None, ip_address=None, password=None):
|
||||||
|
"""
|
||||||
|
Connect to proxy or local host
|
||||||
|
|
||||||
|
Note: Parameters are only for the remote connections
|
||||||
|
|
||||||
|
@param user: Use different user other than registered proxy user
|
||||||
|
@param password: Use different password other than registered pswd
|
||||||
|
@param ip_address: Use specific IP address other than proxy address
|
||||||
|
|
||||||
|
@return: Active SSH connection to target
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = user or self.proxy_server_user
|
||||||
|
ip_address = ip_address or self.proxy_server_address
|
||||||
|
password = password or self.proxy_server_password
|
||||||
|
|
||||||
|
# Establish connection to proxy and return open connection
|
||||||
|
if self.use_proxy:
|
||||||
|
response_obj = self._ssh_from_here(
|
||||||
|
user=user, password=password, target_ip=ip_address)
|
||||||
|
conn = response_obj.connection
|
||||||
|
|
||||||
|
self.last_response = response_obj
|
||||||
|
|
||||||
|
# Return nothing, since it is a local connection.... for now, user can
|
||||||
|
# open pipe/process for local connection.
|
||||||
|
else:
|
||||||
|
conn = self._connect_to_local_proxy()
|
||||||
|
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def can_ssh(self, target_ip, user, password, cmd=DEFAULT_CMD):
|
||||||
|
"""
|
||||||
|
Verifies SSH connectivity to specific target. The routine will connect
|
||||||
|
to the target IP and issue a single command. If the command returns
|
||||||
|
anything, including an error, SSH login was successful.
|
||||||
|
|
||||||
|
@param target_ip: SSH to IP
|
||||||
|
@param user: Log in as user 'x'
|
||||||
|
@param password: Log in using password
|
||||||
|
@param cmd: Command to execute if login worked
|
||||||
|
|
||||||
|
@return: (Boolean) : Did SSH work? True=YES, False=No
|
||||||
|
|
||||||
|
"""
|
||||||
|
output = self.ssh_to_target(
|
||||||
|
target_ip=target_ip, user=user, password=password, cmds=[cmd])
|
||||||
|
self.last_response = output
|
||||||
|
return cmd in output.output
|
||||||
|
|
||||||
|
def ssh_to_target(
|
||||||
|
self, target_ip=None, user=None, password=None, cmds=None,
|
||||||
|
proxy_user=None, proxy_pswd=None, proxy_ip=None,
|
||||||
|
close_when_done=True, response_obj=None):
|
||||||
|
"""
|
||||||
|
SSH to the target host from the specified host. If target_ip is not
|
||||||
|
specified, the response_obj with an open connection must be provided.
|
||||||
|
The open connection will be used to use the commands. If neither the
|
||||||
|
(target_ip, user, and password) or the response_obj is specified,
|
||||||
|
an UnableToConnect exception will be raised.
|
||||||
|
|
||||||
|
NOTE: Currently as implemented, only Linux hosts are supported by this
|
||||||
|
routine.
|
||||||
|
|
||||||
|
NOTE: These parameters are only optional if the response_obj is not
|
||||||
|
provided.
|
||||||
|
|
||||||
|
:param target_ip: IP Address to connect to
|
||||||
|
:param user: username for SSH connection
|
||||||
|
:param password: password for SSH connection
|
||||||
|
|
||||||
|
:param proxy_user: Specify different user than what was configured in
|
||||||
|
the proxy_mgr.
|
||||||
|
:param proxy_pswd: Specify different password than what was
|
||||||
|
configured in the proxy_mgr.
|
||||||
|
:param proxy_ip: Specific different target IP than what was
|
||||||
|
configured in the proxy mgr.
|
||||||
|
:param close_when_done: Close the connection when complete.
|
||||||
|
:param cmds: OrderDict of cmds to execute to verify connection
|
||||||
|
(DEFAULT = 'ls -alF'). The Key is the command, the value is the
|
||||||
|
regexp needed to validate the cmd response. If no commands are
|
||||||
|
executed, the open connection is returned within the response
|
||||||
|
object.
|
||||||
|
:param response_obj: Provided SshResponse Object a for open
|
||||||
|
connection to pass cmds to...
|
||||||
|
|
||||||
|
:return: SSH Response object
|
||||||
|
"""
|
||||||
|
if response_obj is not None:
|
||||||
|
self.display_conn_info(response_obj)
|
||||||
|
|
||||||
|
if response_obj is None:
|
||||||
|
|
||||||
|
# Make sure there is an IP to connect to...
|
||||||
|
if target_ip is None:
|
||||||
|
raise SshUnableToConnect(
|
||||||
|
target_ip=target_ip, target_type='target server')
|
||||||
|
|
||||||
|
password = password or self.proxy_server_password
|
||||||
|
if None in [user, password]:
|
||||||
|
raise MissingCredentials(
|
||||||
|
target_ip=target_ip, target_type='target server')
|
||||||
|
|
||||||
|
# Ok, we have enough info to log in...
|
||||||
|
msg = 'No connection was provided. Establishing connection.'
|
||||||
|
self.logger.info(msg)
|
||||||
|
ssh_args = {'target_ip': target_ip, 'user': user,
|
||||||
|
'password': password}
|
||||||
|
|
||||||
|
# If we need a proxy
|
||||||
|
if self.use_proxy:
|
||||||
|
|
||||||
|
# Get the proxy connection info...
|
||||||
|
proxy_user = proxy_user or self.proxy_server_user
|
||||||
|
proxy_pswd = proxy_pswd or self.proxy_server_password
|
||||||
|
proxy_ip = proxy_ip or self.proxy_server_address
|
||||||
|
|
||||||
|
if None in [proxy_user, proxy_pswd, proxy_ip]:
|
||||||
|
raise MissingCredentials(
|
||||||
|
target_ip=proxy_ip, target_type='proxy server')
|
||||||
|
|
||||||
|
# Establish and save the connection to proxy server
|
||||||
|
proxy_connection = self._ssh_from_here(
|
||||||
|
target_ip=proxy_ip, user=proxy_user, password=proxy_pswd)
|
||||||
|
ssh_args['response_obj'] = proxy_connection
|
||||||
|
|
||||||
|
self.logger.debug('Connection Hop Path: {0}'.format(
|
||||||
|
self._conn_path))
|
||||||
|
|
||||||
|
# Make sure we connected...
|
||||||
|
if proxy_connection.connection is None:
|
||||||
|
self.logger.error(
|
||||||
|
'Unable to connect to proxy: {ip}'.format(ip=proxy_ip))
|
||||||
|
self.logger.error(proxy_connection.errors)
|
||||||
|
raise SshUnableToConnect(
|
||||||
|
target_ip=proxy_ip, target_type='proxy')
|
||||||
|
|
||||||
|
# Make SSH connection and verify connection was successful.
|
||||||
|
response_obj = self._ssh_from_here(**ssh_args)
|
||||||
|
if response_obj.connection is None:
|
||||||
|
self.logger.error(
|
||||||
|
'Unable to connect to host: {ip}'.format(ip=proxy_ip))
|
||||||
|
self.logger.error(response_obj.errors)
|
||||||
|
self.logger.debug('Connection Hop Path: {0}'.format(
|
||||||
|
self._conn_path))
|
||||||
|
|
||||||
|
raise SshUnableToConnect(
|
||||||
|
target_ip=target_ip, target_type='target server')
|
||||||
|
|
||||||
|
# If there are commands to execute
|
||||||
|
if cmds is not None:
|
||||||
|
response_obj = self._cmds_via_open_connection(response_obj, cmds)
|
||||||
|
|
||||||
|
self.last_response = response_obj
|
||||||
|
|
||||||
|
# Close the connection if necessary.
|
||||||
|
if close_when_done:
|
||||||
|
self.close_connections(response_obj)
|
||||||
|
|
||||||
|
return response_obj
|
||||||
|
|
||||||
|
def _ssh_from_here(self, target_ip, user, password, response_obj=None):
|
||||||
|
"""
|
||||||
|
Connect via ssh using a pexpect process from the local host or an
|
||||||
|
open pexpect connection to remote host.
|
||||||
|
|
||||||
|
@param target_ip: The IP address of the target host
|
||||||
|
@param user: Username on target host to connect to host as...
|
||||||
|
@param password: Password on target host to connect to host as...
|
||||||
|
@param cmd: Command to execute on the host to validate connection
|
||||||
|
@param timeout: Connection Timeout (if exceeded, stop trying connection
|
||||||
|
and make connection as FAILED).
|
||||||
|
|
||||||
|
@return: String of connection output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
response_obj = response_obj or SshResponse()
|
||||||
|
self.session_password = password
|
||||||
|
|
||||||
|
ssh_options = ('-oStrictHostKeyChecking=no '
|
||||||
|
'-oUserKnownHostsFile=/dev/null')
|
||||||
|
|
||||||
|
# Build SSH command
|
||||||
|
ssh_cmd = 'ssh {options} {user}@{ip}'.format(
|
||||||
|
user=user, ip=target_ip, options=ssh_options)
|
||||||
|
self.logger.debug('SSH INVOCATION CMD: {cmd}'.format(cmd=ssh_cmd))
|
||||||
|
response_obj.add_to_stdin(ssh_cmd)
|
||||||
|
|
||||||
|
# Build list of potential and expected output
|
||||||
|
# NOTE: LINUX_REGEX_PROMPT must be the last entry in the ordered dict
|
||||||
|
expectations = OrderedDict([
|
||||||
|
(pexpect.TIMEOUT, None),
|
||||||
|
(self.PSWD_PROMPT_REGEX, password),
|
||||||
|
(self.SSH_KEY_REGEX, 'yes'),
|
||||||
|
(self.LINUX_PROMPT_REGEX, None)])
|
||||||
|
|
||||||
|
# Set ssh process from the open connection
|
||||||
|
ssh_process = response_obj.connection
|
||||||
|
|
||||||
|
# If the open connection was empty, establish it from the local host
|
||||||
|
if ssh_process is None:
|
||||||
|
ssh_process = pexpect.spawn(ssh_cmd)
|
||||||
|
|
||||||
|
if ssh_process is None:
|
||||||
|
self.logger.error(
|
||||||
|
'Unable to connect to host: {ip}'.format(ip=target_ip))
|
||||||
|
raise SshUnableToConnect(target_ip=target_ip)
|
||||||
|
|
||||||
|
# Record the IP to track hops
|
||||||
|
if target_ip not in self._conn_path:
|
||||||
|
self._conn_path.append(target_ip)
|
||||||
|
ssh_process.delaybeforesend = self._pexpect_cmd_delay
|
||||||
|
response_obj.connection = ssh_process
|
||||||
|
|
||||||
|
# Use the open connection to establish an SSH connection to the target
|
||||||
|
else:
|
||||||
|
ssh_process.sendline(ssh_cmd)
|
||||||
|
if target_ip not in self._conn_path:
|
||||||
|
self._conn_path.append(target_ip)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
# Watch the connection for expected output.
|
||||||
|
try:
|
||||||
|
response = ssh_process.expect(expectations.keys())
|
||||||
|
|
||||||
|
# TIMEOUT, break out of loop and indicate FAILURE
|
||||||
|
except pexpect.TIMEOUT:
|
||||||
|
err = "SSH'ing to target timed out. {0} --> {1}"
|
||||||
|
err_msg = err.format(
|
||||||
|
ssh_process.before, ssh_process.after)
|
||||||
|
self.logger.error(err_msg)
|
||||||
|
|
||||||
|
# Record IO and remove IP from the tracking list
|
||||||
|
response_obj.add_to_stdout(
|
||||||
|
str(ssh_process.before) + str(ssh_process.after))
|
||||||
|
self._conn_path.pop()
|
||||||
|
|
||||||
|
if not self._conn_path:
|
||||||
|
response_obj.connection = None
|
||||||
|
break
|
||||||
|
|
||||||
|
# CONNECTION CLOSED, save output and break out of loop.
|
||||||
|
except pexpect.EOF:
|
||||||
|
self.logger.debug('Reached END OF FILE')
|
||||||
|
response_obj.add_to_stdout(
|
||||||
|
str(ssh_process.before) + str(ssh_process.after))
|
||||||
|
self._conn_path.pop()
|
||||||
|
if not self._conn_path:
|
||||||
|
response_obj.connection = None
|
||||||
|
break
|
||||||
|
|
||||||
|
# If TIMEOUT returned by response, break out of loop and indicate
|
||||||
|
# FAILURE...
|
||||||
|
if response == 0:
|
||||||
|
err = "SSH'ing target timed out. {0} --> {1}"
|
||||||
|
self.logger.error(err.format(
|
||||||
|
ssh_process.before, ssh_process.after))
|
||||||
|
response_obj.add_to_stdout(
|
||||||
|
str(ssh_process.before) + str(ssh_process.after))
|
||||||
|
self._conn_path.pop()
|
||||||
|
if not self._conn_path:
|
||||||
|
response_obj.connection = None
|
||||||
|
break
|
||||||
|
|
||||||
|
# Connection established, the expected prompt was found
|
||||||
|
# (last element in the expectation ordered dict)
|
||||||
|
if response == len(expectations.keys()) - 1:
|
||||||
|
response_obj.add_to_stdout(
|
||||||
|
str(ssh_process.before) + str(ssh_process.match.group()))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Received expected output, transmit corresponding input
|
||||||
|
next_transmit = expectations[expectations.keys()[response]]
|
||||||
|
if next_transmit is None:
|
||||||
|
self.logger.warn("Didn't drop out of loop, but nothing "
|
||||||
|
"additional to transmit.")
|
||||||
|
self.logger.debug('Option pexpect returned: {0} of {1}'.format(
|
||||||
|
response, len(expectations.keys()) - 1))
|
||||||
|
self.logger.debug('Transaction thus far:\n{0}'.format(
|
||||||
|
str(ssh_process.before) + str(ssh_process.match.group()) +
|
||||||
|
str(ssh_process.after)))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Transmit the next command in the process based on matched
|
||||||
|
# expectation
|
||||||
|
self.logger.debug("TX'ing: '{0}'".format(next_transmit))
|
||||||
|
response_obj.add_to_stdout(str(ssh_process.before) +
|
||||||
|
ssh_process.match.group())
|
||||||
|
response_obj.add_to_stdin(next_transmit)
|
||||||
|
ssh_process.sendline(next_transmit)
|
||||||
|
|
||||||
|
# Broke from loop, return all received output...
|
||||||
|
self.last_response = response_obj
|
||||||
|
return response_obj
|
||||||
|
|
||||||
|
def _cmds_via_open_connection(self, response_obj, cmds):
|
||||||
|
"""
|
||||||
|
SSH from the local host using pexpect.
|
||||||
|
|
||||||
|
@param response_obj: Populated SshResponse Obj
|
||||||
|
@param cmds: Ordered Dict of commands to execute on the host to
|
||||||
|
validate connection
|
||||||
|
|
||||||
|
@return: SshResponse Obj
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Build list of potential and expected output
|
||||||
|
expectations = OrderedDict([
|
||||||
|
(pexpect.TIMEOUT, None),
|
||||||
|
(self.PSWD_PROMPT_REGEX, self.session_password),
|
||||||
|
(self.LINUX_PROMPT_REGEX, None)])
|
||||||
|
|
||||||
|
# Get the SSH connection to the target host
|
||||||
|
ssh_process = response_obj.connection
|
||||||
|
|
||||||
|
for cmd in cmds:
|
||||||
|
self.logger.debug("TX'ing CMD: '{0}'".format(cmd))
|
||||||
|
ssh_process.sendline(cmd)
|
||||||
|
response_obj.add_to_stdin(cmd)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
# Watch connection for potential and expected output.
|
||||||
|
try:
|
||||||
|
response = ssh_process.expect(expectations.keys())
|
||||||
|
|
||||||
|
# TIMEOUT, break out of loop and indicate FAILURE
|
||||||
|
except pexpect.TIMEOUT:
|
||||||
|
err = "CMD '{cmd}' timed out. {before} --> {after}"
|
||||||
|
self.logger.error(err.format(
|
||||||
|
before=ssh_process.before, after=ssh_process.after,
|
||||||
|
cmd=cmd))
|
||||||
|
self.logger.debug('Connection Hop Path: {0}'.format(
|
||||||
|
self._conn_path))
|
||||||
|
|
||||||
|
response_obj.add_to_stdout(str(ssh_process.before))
|
||||||
|
if not self._conn_path:
|
||||||
|
response_obj.connection = None
|
||||||
|
break
|
||||||
|
|
||||||
|
# CONNECTION CLOSED, save output and break out of loop.
|
||||||
|
except pexpect.EOF:
|
||||||
|
self.logger.debug('Reached END OF FILE')
|
||||||
|
response_obj.add_to_stdout(str(ssh_process.before))
|
||||||
|
if cmd == 'exit':
|
||||||
|
output = (str(ssh_process.before) +
|
||||||
|
str(ssh_process.after))
|
||||||
|
response_obj.add_to_cmd_out(cmd, output)
|
||||||
|
|
||||||
|
self._conn_path.pop()
|
||||||
|
if not self._conn_path:
|
||||||
|
response_obj.connection = None
|
||||||
|
break
|
||||||
|
|
||||||
|
# If TIMEOUT returned by response, break out of loop and
|
||||||
|
# indicate FAILURE...
|
||||||
|
if response == 0:
|
||||||
|
err = "CMD '{cmd}' timed out. {before} --> {after}"
|
||||||
|
self.logger.error(err.format(
|
||||||
|
before=ssh_process.before, after=ssh_process.after,
|
||||||
|
cmd=cmd))
|
||||||
|
|
||||||
|
response_obj.add_to_stdout(
|
||||||
|
str(ssh_process.before) + str(ssh_process.after))
|
||||||
|
self.logger.debug('Connection Hop Path: {0}'.format(
|
||||||
|
self._conn_path))
|
||||||
|
break
|
||||||
|
|
||||||
|
if response == (len(expectations.keys()) - 1):
|
||||||
|
self.logger.debug('CMD {cmd} issued.'.format(cmd=cmd))
|
||||||
|
output = (str(ssh_process.before) +
|
||||||
|
ssh_process.match.group() +
|
||||||
|
str(ssh_process.after))
|
||||||
|
response_obj.add_to_cmd_out(cmd, output)
|
||||||
|
self.last_response = response_obj
|
||||||
|
break
|
||||||
|
|
||||||
|
# Transmit the next command/input based on matched expectation
|
||||||
|
next_transmit = expectations[expectations.keys()[response]]
|
||||||
|
self.logger.debug("TX'ing: '{0}'".format(next_transmit))
|
||||||
|
response_obj.add_to_stdout(
|
||||||
|
str(ssh_process.before) + ssh_process.match.group())
|
||||||
|
response_obj.add_to_stdin(next_transmit)
|
||||||
|
self.last_response = response_obj
|
||||||
|
|
||||||
|
# Broke from loop, return all received output...
|
||||||
|
self.last_response = response_obj
|
||||||
|
return response_obj
|
||||||
|
|
||||||
|
def close_connections(self, response_obj):
|
||||||
|
"""
|
||||||
|
Close all open connections, based on IP/hop tracking
|
||||||
|
|
||||||
|
@param response_obj: Populated SshResponse Object
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.logger.debug('Closing all open connections: {0}'.format(
|
||||||
|
self._conn_path))
|
||||||
|
|
||||||
|
# If there are connections open...
|
||||||
|
if getattr(response_obj, 'connection', None) is not None:
|
||||||
|
|
||||||
|
# Iterate through the hop list (if the connection is still open)
|
||||||
|
while (list(set(self._conn_path)) and
|
||||||
|
response_obj.connection is not None):
|
||||||
|
|
||||||
|
response_obj = self._cmds_via_open_connection(
|
||||||
|
response_obj, ['exit'])
|
||||||
|
self.last_response = response_obj
|
@ -3,3 +3,4 @@ netaddr
|
|||||||
XenAPI
|
XenAPI
|
||||||
dateutils
|
dateutils
|
||||||
dnspython
|
dnspython
|
||||||
|
pexpect
|
||||||
|
Loading…
x
Reference in New Issue
Block a user