Create OpenStack topology API to interact with cloud nodes

Change-Id: I0e1e071c7b4fc70793d44eddf9b1eb1e094a0912
This commit is contained in:
Federico Ressi 2019-10-01 17:16:59 +02:00
parent 7444b88d02
commit 298993d06a
7 changed files with 431 additions and 7 deletions

View File

@ -34,6 +34,7 @@ CONFIG_MODULES = ['tobiko.openstack.glance.config',
'tobiko.openstack.neutron.config',
'tobiko.openstack.nova.config',
'tobiko.openstack.os_faults.config',
'tobiko.openstack.topology.config',
'tobiko.shell.ssh.config',
'tobiko.shell.ping.config',
'tobiko.shell.sh.config',

View File

@ -0,0 +1,24 @@
# Copyright 2019 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
from tobiko.openstack.topology import _exception
from tobiko.openstack.topology import _topology
NoSuchOpenStackTopologyGroup = _exception.NoSuchOpenStackTopologyGroup
NoSuchOpenStackTopologyNode = _exception.NoSuchOpenStackTopologyNode
get_openstack_topology = _topology.get_openstack_topology
OpenStackTopology = _topology.OpenStackTopology

View File

@ -0,0 +1,24 @@
# Copyright 2019 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 tobiko
class NoSuchOpenStackTopologyNode(tobiko.TobikoException):
message = "No such topology node: {name!r}"
class NoSuchOpenStackTopologyGroup(tobiko.TobikoException):
message = "No such topology group: {name!r}"

View File

@ -0,0 +1,260 @@
# Copyright 2019 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 weakref
import socket
import netaddr
from oslo_log import log
import tobiko
from tobiko.shell import ping
from tobiko.shell import sh
from tobiko.shell import ssh
from tobiko.openstack import nova
from tobiko.openstack.topology import _exception
LOG = log.getLogger(__name__)
def get_openstack_topology(topology_class=None):
topology_class = topology_class or DEFAULT_TOPOLOGY_CLASS
return tobiko.setup_fixture(DEFAULT_TOPOLOGY_CLASS)
def list_openstack_nodes():
return get_openstack_topology().nodes
def list_openstack_node_groups():
return get_openstack_topology().groups
def get_default_openstack_topology_class():
return DEFAULT_TOPOLOGY_CLASS
def set_default_openstack_topology_class(topology_class):
# pylint: disable=global-statement
global DEFAULT_TOPOLOGY_CLASS
if not issubclass(topology_class, OpenStackTopology):
message = "{!r} is not subclass of OpenStackTopology".format(
topology_class)
raise TypeError(message)
DEFAULT_TOPOLOGY_CLASS = topology_class
class OpenStackTopologyElement(object):
def __init__(self, topology, name):
self._topology = weakref.ref(topology)
self.name = name
@property
def topology(self):
return self._topology()
def __repr__(self):
return "{cls!s}(topology={topology!r}, name={name!r})".format(
cls=type(self).__name__,
topology=self.topology,
name=self.name)
class OpenStackTopologyNode(OpenStackTopologyElement):
def __init__(self, topology, name, ssh_client=None):
super(OpenStackTopologyNode, self).__init__(topology=topology,
name=name)
self._addresses = list()
self._groups = set()
self._ssh_client = ssh_client
def add_group(self, name):
self._groups.add(name)
@property
def groups(self):
return tobiko.select(self.topology.get_group(name)
for name in sorted(self._groups))
def add_address(self, address):
self._addresses.append(netaddr.IPAddress(address))
@property
def addresses(self):
return tobiko.select(address
for address in self._addresses)
@property
def ssh_client(self):
ssh_client = self._ssh_client
if ssh_client is None:
self._ssh_client = ssh_client = self.topology.get_ssh_client(
host=self.addresses.first)
return ssh_client
class OpenStackTopologyNodeGroup(OpenStackTopologyElement):
def __init__(self, topology, name):
super(OpenStackTopologyNodeGroup, self).__init__(topology=topology,
name=name)
self._nodes = set()
def add_node(self, name):
self._nodes.add(name)
@property
def nodes(self):
return tobiko.select(self.topology.get_node(name)
for name in sorted(self._nodes))
class OpenStackTopologyConfig(tobiko.SharedFixture):
conf = None
def setup_fixture(self):
from tobiko import config
CONF = config.CONF
self.conf = CONF.tobiko.topology
class OpenStackTopology(tobiko.SharedFixture):
_nodes = None
_groups = None
config = tobiko.required_setup_fixture(OpenStackTopologyConfig)
def setup_fixture(self):
self.clear_nodes()
self.discover_nodes()
def clear_nodes(self):
self._nodes = {}
self._groups = {}
def discover_nodes(self):
self.discover_configured_nodes()
self.discover_compute_nodes()
def discover_configured_nodes(self):
for host in self.config.conf.nodes or []:
self.add_node(address=host)
def discover_compute_nodes(self):
for hypervisor in nova.list_hypervisors():
address = hypervisor.host_ip
if not ping.ping(address).received:
LOG.warning("Cannot reach hypervisor IP address %r from host "
"%r", hypervisor.host_ip, socket.gethostname())
address = None
self.add_node(address=address,
hostname=node_name_from_hostname(
hypervisor.hypervisor_hostname),
group_names=['compute'])
def add_node(self, name=None, address=None, hostname=None,
group_names=None, ssh_client=None):
name = name or self.get_node_name(hostname=hostname,
address=address,
ssh_client=ssh_client)
node = self.create_node(name=name, ssh_client=ssh_client)
if address:
node.add_address(address=address)
for group_name in group_names or []:
self.add_group(name=group_name, node_names=[name])
return node
def create_node(self, name, **kwargs):
try:
return self.get_node(name=name)
except _exception.NoSuchOpenStackTopologyNode:
self._nodes[name] = node = self.new_node(name=name, **kwargs)
return node
def get_node(self, name):
try:
return self._nodes[name]
except KeyError:
raise _exception.NoSuchOpenStackTopologyNode(name=name)
def new_node(self, name, **kwargs):
return OpenStackTopologyNode(topology=self, name=name, **kwargs)
@property
def nodes(self):
return tobiko.select(self.get_node(name)
for name in sorted(self._nodes))
def get_ssh_client(self, host, username=None, port=None, key_filename=None,
**ssh_parameters):
if not host:
message = "Invalid host address: {!r}".format(host)
raise ValueError(message)
conf = self.config.conf
return ssh.ssh_client(host=str(host),
username=(username or conf.username),
port=(port or conf.port),
key_filename=(key_filename or conf.key_file),
**ssh_parameters)
def add_group(self, name, node_names=None):
group = self.create_group(name=name)
for node_name in node_names or []:
group.add_node(name=node_name)
self.get_node(name=node_name).add_group(name=name)
def create_group(self, name):
try:
return self.get_group(name=name)
except _exception.NoSuchOpenStackTopologyGroup:
self._groups[name] = group = self.new_group(name=name)
return group
def get_group(self, name):
try:
return self._groups[name]
except KeyError:
raise _exception.NoSuchOpenStackTopologyGroup(name=name)
def new_group(self, name):
return OpenStackTopologyNodeGroup(topology=self, name=name)
@property
def groups(self):
return tobiko.select(self.get_group(name)
for name in sorted(self._groups))
def get_node_name(self, hostname=None, address=None, ssh_client=None):
if address and not hostname:
ssh_client = ssh_client or self.get_ssh_client(host=address)
hostname = sh.get_hostname(ssh_client=ssh_client)
if hostname:
return node_name_from_hostname(hostname=hostname)
message = ("Unable to get node name: hostname={!r}, "
"address={!r}").format(
hostname, address)
raise ValueError(message)
DEFAULT_TOPOLOGY_CLASS = OpenStackTopology
def node_name_from_hostname(hostname):
return hostname.split('.', 1)[0].lower()

View File

@ -0,0 +1,42 @@
# Copyright 2019 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 itertools
from oslo_config import cfg
GROUP_NAME = "topology"
OPTIONS = [
cfg.ListOpt('nodes',
default=None,
help="List of hostname nodes"),
cfg.StrOpt('key_file',
default='~/.ssh/id_rsa',
help="Default SSH key to login to cloud nodes"),
cfg.StrOpt('username',
default=None,
help="Default username for SSH login"),
cfg.StrOpt('port',
default=None,
help="Default port for SSH login"),
]
def register_tobiko_options(conf):
conf.register_opts(group=cfg.OptGroup(GROUP_NAME), opts=OPTIONS)
def list_options():
return [(GROUP_NAME, itertools.chain(OPTIONS))]

View File

@ -173,20 +173,28 @@ class PingStatistics(object):
def assert_transmitted(self):
if not self.transmitted:
tobiko.fail("Any package has been transmitted to %(destination)r",
tobiko.fail("{transmitted!r} package(s) has been transmitted "
"to {destination!r}",
transmitted=self.transmitted,
destination=self.destination)
def assert_not_transmitted(self):
if self.transmitted:
tobiko.fail("Some packages has been transmitted to "
"%(destination)r", destination=self.destination)
tobiko.fail("{transmitted!r} package(s) has been transmitted to "
"{destination!r}",
transmitted=self.transmitted,
destination=self.destination)
def assert_replied(self):
if not self.received:
tobiko.fail("Any reply package has been received from "
"%(destination)r", destination=self.destination)
tobiko.fail("{received!r} reply package(s) has been received from "
"{destination!r}",
received=self.received,
destination=self.destination)
def assert_not_replied(self):
if self.received:
tobiko.fail("Some reply packages has been received from "
"%(destination)r", destination=self.destination)
tobiko.fail("{received!r} reply package(s) has been received from "
"{destination!r}",
received=self.received,
destination=self.destination)

View File

@ -0,0 +1,65 @@
# Copyright (c) 2019 Red Hat, Inc.
#
# All Rights Reserved.
#
# 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 netaddr
import testtools
import tobiko
from tobiko.openstack import nova
from tobiko.openstack import topology
from tobiko.shell import ping
from tobiko.shell import sh
class OpenStackTopologyTest(testtools.TestCase):
topology = tobiko.required_setup_fixture(topology.OpenStackTopology)
def test_get_openstack_topology(self):
topology_class = type(self.topology)
topo = topology.get_openstack_topology(topology_class=topology_class)
self.assertIsInstance(topo, topology.OpenStackTopology)
self.assertNotEqual([], list(topo.nodes))
self.assertNotEqual([], list(topo.groups))
def test_ping_node(self):
topo = topology.get_openstack_topology()
for node in topo.nodes:
for address in node.addresses:
ping.ping(address, count=1).assert_replied()
def test_ssh_client(self):
for node in self.topology.nodes:
hostname = sh.get_hostname(
ssh_client=node.ssh_client).split('.')[0]
self.assertEqual(node.name, hostname)
def test_compute_group(self):
group = self.topology.get_group('compute')
nodes = group.nodes
hypervisors = {hypervisor.hypervisor_hostname.split('.')[0]: hypervisor
for hypervisor in nova.list_hypervisors()}
self.assertEqual(sorted(hypervisors),
sorted([node.name for node in nodes]))
for name, hypervisor in hypervisors.items():
node = self.topology.get_node(name)
self.assertEqual(name, node.name)
if ping.ping(hypervisor.host_ip).received:
self.assertIn(netaddr.IPAddress(hypervisor.host_ip),
node.addresses)
self.assertIn(group, node.groups)