b564c4a7c8
Change-Id: I67f998e7feae496cd8bdce4259232ca4bbd9f73e
172 lines
6.6 KiB
Python
172 lines
6.6 KiB
Python
# Copyright 2020 Red Hat
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
from __future__ import absolute_import
|
|
|
|
import collections
|
|
import typing
|
|
|
|
import netaddr
|
|
from oslo_log import log
|
|
|
|
import tobiko
|
|
from tobiko.openstack.topology import _config
|
|
from tobiko.shell import ssh
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
class UreachableSSHServer(tobiko.TobikoException):
|
|
message = ("Unable to reach SSH server through any address: {addresses}. "
|
|
"Failures: {failures}")
|
|
|
|
|
|
class SSHConnection(object):
|
|
|
|
def __init__(self,
|
|
address: netaddr.IPAddress,
|
|
ssh_client: typing.Optional[ssh.SSHClientFixture] = None,
|
|
proxy_client: typing.Optional[ssh.SSHClientFixture] = None,
|
|
failure: typing.Optional[Exception] = None):
|
|
self.address = address
|
|
self.ssh_client = ssh_client
|
|
self.proxy_client = proxy_client
|
|
self.failure = failure
|
|
|
|
def __repr__(self) -> str:
|
|
attributes = ", ".join(f"{n}={v!r}"
|
|
for n, v in self._iter_attributes())
|
|
return f"{type(self).__name__}({attributes})"
|
|
|
|
def _iter_attributes(self):
|
|
yield 'address', self.address
|
|
if self.ssh_client is not None:
|
|
yield 'ssh_client', self.ssh_client
|
|
if self.proxy_client is not None:
|
|
yield 'proxy_client', self.proxy_client
|
|
if self.failure is not None:
|
|
yield 'failure', self.failure
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
return (self.failure is None and
|
|
self.ssh_client is not None)
|
|
|
|
|
|
SSHConnectionKey = typing.Tuple[netaddr.IPAddress,
|
|
typing.Optional[ssh.SSHClientFixture]]
|
|
SSHConnectionDict = typing.Dict[SSHConnectionKey, SSHConnection]
|
|
|
|
|
|
class SSHConnectionManager(tobiko.SharedFixture):
|
|
|
|
config = tobiko.required_setup_fixture(_config.OpenStackTopologyConfig)
|
|
|
|
def __init__(self):
|
|
super(SSHConnectionManager, self).__init__()
|
|
self._connections: SSHConnectionDict = collections.OrderedDict()
|
|
|
|
def cleanup_fixture(self):
|
|
connections = list(self._connections.values())
|
|
self._connections.clear()
|
|
for connection in connections:
|
|
ssh_client = connection.ssh_client
|
|
if ssh_client is not None:
|
|
ssh_client.close()
|
|
|
|
def connect(self,
|
|
addresses: typing.List[netaddr.IPAddress],
|
|
proxy_client: typing.Optional[ssh.SSHClientFixture] = None,
|
|
**connect_parameters) \
|
|
-> ssh.SSHClientFixture:
|
|
if not addresses:
|
|
raise ValueError(f"'addresses' list is empty: {addresses}")
|
|
connections = self.list_connections(addresses,
|
|
proxy_client=proxy_client)
|
|
try:
|
|
connection = connections.with_attributes(is_valid=True).first
|
|
except tobiko.ObjectNotFound:
|
|
pass
|
|
else:
|
|
assert isinstance(connection.ssh_client, ssh.SSHClientFixture)
|
|
return connection.ssh_client
|
|
|
|
for connection in connections.with_attributes(failure=None):
|
|
# connection not tried yet
|
|
LOG.debug("Establishing SSH connection to "
|
|
f"'{connection.address}' (proxy_client={proxy_client})")
|
|
try:
|
|
ssh_client = self.ssh_client(connection.address,
|
|
proxy_client=proxy_client,
|
|
**connect_parameters)
|
|
ssh_client.connect(retry_count=1, connection_attempts=1)
|
|
except Exception as ex:
|
|
LOG.debug("Failed establishing SSH connect to "
|
|
f"'{connection.address}': {ex}")
|
|
# avoid re-checking again later the same address
|
|
connection.failure = ex
|
|
continue
|
|
else:
|
|
# cache valid connection SSH client for later use
|
|
connection.ssh_client = ssh_client
|
|
assert connection.is_valid
|
|
return ssh_client
|
|
|
|
failures = '\n'.join(str(connection.failure)
|
|
for connection in connections)
|
|
raise UreachableSSHServer(addresses=addresses,
|
|
failures=failures)
|
|
|
|
def list_connections(
|
|
self,
|
|
addresses: typing.List[netaddr.IPAddress],
|
|
proxy_client: ssh.SSHClientFixture = None) \
|
|
-> tobiko.Selection[SSHConnection]:
|
|
# Ensure there is any address duplication
|
|
addresses = list(collections.OrderedDict.fromkeys(addresses))
|
|
return tobiko.Selection[SSHConnection](
|
|
self.get_connection(address, proxy_client=proxy_client)
|
|
for address in addresses)
|
|
|
|
def get_connection(
|
|
self,
|
|
address: netaddr.IPAddress,
|
|
proxy_client: ssh.SSHClientFixture = None) \
|
|
-> SSHConnection:
|
|
tobiko.check_valid_type(address, netaddr.IPAddress)
|
|
tobiko.check_valid_type(proxy_client, ssh.SSHClientFixture,
|
|
type(None))
|
|
connection = SSHConnection(address, proxy_client=proxy_client)
|
|
return self._connections.setdefault((address, proxy_client),
|
|
connection)
|
|
|
|
def ssh_client(self, address, username=None, port=None,
|
|
key_filename=None, **ssh_parameters):
|
|
username = username or self.config.conf.username
|
|
port = port or self.config.conf.port
|
|
key_filename = key_filename or self.config.conf.key_file
|
|
return ssh.ssh_client(host=str(address),
|
|
username=username,
|
|
key_filename=key_filename,
|
|
**ssh_parameters)
|
|
|
|
|
|
SSH_CONNECTIONS = SSHConnectionManager()
|
|
|
|
|
|
def ssh_connect(addresses: typing.List[netaddr.IPAddress],
|
|
manager: typing.Optional[SSHConnectionManager] = None,
|
|
**connect_parameters) -> ssh.SSHClientFixture:
|
|
manager = manager or SSH_CONNECTIONS
|
|
return manager.connect(addresses=addresses, **connect_parameters)
|