Add CirrOS based HTTP server stack

Change-Id: I431ea4dda8b98f21f6f200af75168ab9427a2c7b
This commit is contained in:
Federico Ressi 2021-01-12 11:28:19 +01:00
parent ee0740b8b1
commit a4db6e4d9c
4 changed files with 196 additions and 30 deletions

View File

@ -39,6 +39,7 @@ CirrosSameHostServerStackFixture = _cirros.CirrosSameHostServerStackFixture
RebootCirrosServerOperation = _cirros.RebootCirrosServerOperation RebootCirrosServerOperation = _cirros.RebootCirrosServerOperation
EvacuableCirrosImageFixture = _cirros.EvacuableCirrosImageFixture EvacuableCirrosImageFixture = _cirros.EvacuableCirrosImageFixture
EvacuableServerStackFixture = _cirros.EvacuableServerStackFixture EvacuableServerStackFixture = _cirros.EvacuableServerStackFixture
CirrosHttpServerStackFixture = _cirros.CirrosHttpServerStackFixture
RedHatFlavorStackFixture = _redhat.RedHatFlavorStackFixture RedHatFlavorStackFixture = _redhat.RedHatFlavorStackFixture
RhelImageFixture = _redhat.RhelImageFixture RhelImageFixture = _redhat.RhelImageFixture

View File

@ -93,3 +93,23 @@ class EvacuableServerStackFixture(CirrosServerStackFixture):
class CirrosExternalServerStackFixture(CirrosServerStackFixture, class CirrosExternalServerStackFixture(CirrosServerStackFixture,
_nova.ExternalServerStackFixture): _nova.ExternalServerStackFixture):
pass pass
class CirrosHttpServerStackFixture(CirrosPeerServerStackFixture,
_nova.HttpServerStackFixture):
@property
def user_data(self):
# Launch a webserver on port 80 that replies the server name to the
# client
# This webserver relies on the nc command which may fail if multiple
# clients connect at the same time. For concurrency testing,
# OctaviaCentosServerStackFixture is more suited to handle multiple
# requests.
reply = ("HTTP/1.1 200 OK\r\n"
"Content-Length:8\r\n"
"\r\n"
"$(hostname)")
command = f'nc -lk -p {self.http_server_port} -e echo -e "{reply}"'
return ("#!/bin/sh\n"
f"{command}")

View File

@ -15,21 +15,25 @@
# under the License. # under the License.
from __future__ import absolute_import from __future__ import absolute_import
import abc
import os import os
import typing # noqa import typing
import netaddr
import six import six
from oslo_log import log from oslo_log import log
import tobiko import tobiko
from tobiko import config from tobiko import config
from tobiko.openstack import glance
from tobiko.openstack import heat from tobiko.openstack import heat
from tobiko.openstack import neutron from tobiko.openstack import neutron
from tobiko.openstack import nova from tobiko.openstack import nova
from tobiko.openstack.stacks import _hot from tobiko.openstack.stacks import _hot
from tobiko.openstack.stacks import _neutron from tobiko.openstack.stacks import _neutron
from tobiko.shell import ssh from tobiko.shell import curl
from tobiko.shell import sh from tobiko.shell import sh
from tobiko.shell import ssh
CONF = config.CONF CONF = config.CONF
@ -81,7 +85,7 @@ class FlavorStackFixture(heat.HeatStackFixture):
@neutron.skip_if_missing_networking_extensions('port-security') @neutron.skip_if_missing_networking_extensions('port-security')
class ServerStackFixture(heat.HeatStackFixture): class ServerStackFixture(heat.HeatStackFixture, abc.ABC):
#: Heat template file #: Heat template file
template = _hot.heat_template_file('nova/server.yaml') template = _hot.heat_template_file('nova/server.yaml')
@ -92,8 +96,10 @@ class ServerStackFixture(heat.HeatStackFixture):
#: stack with the internal where the server port is created #: stack with the internal where the server port is created
network_stack = tobiko.required_setup_fixture(_neutron.NetworkStackFixture) network_stack = tobiko.required_setup_fixture(_neutron.NetworkStackFixture)
#: Glance image used to create a Nova server instance @property
image_fixture = None def image_fixture(self) -> glance.GlanceImageFixture:
"""Glance image used to create a Nova server instance"""
raise NotImplementedError
def delete_stack(self, stack_id=None): def delete_stack(self, stack_id=None):
if self._outputs: if self._outputs:
@ -101,28 +107,30 @@ class ServerStackFixture(heat.HeatStackFixture):
super(ServerStackFixture, self).delete_stack(stack_id=stack_id) super(ServerStackFixture, self).delete_stack(stack_id=stack_id)
@property @property
def image(self): def image(self) -> str:
return self.image_fixture.image_id return self.image_fixture.image_id
@property @property
def username(self): def username(self) -> str:
"""username used to login to a Nova server instance""" """username used to login to a Nova server instance"""
return self.image_fixture.username return self.image_fixture.username or 'root'
@property @property
def password(self): def password(self) -> typing.Optional[str]:
"""password used to login to a Nova server instance""" """password used to login to a Nova server instance"""
return self.image_fixture.password return self.image_fixture.password
@property @property
def connection_timeout(self): def connection_timeout(self) -> tobiko.Seconds:
return self.image_fixture.connection_timeout return self.image_fixture.connection_timeout
# Stack used to create flavor for Nova server instance @property
flavor_stack = None def flavor_stack(self) -> FlavorStackFixture:
"""stack used to create flavor for Nova server instance"""
raise NotImplementedError
@property @property
def flavor(self): def flavor(self) -> str:
"""Flavor for Nova server instance""" """Flavor for Nova server instance"""
return self.flavor_stack.flavor_id return self.flavor_stack.flavor_id
@ -130,23 +138,23 @@ class ServerStackFixture(heat.HeatStackFixture):
port_security_enabled = False port_security_enabled = False
#: Security groups to be associated to network ports #: Security groups to be associated to network ports
security_groups = [] # type: typing.List[str] security_groups: typing.List[str] = []
@property @property
def key_name(self): def key_name(self) -> str:
return self.key_pair_stack.key_name return self.key_pair_stack.key_name
@property @property
def network(self): def network(self) -> str:
return self.network_stack.network_id return self.network_stack.network_id
#: Floating IP network where the Neutron floating IP are created #: Floating IP network where the Neutron floating IP are created
@property @property
def floating_network(self): def floating_network(self) -> str:
return self.network_stack.floating_network return self.network_stack.floating_network
@property @property
def has_floating_ip(self): def has_floating_ip(self) -> bool:
return bool(self.floating_network) return bool(self.floating_network)
@property @property
@ -157,17 +165,42 @@ class ServerStackFixture(heat.HeatStackFixture):
connection_timeout=self.connection_timeout) connection_timeout=self.connection_timeout)
@property @property
def ssh_command(self): def ssh_command(self) -> sh.ShellCommand:
return ssh.ssh_command(host=self.ip_address, return ssh.ssh_command(host=self.ip_address,
username=self.username) username=self.username)
@property @property
def ip_address(self): def ip_address(self) -> str:
if self.has_floating_ip: if self.has_floating_ip:
return self.floating_ip_address return self.floating_ip_address
else: else:
return self.outputs.fixed_ips[0]['ip_address'] return self.outputs.fixed_ips[0]['ip_address']
def list_fixed_ips(self, ip_version: typing.Optional[int] = None) -> \
tobiko.Selection[netaddr.IPAddress]:
fixed_ips: tobiko.Selection[netaddr.IPAddress] = tobiko.Selection(
netaddr.IPAddress(fixed_ip['ip_address'])
for fixed_ip in self.fixed_ips)
if ip_version is not None:
fixed_ips.with_attributes(version=ip_version)
return fixed_ips
def find_fixed_ip(self, ip_version: typing.Optional[int] = None,
unique=False) -> netaddr.IPAddress:
fixed_ips = self.list_fixed_ips(ip_version=ip_version)
if unique:
return fixed_ips.unique
else:
return fixed_ips.first
@property
def fixed_ipv4(self):
return self.find_fixed_ip(ip_version=4)
@property
def fixed_ipv6(self):
return self.find_fixed_ip(ip_version=6)
#: Schedule on different host that this Nova server instance ID #: Schedule on different host that this Nova server instance ID
different_host = None different_host = None
@ -305,7 +338,8 @@ class ServerStackFixture(heat.HeatStackFixture):
"method not implemented") "method not implemented")
class ExternalServerStackFixture(ServerStackFixture): class ExternalServerStackFixture(ServerStackFixture, abc.ABC):
# pylint: disable=abstract-method
#: stack with the network where the server port is created #: stack with the network where the server port is created
network_stack = tobiko.required_setup_fixture( network_stack = tobiko.required_setup_fixture(
@ -321,17 +355,19 @@ class ExternalServerStackFixture(ServerStackFixture):
return self.network_stack.network_id return self.network_stack.network_id
class PeerServerStackFixture(ServerStackFixture): class PeerServerStackFixture(ServerStackFixture, abc.ABC):
"""Server witch networking access requires passing by another Nova server """Server witch networking access requires passing by another Nova server
""" """
has_floating_ip = False has_floating_ip = False
#: Peer server used to reach this one @property
peer_stack = None def peer_stack(self) -> ServerStackFixture:
"""Peer server used to reach this one"""
raise NotImplementedError
@property @property
def ssh_client(self): def ssh_client(self) -> ssh.SSHClientFixture:
return ssh.ssh_client(host=self.ip_address, return ssh.ssh_client(host=self.ip_address,
username=self.username, username=self.username,
password=self.password, password=self.password,
@ -339,7 +375,7 @@ class PeerServerStackFixture(ServerStackFixture):
proxy_jump=self.peer_stack.ssh_client) proxy_jump=self.peer_stack.ssh_client)
@property @property
def ssh_command(self): def ssh_command(self) -> sh.ShellCommand:
proxy_command = self.peer_stack.ssh_command + [ proxy_command = self.peer_stack.ssh_command + [
'nc', self.ip_address, '22'] 'nc', self.ip_address, '22']
return ssh.ssh_command(host=self.ip_address, return ssh.ssh_command(host=self.ip_address,
@ -347,19 +383,20 @@ class PeerServerStackFixture(ServerStackFixture):
proxy_command=proxy_command) proxy_command=proxy_command)
@property @property
def network(self): def network(self) -> str:
return self.peer_stack.network return self.peer_stack.network
@nova.skip_if_missing_hypervisors(count=2, state='up', status='enabled') @nova.skip_if_missing_hypervisors(count=2, state='up', status='enabled')
class DifferentHostServerStackFixture(PeerServerStackFixture): class DifferentHostServerStackFixture(PeerServerStackFixture, abc.ABC):
# pylint: disable=abstract-method
@property @property
def different_host(self): def different_host(self):
return [self.peer_stack.server_id] return [self.peer_stack.server_id]
class SameHostServerStackFixture(PeerServerStackFixture): class SameHostServerStackFixture(PeerServerStackFixture, abc.ABC):
@property @property
def same_host(self): def same_host(self):
@ -373,6 +410,43 @@ def as_str(text):
return text.decode() return text.decode()
class HttpServerStackFixture(PeerServerStackFixture, abc.ABC):
http_server_port = 80
http_request_scheme = 'http'
http_request_path = ''
def send_http_request(
self,
hostname: typing.Union[str, netaddr.IPAddress, None] = None,
ip_version: typing.Optional[int] = None,
port: typing.Optional[int] = None,
path: typing.Optional[str] = None,
retry_count: typing.Optional[int] = None,
retry_timeout: tobiko.Seconds = None,
retry_interval: tobiko.Seconds = None,
ssh_client: typing.Optional[ssh.SSHClientFixture] = None,
**curl_parameters) -> str:
if hostname is None:
hostname = self.find_fixed_ip(ip_version=ip_version)
if port is None:
port = self.http_server_port
if path is None:
path = self.http_request_path
if ssh_client is None:
ssh_client = self.peer_stack.ssh_client
return curl.execute_curl(scheme='http',
hostname=hostname,
port=port,
path=path,
retry_count=retry_count,
retry_timeout=retry_timeout,
retry_interval=retry_interval,
ssh_client=ssh_client,
**curl_parameters)
class ServerGroupStackFixture(heat.HeatStackFixture): class ServerGroupStackFixture(heat.HeatStackFixture):
template = _hot.heat_template_file('nova/server_group.yaml') template = _hot.heat_template_file('nova/server_group.yaml')

View File

@ -25,6 +25,7 @@ from tobiko.openstack import keystone
from tobiko.openstack import neutron from tobiko.openstack import neutron
from tobiko.openstack import nova from tobiko.openstack import nova
from tobiko.openstack import stacks from tobiko.openstack import stacks
from tobiko.shell import curl
from tobiko.shell import ping from tobiko.shell import ping
from tobiko.shell import sh from tobiko.shell import sh
@ -38,11 +39,28 @@ class CirrosServerStackTest(testtools.TestCase):
nameservers_filenames: typing.Optional[typing.Sequence[str]] = None nameservers_filenames: typing.Optional[typing.Sequence[str]] = None
def test_ping(self): def test_ping_floating_ip(self):
"""Test connectivity to floating IP address""" """Test connectivity to floating IP address"""
ping.ping_until_received( ping.ping_until_received(
self.stack.floating_ip_address).assert_replied() self.stack.floating_ip_address).assert_replied()
def test_ping_fixed_ipv4(self):
ping.ping_until_received(
self.get_fixed_ip(ip_version=4),
ssh_client=self.stack.ssh_client).assert_replied()
def test_ping_fixed_ipv6(self):
ping.ping_until_received(
self.get_fixed_ip(ip_version=6),
ssh_client=self.stack.ssh_client).assert_replied()
def get_fixed_ip(self, ip_version: int):
try:
return self.stack.find_fixed_ip(ip_version=ip_version)
except tobiko.ObjectNotFound:
self.skipTest(f"Server {self.stack.server_id} has any "
f"IPv{ip_version} address.")
def test_ssh_connect(self): def test_ssh_connect(self):
"""Test SSH connectivity via Paramiko SSHClient""" """Test SSH connectivity via Paramiko SSHClient"""
self.stack.ssh_client.connect() self.stack.ssh_client.connect()
@ -135,3 +153,56 @@ class EvacuablesServerStackTest(CirrosServerStackTest):
def test_image_tags(self): def test_image_tags(self):
image = self.stack.image_fixture.get_image() image = self.stack.image_fixture.get_image()
self.assertEqual(['evacuable'], image.tags) self.assertEqual(['evacuable'], image.tags)
class CirrosPeerServerStackTest(CirrosServerStackTest):
#: Stack of resources with an HTTP server
stack = tobiko.required_setup_fixture(stacks.CirrosPeerServerStackFixture)
def test_ping_floating_ip(self):
self.skipTest(f"Server '{self.stack.server_id}' has any floating IP")
def test_ping_fixed_ipv4(self):
ping.ping_until_received(
self.get_fixed_ip(ip_version=4),
ssh_client=self.stack.peer_stack.ssh_client).assert_replied()
def test_ping_fixed_ipv6(self):
ping.ping_until_received(
self.get_fixed_ip(ip_version=6),
ssh_client=self.stack.peer_stack.ssh_client).assert_replied()
class HttpServerStackTest(CirrosPeerServerStackTest):
#: Stack of resources with an HTTP server
stack = tobiko.required_setup_fixture(stacks.CirrosHttpServerStackFixture)
def test_server_port_ipv4(self):
self._test_server_port(ip_version=4)
def test_server_port_ipv6(self):
self._test_server_port(ip_version=6)
def _test_server_port(self, ip_version: int):
scheme = self.stack.http_request_scheme
ip_address = self.get_fixed_ip(ip_version=ip_version)
port = self.stack.http_server_port
ssh_client = self.stack.peer_stack.ssh_client
reply = curl.execute_curl(scheme=scheme,
hostname=ip_address,
port=port,
ssh_client=ssh_client,
connect_timeout=5.,
retry_count=10,
retry_timeout=60.)
self.assertEqual(self.stack.server_name, reply)
def test_send_http_request_ipv4(self):
reply = self.stack.send_http_request(ip_version=4)
self.assertEqual(self.stack.server_name, reply)
def test_send_http_request_ipv6(self):
reply = self.stack.send_http_request(ip_version=6)
self.assertEqual(self.stack.server_name, reply)