tobiko/tobiko/openstack/topology/_connection.py

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)