Merge "Create scenario tests for load balancers"

This commit is contained in:
Zuul 2018-04-10 23:26:58 +00:00 committed by Gerrit Code Review
commit 51e91b4dfe
34 changed files with 2987 additions and 70 deletions

6
.gitignore vendored
View File

@ -44,6 +44,7 @@ output/*/index.html
# Sphinx
doc/build
doc/source/_build
# pbr generates these
AUTHORS
@ -55,4 +56,7 @@ ChangeLog
.*sw?
# Files created by releasenotes build
releasenotes/build
releasenotes/build
# Oslo config generator
etc/octavia.tempest.conf.sample

9
doc/requirements.txt Normal file
View File

@ -0,0 +1,9 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
sphinx>=1.6.2,!=1.6.6,!=1.6.7 # BSD
openstackdocstheme>=1.18.1 # Apache-2.0
# releasenotes
reno>=2.5.0 # Apache-2.0

View File

@ -15,20 +15,27 @@
import os
import sys
import openstackdocstheme
from sphinx import apidoc
sys.path.insert(0, os.path.abspath('../..'))
sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
#'sphinx.ext.intersphinx',
'oslosphinx'
'sphinx.ext.viewcode',
'openstackdocstheme',
'oslo_config.sphinxext'
]
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
# execute "export SPHINX_DEBUG=1" in your terminal to disable
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
@ -45,11 +52,14 @@ add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = True
add_module_names = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
modindex_common_prefix = ['octavia_tempest_plugin.']
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
@ -58,9 +68,19 @@ pygments_style = 'sphinx'
# html_theme = '_theme'
# html_static_path = ['static']
html_theme = 'openstackdocs'
html_last_updated_fmt = '%Y-%m-%d %H:%M'
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
# If false, no module index is generated.
html_domain_indices = True
# If false, no index is generated.
html_use_index = True
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
@ -73,3 +93,27 @@ latex_documents = [
# Example configuration for intersphinx: refer to the Python standard library.
#intersphinx_mapping = {'http://docs.python.org/': None}
repository_name = 'openstack/octavia-tempest-plugin'
bug_project = '910'
bug_tag = 'docs'
# TODO(mordred) We should extract this into a sphinx plugin
def run_apidoc(_):
cur_dir = os.path.abspath(os.path.dirname(__file__))
out_dir = os.path.join(cur_dir, '_build', 'modules')
module = os.path.join(cur_dir, '..', '..', 'octavia_tempest_plugin')
# Keep the order of arguments same as the sphinx-apidoc help, otherwise it
# would cause unexpected errors:
# sphinx-apidoc [options] -o <output_path> <module_path>
# [exclude_pattern, ...]
apidoc.main([
'--force',
'-o',
out_dir,
module,
])
def setup(app):
app.connect('builder-inited', run_apidoc)

26
doc/source/configref.rst Normal file
View File

@ -0,0 +1,26 @@
..
Copyright 2018 Rackspace US Inc.
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.
Octavia Tempest Plugin Configuration Options
============================================
.. contents:: Table of Contents
:depth: 2
.. note:: Not all of these options are used by the Octavia tempest tests.
.. show-options::
tempest.config

View File

@ -14,11 +14,16 @@ Contents:
readme
installation
contributing
configref
Indices and tables
==================
.. toctree::
:hidden:
_build/modules/modules
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,31 @@
# Copyright 2017 GoDaddy
#
# 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 tempest import clients
from tempest import config
from octavia_tempest_plugin.services.load_balancer.v2 import (
loadbalancer_client)
CONF = config.CONF
SERVICE_TYPE = 'load-balancer'
class ManagerV2(clients.Manager):
def __init__(self, credentials):
super(ManagerV2, self).__init__(credentials)
self.loadbalancer_client = loadbalancer_client.LoadbalancerClient(
self.auth_provider, SERVICE_TYPE, CONF.identity.region)

View File

@ -0,0 +1,62 @@
# Copyright 2018 Rackspace US 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.
# API field names
ACTIVE_CONNECTIONS = 'active_connections'
ADMIN_STATE_UP = 'admin_state_up'
BYTES_IN = 'bytes_in'
BYTES_OUT = 'bytes_out'
CREATED_AT = 'created_at'
DESCRIPTION = 'description'
FLAVOR = 'flavor'
ID = 'id'
LISTENERS = 'listeners'
LOADBALANCER = 'loadbalancer'
NAME = 'name'
OPERATING_STATUS = 'operating_status'
POOLS = 'pools'
PROJECT_ID = 'project_id'
PROVIDER = 'provider'
PROVISIONING_STATUS = 'provisioning_status'
REQUEST_ERRORS = 'request_errors'
TOTAL_CONNECTIONS = 'total_connections'
UPDATED_AT = 'updated_at'
VIP_ADDRESS = 'vip_address'
VIP_NETWORK_ID = 'vip_network_id'
VIP_PORT_ID = 'vip_port_id'
VIP_SUBNET_ID = 'vip_subnet_id'
VIP_QOS_POLICY_ID = 'vip_qos_policy_id'
# API valid fields
SHOW_LOAD_BALANCER_RESPONSE_FIELDS = (
ADMIN_STATE_UP, CREATED_AT, DESCRIPTION, FLAVOR, ID, LISTENERS, NAME,
OPERATING_STATUS, POOLS, PROJECT_ID, PROVIDER, PROVISIONING_STATUS,
UPDATED_AT, VIP_ADDRESS, VIP_NETWORK_ID, VIP_PORT_ID, VIP_SUBNET_ID,
VIP_QOS_POLICY_ID)
# Other constants
ACTIVE = 'ACTIVE'
ADMIN_STATE_UP_TRUE = 'true'
ASC = 'asc'
DELETED = 'DELETED'
DESC = 'desc'
FIELDS = 'fields'
OFFLINE = 'OFFLINE'
ONLINE = 'ONLINE'
SORT = 'sort'
# RBAC options
ADVANCED = 'advanced'
OWNERADMIN = 'owner_or_admin'
NONE = 'none'

View File

@ -0,0 +1,146 @@
# Copyright 2016 Rackspace Inc.
#
# 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 oslo_config import cfg
from octavia_tempest_plugin.common import constants as const
service_available_group = cfg.OptGroup(name='service_available',
title='Available OpenStack Services')
ServiceAvailableGroup = [
cfg.BoolOpt('load_balancer',
default=True,
help="Whether or not the load-balancer service is expected "
"to be available."),
]
octavia_group = cfg.OptGroup(name='load_balancer',
title='load-balancer service options')
OctaviaGroup = [
# Tempest plugin common options
cfg.StrOpt("region",
default="",
help="The region name to use. If empty, the value "
"of identity.region is used instead. If no such region "
"is found in the service catalog, the first found one is "
"used."),
cfg.StrOpt('catalog_type',
default='load-balancer',
help='Catalog type of the Octavia service.'),
cfg.StrOpt('endpoint_type',
default='publicURL',
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
help="The endpoint type to use for the load-balancer service"),
cfg.IntOpt('build_interval',
default=5,
help='Time in seconds between build status checks for '
'non-load-balancer resources to build'),
cfg.IntOpt('build_timeout',
default=30,
help='Timeout in seconds to wait for non-load-balancer '
'resources to build'),
# load-balancer specific options
cfg.IntOpt('check_interval',
default=5,
help='Interval to check for status changes.'),
cfg.IntOpt('check_timeout',
default=60,
help='Timeout, in seconds, to wait for a status change.'),
cfg.BoolOpt('test_with_noop',
default=False,
help='Runs the tests assuming no-op drivers are being used. '
'Tests will assume no actual amphora are created.'),
cfg.IntOpt('lb_build_interval',
default=10,
help='Time in seconds between build status checks for a '
'load balancer.'),
cfg.IntOpt('lb_build_timeout',
default=900,
help='Timeout in seconds to wait for a '
'load balancer to build.'),
cfg.StrOpt('member_role',
default='load-balancer_member',
help='The load balancing member RBAC role.'),
cfg.StrOpt('admin_role',
default='load-balancer_admin',
help='The load balancing admin RBAC role.'),
cfg.IntOpt('scp_connection_timeout',
default=5,
help='Timeout in seconds to wait for a '
'scp connection to complete.'),
cfg.IntOpt('scp_connection_attempts',
default=20,
help='Retries for scp to attempt to connect.'),
cfg.StrOpt('provider',
default='octavia',
help='The provider driver to use for the tests.'),
cfg.StrOpt('RBAC_test_type', default=const.ADVANCED,
choices=[const.ADVANCED, const.OWNERADMIN, const.NONE],
help='Type of RBAC tests to run. "advanced" runs the octavia '
'default RBAC tests. "owner_or_admin" runs the legacy '
'owner or admin tests. "none" disables the RBAC tests.'),
# Networking
cfg.BoolOpt('test_with_ipv6',
default=True,
help='When true the IPv6 tests will be run.'),
cfg.BoolOpt('disable_boot_network', default=False,
help='True if your cloud does not allow creating networks or '
'specifying the boot network for instances.'),
cfg.BoolOpt('enable_security_groups', default=False,
help='When true, security groups will be created for the test '
'servers. When false, port security will be disabled on '
'the created networks.'),
cfg.StrOpt('test_network_override',
help='Overrides network creation and uses this network ID for '
'all tests (VIP, members, etc.). Required if '
'test_subnet_override is set.'),
cfg.StrOpt('test_subnet_override',
help='Overrides subnet creation and uses this subnet ID for '
'all IPv4 tests (VIP, members, etc.). Optional'),
cfg.StrOpt('test_ipv6_subnet_override',
help='Overrides subnet creation and uses this subnet ID for '
'all IPv6 tests (VIP, members, etc.). Optional and only '
'valid if test_network_override is set.'),
cfg.StrOpt('vip_subnet_cidr',
default='10.1.1.0/24',
help='CIDR format subnet to use for the vip subnet.'),
cfg.StrOpt('vip_ipv6_subnet_cidr',
default='fdde:1a92:7523:70a0::/64',
help='CIDR format subnet to use for the IPv6 vip subnet.'),
cfg.StrOpt('member_1_ipv4_subnet_cidr',
default='10.2.1.0/24',
help='CIDR format subnet to use for the member 1 subnet.'),
cfg.StrOpt('member_1_ipv6_subnet_cidr',
default='fd7b:f9f7:0fff:4eca::/64',
help='CIDR format subnet to use for the member 1 ipv6 subnet.'),
cfg.StrOpt('member_2_ipv4_subnet_cidr',
default='10.2.2.0/24',
help='CIDR format subnet to use for the member 2 subnet.'),
cfg.StrOpt('member_2_ipv6_subnet_cidr',
default='fd77:1457:4cf0:26a8::/64',
help='CIDR format subnet to use for the member 1 ipv6 subnet.'),
# Environment specific options
# These are used to accomidate clouds with specific limitations
cfg.IntOpt('random_server_name_length',
default=0,
help='If non-zero, generate a random name of the length '
'provided for each server, in the format "m[A-Z0-9]*". '),
cfg.StrOpt('availability_zone',
default=None,
help='Availability zone to use for creating servers.'),
]

View File

@ -0,0 +1,60 @@
# Copyright 2017 GoDaddy
#
# 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.
#
import os
from tempest import config
from tempest.test_discover import plugins
from octavia_tempest_plugin import config as project_config
class OctaviaTempestPlugin(plugins.TempestPlugin):
def load_tests(self):
base_path = os.path.split(os.path.dirname(
os.path.abspath(__file__)))[0]
test_dir = "octavia_tempest_plugin/tests"
full_test_dir = os.path.join(base_path, test_dir)
return full_test_dir, base_path
def register_opts(self, conf):
config.register_opt_group(conf, project_config.service_available_group,
project_config.ServiceAvailableGroup)
config.register_opt_group(conf, project_config.octavia_group,
project_config.OctaviaGroup)
def get_opt_lists(self):
return [
(project_config.service_available_group.name,
project_config.ServiceAvailableGroup),
(project_config.octavia_group.name,
project_config.OctaviaGroup),
]
def get_service_clients(self):
octavia_config = config.service_client_config(
project_config.octavia_group.name
)
params = {
'name': 'load-balancer_v2',
'service_version': 'load-balancer.v2',
'module_path': 'octavia_tempest_plugin.services.load_balancer.v2',
'client_names': ['LoadbalancerClient'],
}
params.update(octavia_config)
return [params]

View File

@ -0,0 +1,17 @@
# Copyright 2018 Rackspace US 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 octavia_tempest_plugin.services.load_balancer import v2
__all__ = ['v2']

View File

@ -0,0 +1,18 @@
# Copyright 2018 Rackspace US 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 octavia_tempest_plugin.services.load_balancer.v2.loadbalancer_client \
import LoadbalancerClient
__all__ = ['LoadbalancerClient']

View File

@ -0,0 +1,526 @@
# Copyright 2017 GoDaddy
#
# 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.
#
import json
from tempest import config
from tempest.lib.common import rest_client
CONF = config.CONF
class LoadbalancerClient(rest_client.RestClient):
_uri = '/v2.0/lbaas/loadbalancers'
def __init__(self, auth_provider, service, region, **kwargs):
super(LoadbalancerClient, self).__init__(auth_provider, service,
region, **kwargs)
self.timeout = CONF.load_balancer.lb_build_timeout
self.build_interval = CONF.load_balancer.lb_build_interval
self.resource_name = 'load balancer'
self.get_status = self.show_loadbalancer
def list_loadbalancers(self, query_params=None, return_object_only=True):
"""Get a list of load balancers.
:param query_params: The optional query parameters to append to the
request. Ex. fields=id&fields=name
:param return_object_only: If True, the response returns the object
inside the root tag. False returns the full
response from the API.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: A list of load balancers object.
"""
if query_params:
request_uri = '{0}?{1}'.format(self._uri, query_params)
else:
request_uri = self._uri
response, body = self.get(request_uri)
self.expected_success(200, response.status)
if return_object_only:
return json.loads(body.decode('utf-8'))['loadbalancers']
else:
return json.loads(body.decode('utf-8'))
def create_loadbalancer_dict(self, lb_dict, return_object_only=True):
"""Create a load balancer using a dictionary.
Example lb_dict::
lb_dict = {'loadbalancer': {
'vip_network_id': 'd0be73da-921a-4e03-9c49-f13f18f7e39f',
'name': 'TEMPEST_TEST_LB',
'description': 'LB for Tempest tests'}
}
:param lb_dict: A dictionary describing the load balancer.
:param return_object_only: If True, the response returns the object
inside the root tag. False returns the full
response from the API.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: A load balancer object.
"""
response, body = self.post(self._uri, json.dumps(lb_dict))
self.expected_success(201, response.status)
if return_object_only:
return json.loads(body.decode('utf-8'))['loadbalancer']
else:
return json.loads(body.decode('utf-8'))
def create_loadbalancer(self, admin_state_up=None, description=None,
flavor=None, listeners=None, name=None,
project_id=None, provider=None, vip_address=None,
vip_network_id=None, vip_port_id=None,
vip_qos_policy_id=None, vip_subnet_id=None,
return_object_only=True):
"""Create a load balancer.
:param admin_state_up: The administrative state of the resource, which
is up (true) or down (false).
:param description: A human-readable description for the resource.
:param flavor: The load balancer flavor ID.
:param listeners: A list of listner dictionaries.
:param name: Human-readable name of the resource.
:param project_id: The ID of the project owning this resource.
:param provider: Provider name for the load balancer.
:param vip_address: The IP address of the Virtual IP (VIP).
:param vip_network_id: The ID of the network for the Virtual IP (VIP).
:param vip_port_id: The ID of the Virtual IP (VIP) port.
:param vip_qos_policy_id: The ID of the QoS Policy which will apply to
the Virtual IP (VIP).
:param vip_subnet_id: The ID of the subnet for the Virtual IP (VIP).
:param return_object_only: If True, the response returns the object
inside the root tag. False returns the full
response from the API.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: A load balancer object.
"""
method_args = locals()
lb_params = {}
for param, value in method_args.items():
if param not in ('self',
'return_object_only') and value is not None:
lb_params[param] = value
lb_dict = {'loadbalancer': lb_params}
return self.create_loadbalancer_dict(lb_dict, return_object_only)
def delete_loadbalancer(self, lb_id, cascade=False, ignore_errors=False):
"""Delete a load balancer.
:param lb_id: The load balancer ID to delete.
:param cascade: If true will delete all child objects of the
load balancer.
:param ignore_errors: True if errors should be ignored.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: None if ignore_errors is True, the response status code
if not.
"""
if cascade:
uri = '{0}/{1}?cascade=true'.format(self._uri, lb_id)
else:
uri = '{0}/{1}'.format(self._uri, lb_id)
if ignore_errors:
try:
response, body = self.delete(uri)
except ignore_errors:
return
else:
response, body = self.delete(uri)
self.expected_success(204, response.status)
return response.status
def failover_loadbalancer(self, lb_id):
"""Failover a load balancer.
:param lb_id: The load balancer ID to query.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: None
"""
uri = '{0}/{1}/failover'.format(self._uri, lb_id)
response, body = self.put(uri, '')
self.expected_success(202, response.status)
return
def show_loadbalancer(self, lb_id, query_params=None,
return_object_only=True):
"""Get load balancer details.
:param lb_id: The load balancer ID to query.
:param query_params: The optional query parameters to append to the
request. Ex. fields=id&fields=name
:param return_object_only: If True, the response returns the object
inside the root tag. False returns the full
response from the API.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: A load balancer object.
"""
if query_params:
request_uri = '{0}/{1}?{2}'.format(self._uri, lb_id, query_params)
else:
request_uri = '{0}/{1}'.format(self._uri, lb_id)
response, body = self.get(request_uri)
self.expected_success(200, response.status)
if return_object_only:
return json.loads(body.decode('utf-8'))['loadbalancer']
else:
return json.loads(body.decode('utf-8'))
def get_loadbalancer_stats(self, lb_id, query_params=None,
return_object_only=True):
"""Get load balancer statistics.
:param lb_id: The load balancer ID to query.
:param query_params: The optional query parameters to append to the
request. Ex. fields=id&fields=name
:param return_object_only: If True, the response returns the object
inside the root tag. False returns the full
response from the API.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: A load balancer statistics object.
"""
if query_params:
request_uri = '{0}/{1}/stats?{2}'.format(self._uri, lb_id,
query_params)
else:
request_uri = '{0}/{1}/stats'.format(self._uri, lb_id)
response, body = self.get(request_uri)
self.expected_success(200, response.status)
if return_object_only:
return json.loads(body.decode('utf-8'))['stats']
else:
return json.loads(body.decode('utf-8'))
def get_loadbalancer_status(self, lb_id, query_params=None,
return_object_only=True):
"""Get a load balancer status tree.
:param lb_id: The load balancer ID to query.
:param query_params: The optional query parameters to append to the
request. Ex. fields=id&fields=name
:param return_object_only: If True, the response returns the object
inside the root tag. False returns the full
response from the API.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: A load balancer statuses object.
"""
if query_params:
request_uri = '{0}/{1}/status?{2}'.format(self._uri, lb_id,
query_params)
else:
request_uri = '{0}/{1}/status'.format(self._uri, lb_id)
response, body = self.get(request_uri)
self.expected_success(200, response.status)
if return_object_only:
return json.loads(body.decode('utf-8'))['statuses']
else:
return json.loads(body.decode('utf-8'))
def update_loadbalancer_dict(self, lb_id, lb_dict,
return_object_only=True):
"""Update a load balancer using a dictionary.
Example lb_dict::
lb_dict = {'loadbalancer': {'name': 'TEMPEST_TEST_LB_UPDATED'} }
:param lb_id: The load balancer ID to update.
:param lb_dict: A dictionary of elements to update on the load
balancer.
:param return_object_only: If True, the response returns the object
inside the root tag. False returns the full
response from the API.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: A load balancer object.
"""
uri = '{0}/{1}'.format(self._uri, lb_id)
response, body = self.put(uri, json.dumps(lb_dict))
self.expected_success(200, response.status)
if return_object_only:
return json.loads(body.decode('utf-8'))['loadbalancer']
else:
return json.loads(body.decode('utf-8'))
def update_loadbalancer(self, lb_id, admin_state_up=None, description=None,
name=None, vip_qos_policy_id=None,
return_object_only=True):
"""Update a load balancer.
:param lb_id: The load balancer ID to update.
:param admin_state_up: The administrative state of the resource, which
is up (true) or down (false).
:param description: A human-readable description for the resource.
:param name: Human-readable name of the resource.
:param vip_qos_policy_id: The ID of the QoS Policy which will apply to
the Virtual IP (VIP).
:param return_object_only: If True, the response returns the object
inside the root tag. False returns the full
response from the API.
:raises AssertionError: if the expected_code isn't a valid http success
response code
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises Forbidden: If a 403 response code is received
:raises Gone: If a 410 response code is received
:raises InvalidContentType: If a 415 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
:raises InvalidHttpSuccessCode: if the read code isn't an expected
http success code
:raises NotFound: If a 404 response code is received
:raises NotImplemented: If a 501 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises ServerFault: If a 500 response code is received
:raises Unauthorized: If a 401 response code is received
:raises UnexpectedContentType: If the content-type of the response
isn't an expect type
:raises UnexpectedResponseCode: If a response code above 400 is
received and it doesn't fall into any
of the handled checks
:raises UnprocessableEntity: If a 422 response code is received and
couldn't be parsed
:returns: A load balancer object.
"""
method_args = locals()
lb_params = {}
for param, value in method_args.items():
if param not in ('self', 'lb_id',
'return_object_only') and value is not None:
lb_params[param] = value
lb_dict = {'loadbalancer': lb_params}
return self.update_loadbalancer_dict(lb_id, lb_dict,
return_object_only)

View File

@ -0,0 +1,834 @@
# Copyright 2017 GoDaddy
# Copyright 2017 Catalyst IT Ltd
# Copyright 2018 Rackspace US 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.
import testtools
from uuid import UUID
from dateutil import parser
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from tempest.lib import exceptions
from octavia_tempest_plugin.common import constants as const
from octavia_tempest_plugin.tests import test_base
from octavia_tempest_plugin.tests import waiters
CONF = config.CONF
class LoadBalancerAPITest(test_base.LoadBalancerBaseTest):
"""Test the load balancer object API."""
# Note: This test also covers basic load balancer show API
@decorators.idempotent_id('61c6343c-a5d2-4b9f-8c7d-34ea83f0596b')
def test_load_balancer_ipv4_create(self):
self._test_load_balancer_create(4)
# Note: This test also covers basic load balancer show API
@decorators.idempotent_id('fc9996de-4f55-4fc4-b8ef-a4b9170c7078')
@testtools.skipUnless(CONF.load_balancer.test_with_ipv6,
'IPv6 testing is disabled')
def test_load_balancer_ipv6_create(self):
self._test_load_balancer_create(6)
def _test_load_balancer_create(self, ip_version):
"""Tests load balancer create and basic show APIs.
* Tests that users without the load balancer member role cannot
* create load balancers.
* Create a fully populated load balancer.
* Show load balancer details.
* Validate the show reflects the requested values.
"""
lb_name = data_utils.rand_name("lb_member_lb1-create-"
"ipv{}".format(ip_version))
lb_description = data_utils.arbitrary_string(size=255)
lb_kwargs = {const.ADMIN_STATE_UP: True,
const.DESCRIPTION: lb_description,
const.PROVIDER: CONF.load_balancer.provider,
# TODO(johnsom) Fix test to use a real flavor
# flavor=lb_flavor,
# TODO(johnsom) Add QoS
# vip_qos_policy_id=lb_qos_policy_id)
const.NAME: lb_name}
self._setup_lb_network_kwargs(lb_kwargs, ip_version)
# Test that a user without the load balancer role cannot
# create a load balancer
if CONF.load_balancer.RBAC_test_type == const.ADVANCED:
self.assertRaises(
exceptions.Forbidden,
self.os_primary.loadbalancer_client.create_loadbalancer,
**lb_kwargs)
lb = self.mem_lb_client.create_loadbalancer(**lb_kwargs)
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
self.assertTrue(lb[const.ADMIN_STATE_UP])
parser.parse(lb[const.CREATED_AT])
parser.parse(lb[const.UPDATED_AT])
self.assertEqual(lb_description, lb[const.DESCRIPTION])
UUID(lb[const.ID])
self.assertEqual(lb_name, lb[const.NAME])
# Operating status is a measured status, so no-op will not go online
if CONF.load_balancer.test_with_noop:
self.assertEqual(const.OFFLINE, lb[const.OPERATING_STATUS])
else:
self.assertEqual(const.ONLINE, lb[const.OPERATING_STATUS])
self.assertEqual(self.os_roles_lb_member.credentials.project_id,
lb[const.PROJECT_ID])
self.assertEqual(CONF.load_balancer.provider, lb[const.PROVIDER])
self.assertEqual(self.lb_member_vip_net[const.ID],
lb[const.VIP_NETWORK_ID])
self.assertIsNotNone(lb[const.VIP_PORT_ID])
if lb_kwargs[const.VIP_SUBNET_ID]:
self.assertEqual(lb_kwargs[const.VIP_ADDRESS],
lb[const.VIP_ADDRESS])
self.assertEqual(lb_kwargs[const.VIP_SUBNET_ID],
lb[const.VIP_SUBNET_ID])
# Attempt to clean up so that one full test run doesn't start 10+
# amps before the cleanup phase fires
try:
self.mem_lb_client.delete_loadbalancer(lb[const.ID])
waiters.wait_for_deleted_status_or_not_found(
self.mem_lb_client.show_loadbalancer, lb[const.ID],
const.PROVISIONING_STATUS,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
except Exception:
pass
@decorators.idempotent_id('643ef031-c800-45f2-b229-3c8f8b37c829')
def test_load_balancer_delete(self):
"""Tests load balancer create and delete APIs.
* Creates a load balancer.
* Validates that other accounts cannot delete the load balancer
* Deletes the load balancer.
* Validates the load balancer is in the DELETED state.
"""
lb_name = data_utils.rand_name("lb_member_lb1-delete")
lb = self.mem_lb_client.create_loadbalancer(
name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
# Test that a user without the load balancer role cannot
# delete this load balancer
if CONF.load_balancer.RBAC_test_type == const.ADVANCED:
self.assertRaises(
exceptions.Forbidden,
self.os_primary.loadbalancer_client.delete_loadbalancer,
lb[const.ID])
# Test that a different user, with the load balancer member role
# cannot delete this load balancer
if not CONF.load_balancer.RBAC_test_type == const.NONE:
member2_client = self.os_roles_lb_member2.loadbalancer_client
self.assertRaises(exceptions.Forbidden,
member2_client.delete_loadbalancer,
lb[const.ID])
self.mem_lb_client.delete_loadbalancer(lb[const.ID])
waiters.wait_for_deleted_status_or_not_found(
self.mem_lb_client.show_loadbalancer, lb[const.ID],
const.PROVISIONING_STATUS,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
@decorators.idempotent_id('643ef031-c800-45f2-b229-3c8f8b37c829')
def test_load_balancer_delete_cascade(self):
"""Tests load balancer create and cascade delete APIs.
* Creates a load balancer.
* Validates that other accounts cannot delete the load balancer
* Deletes the load balancer with the cascade parameter.
* Validates the load balancer is in the DELETED state.
"""
lb_name = data_utils.rand_name("lb_member_lb1-cascade_delete")
lb = self.mem_lb_client.create_loadbalancer(
name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
# TODO(johnsom) Add other objects when we have clients for them
# Test that a user without the load balancer role cannot
# delete this load balancer
if CONF.load_balancer.RBAC_test_type == const.ADVANCED:
self.assertRaises(
exceptions.Forbidden,
self.os_primary.loadbalancer_client.delete_loadbalancer,
lb[const.ID], cascade=True)
# Test that a different user, with the load balancer member role
# cannot delete this load balancer
if not CONF.load_balancer.RBAC_test_type == const.NONE:
member2_client = self.os_roles_lb_member2.loadbalancer_client
self.assertRaises(exceptions.Forbidden,
member2_client.delete_loadbalancer,
lb[const.ID], cascade=True)
self.mem_lb_client.delete_loadbalancer(lb[const.ID], cascade=True)
waiters.wait_for_deleted_status_or_not_found(
self.mem_lb_client.show_loadbalancer, lb[const.ID],
const.PROVISIONING_STATUS,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
# Helper functions for test load balancer list
def _filter_lbs_by_id(self, lbs, ids):
return [lb for lb in lbs if lb['id'] not in ids]
def _filter_lbs_by_index(self, lbs, indexes):
return [lb for i, lb in enumerate(lbs) if i not in indexes]
@decorators.idempotent_id('6546ef3c-c0e2-46af-b892-f795f4d01119')
def test_load_balancer_list(self):
"""Tests load balancer list API and field filtering.
* Create three load balancers.
* Validates that other accounts cannot list the load balancers.
* List the load balancers using the default sort order.
* List the load balancers using descending sort order.
* List the load balancers using ascending sort order.
* List the load balancers returning one field at a time.
* List the load balancers returning two fields.
* List the load balancers filtering to one of the three.
* List the load balancers filtered, one field, and sorted.
"""
# Get a list of pre-existing LBs to filter from test data
pretest_lbs = self.mem_lb_client.list_loadbalancers()
# Store their IDs for easy access
pretest_lb_ids = [lb['id'] for lb in pretest_lbs]
lb_name = data_utils.rand_name("lb_member_lb2-list")
lb_description = 'B'
lb = self.mem_lb_client.create_loadbalancer(
admin_state_up=True,
description=lb_description,
# TODO(johnsom) Fix test to use a real flavor
# flavor=lb_flavor,
provider=CONF.load_balancer.provider,
name=lb_name,
# TODO(johnsom) Add QoS
# vip_qos_policy_id=lb_qos_policy_id)
vip_network_id=self.lb_member_vip_net[const.ID])
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb1 = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID],
const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
lb_name = data_utils.rand_name("lb_member_lb1-list")
lb_description = 'A'
lb = self.mem_lb_client.create_loadbalancer(
admin_state_up=True,
description=lb_description,
name=lb_name,
vip_network_id=self.lb_member_vip_net[const.ID])
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb2 = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID],
const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
lb_name = data_utils.rand_name("lb_member_lb3-list")
lb_description = 'C'
lb = self.mem_lb_client.create_loadbalancer(
admin_state_up=False,
description=lb_description,
name=lb_name,
vip_network_id=self.lb_member_vip_net[const.ID])
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb3 = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID],
const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
# Test that a different user cannot list load balancers
if not CONF.load_balancer.RBAC_test_type == const.NONE:
member2_client = self.os_roles_lb_member2.loadbalancer_client
primary = member2_client.list_loadbalancers()
self.assertEqual(0, len(primary))
# Test that a user without the lb member role cannot list load
# balancers
if CONF.load_balancer.RBAC_test_type == const.ADVANCED:
self.assertRaises(
exceptions.Forbidden,
self.os_primary.loadbalancer_client.list_loadbalancers)
# Check the default sort order, created_at
lbs = self.mem_lb_client.list_loadbalancers()
lbs = self._filter_lbs_by_id(lbs, pretest_lb_ids)
self.assertEqual(lb1[const.DESCRIPTION], lbs[0][const.DESCRIPTION])
self.assertEqual(lb2[const.DESCRIPTION], lbs[1][const.DESCRIPTION])
self.assertEqual(lb3[const.DESCRIPTION], lbs[2][const.DESCRIPTION])
# Test sort descending by description
lbs = self.mem_lb_client.list_loadbalancers(
query_params='{sort}={descr}:{desc}'.format(
sort=const.SORT, descr=const.DESCRIPTION, desc=const.DESC))
lbs = self._filter_lbs_by_id(lbs, pretest_lb_ids)
self.assertEqual(lb1[const.DESCRIPTION], lbs[1][const.DESCRIPTION])
self.assertEqual(lb2[const.DESCRIPTION], lbs[2][const.DESCRIPTION])
self.assertEqual(lb3[const.DESCRIPTION], lbs[0][const.DESCRIPTION])
# Test sort ascending by description
lbs = self.mem_lb_client.list_loadbalancers(
query_params='{sort}={descr}:{asc}'.format(sort=const.SORT,
descr=const.DESCRIPTION,
asc=const.ASC))
lbs = self._filter_lbs_by_id(lbs, pretest_lb_ids)
self.assertEqual(lb1[const.DESCRIPTION], lbs[1][const.DESCRIPTION])
self.assertEqual(lb2[const.DESCRIPTION], lbs[0][const.DESCRIPTION])
self.assertEqual(lb3[const.DESCRIPTION], lbs[2][const.DESCRIPTION])
# Determine indexes of pretest LBs in default sort
pretest_lb_indexes = []
lbs = self.mem_lb_client.list_loadbalancers()
for i, lb in enumerate(lbs):
if lb['id'] in pretest_lb_ids:
pretest_lb_indexes.append(i)
# Test fields
for field in const.SHOW_LOAD_BALANCER_RESPONSE_FIELDS:
lbs = self.mem_lb_client.list_loadbalancers(
query_params='{fields}={field}'.format(fields=const.FIELDS,
field=field))
lbs = self._filter_lbs_by_index(lbs, pretest_lb_indexes)
self.assertEqual(1, len(lbs[0]))
self.assertEqual(lb1[field], lbs[0][field])
self.assertEqual(lb2[field], lbs[1][field])
self.assertEqual(lb3[field], lbs[2][field])
# Test multiple fields at the same time
lbs = self.mem_lb_client.list_loadbalancers(
query_params='{fields}={admin}&{fields}={created}'.format(
fields=const.FIELDS, admin=const.ADMIN_STATE_UP,
created=const.CREATED_AT))
lbs = self._filter_lbs_by_index(lbs, pretest_lb_indexes)
self.assertEqual(2, len(lbs[0]))
self.assertTrue(lbs[0][const.ADMIN_STATE_UP])
parser.parse(lbs[0][const.CREATED_AT])
self.assertTrue(lbs[1][const.ADMIN_STATE_UP])
parser.parse(lbs[1][const.CREATED_AT])
self.assertFalse(lbs[2][const.ADMIN_STATE_UP])
parser.parse(lbs[2][const.CREATED_AT])
# Test filtering
lbs = self.mem_lb_client.list_loadbalancers(
query_params='{desc}={lb_desc}'.format(
desc=const.DESCRIPTION, lb_desc=lb2[const.DESCRIPTION]))
self.assertEqual(1, len(lbs))
self.assertEqual(lb2[const.DESCRIPTION], lbs[0][const.DESCRIPTION])
# Test combined params
lbs = self.mem_lb_client.list_loadbalancers(
query_params='{admin}={true}&{fields}={descr}&{fields}={id}&'
'{sort}={descr}:{desc}'.format(
admin=const.ADMIN_STATE_UP,
true=const.ADMIN_STATE_UP_TRUE,
fields=const.FIELDS, descr=const.DESCRIPTION,
id=const.ID, sort=const.SORT, desc=const.DESC))
lbs = self._filter_lbs_by_id(lbs, pretest_lb_ids)
# Should get two load balancers
self.assertEqual(2, len(lbs))
# Load balancers should have two fields
self.assertEqual(2, len(lbs[0]))
# Should be in descending order
self.assertEqual(lb2[const.DESCRIPTION], lbs[1][const.DESCRIPTION])
self.assertEqual(lb1[const.DESCRIPTION], lbs[0][const.DESCRIPTION])
# Attempt to clean up so that one full test run doesn't start 10+
# amps before the cleanup phase fires
created_lb_ids = lb1[const.ID], lb2[const.ID], lb3[const.ID]
for lb_id in created_lb_ids:
try:
self.mem_lb_client.delete_loadbalancer(lb_id)
except Exception:
pass
for lb_id in created_lb_ids:
try:
waiters.wait_for_deleted_status_or_not_found(
self.mem_lb_client.show_loadbalancer, lb_id,
const.PROVISIONING_STATUS,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
except Exception:
pass
@decorators.idempotent_id('826ae612-8717-4c64-a8a7-cb9570a85870')
def test_load_balancer_show(self):
"""Tests load balancer show API.
* Create a fully populated load balancer.
* Show load balancer details.
* Validate the show reflects the requested values.
* Validates that other accounts cannot see the load balancer.
"""
lb_name = data_utils.rand_name("lb_member_lb1-show")
lb_description = data_utils.arbitrary_string(size=255)
lb_kwargs = {const.ADMIN_STATE_UP: False,
const.DESCRIPTION: lb_description,
# TODO(johnsom) Fix test to use a real flavor
# flavor=lb_flavor,
# TODO(johnsom) Add QoS
# vip_qos_policy_id=lb_qos_policy_id)
const.PROVIDER: CONF.load_balancer.provider,
const.NAME: lb_name}
self._setup_lb_network_kwargs(lb_kwargs, 4)
lb = self.mem_lb_client.create_loadbalancer(**lb_kwargs)
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
self.assertFalse(lb[const.ADMIN_STATE_UP])
parser.parse(lb[const.CREATED_AT])
parser.parse(lb[const.UPDATED_AT])
self.assertEqual(lb_description, lb[const.DESCRIPTION])
UUID(lb[const.ID])
self.assertEqual(lb_name, lb[const.NAME])
self.assertEqual(const.OFFLINE, lb[const.OPERATING_STATUS])
self.assertEqual(self.os_roles_lb_member.credentials.project_id,
lb[const.PROJECT_ID])
self.assertEqual(CONF.load_balancer.provider, lb[const.PROVIDER])
self.assertEqual(self.lb_member_vip_net[const.ID],
lb[const.VIP_NETWORK_ID])
self.assertIsNotNone(lb[const.VIP_PORT_ID])
if lb_kwargs[const.VIP_SUBNET_ID]:
self.assertEqual(lb_kwargs[const.VIP_ADDRESS],
lb[const.VIP_ADDRESS])
self.assertEqual(lb_kwargs[const.VIP_SUBNET_ID],
lb[const.VIP_SUBNET_ID])
# Test that a user with lb_admin role can see the load balanacer
if CONF.load_balancer.RBAC_test_type == const.ADVANCED:
lb_client = self.os_roles_lb_admin.loadbalancer_client
lb_adm = lb_client.show_loadbalancer(lb[const.ID])
self.assertEqual(lb_name, lb_adm[const.NAME])
# Test that a user with cloud admin role can see the load balanacer
if not CONF.load_balancer.RBAC_test_type == const.NONE:
adm = self.os_admin.loadbalancer_client.show_loadbalancer(
lb[const.ID])
self.assertEqual(lb_name, adm[const.NAME])
# Test that a different user, with load balancer member role, cannot
# see this load balancer
if not CONF.load_balancer.RBAC_test_type == const.NONE:
member2_client = self.os_roles_lb_member2.loadbalancer_client
self.assertRaises(exceptions.Forbidden,
member2_client.show_loadbalancer,
lb[const.ID])
# Test that a user, without the load balancer member role, cannot
# show load balancers
if CONF.load_balancer.RBAC_test_type == const.ADVANCED:
self.assertRaises(
exceptions.Forbidden,
self.os_primary.loadbalancer_client.show_loadbalancer,
lb[const.ID])
# Attempt to clean up so that one full test run doesn't start 10+
# amps before the cleanup phase fires
try:
self.mem_lb_client.delete_loadbalancer(lb[const.ID])
waiters.wait_for_deleted_status_or_not_found(
self.mem_lb_client.show_loadbalancer, lb[const.ID],
const.PROVISIONING_STATUS,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
except Exception:
pass
@decorators.idempotent_id('b75a4d15-49d2-4149-a745-635eed1aacc3')
def test_load_balancer_update(self):
"""Tests load balancer show API and field filtering.
* Create a fully populated load balancer.
* Show load balancer details.
* Validate the show reflects the initial values.
* Validates that other accounts cannot update the load balancer.
* Update the load balancer details.
* Show load balancer details.
* Validate the show reflects the initial values.
"""
lb_name = data_utils.rand_name("lb_member_lb1-update")
lb_description = data_utils.arbitrary_string(size=255)
lb_kwargs = {const.ADMIN_STATE_UP: False,
const.DESCRIPTION: lb_description,
const.PROVIDER: CONF.load_balancer.provider,
# TODO(johnsom) Fix test to use a real flavor
# flavor=lb_flavor,
# TODO(johnsom) Add QoS
# vip_qos_policy_id=lb_qos_policy_id)
const.NAME: lb_name}
self._setup_lb_network_kwargs(lb_kwargs, 4)
lb = self.mem_lb_client.create_loadbalancer(**lb_kwargs)
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
self.assertFalse(lb[const.ADMIN_STATE_UP])
parser.parse(lb[const.CREATED_AT])
parser.parse(lb[const.UPDATED_AT])
self.assertEqual(lb_description, lb[const.DESCRIPTION])
UUID(lb[const.ID])
self.assertEqual(lb_name, lb[const.NAME])
self.assertEqual(const.OFFLINE, lb[const.OPERATING_STATUS])
self.assertEqual(self.os_roles_lb_member.credentials.project_id,
lb[const.PROJECT_ID])
self.assertEqual(CONF.load_balancer.provider, lb[const.PROVIDER])
self.assertEqual(self.lb_member_vip_net[const.ID],
lb[const.VIP_NETWORK_ID])
self.assertIsNotNone(lb[const.VIP_PORT_ID])
if lb_kwargs[const.VIP_SUBNET_ID]:
self.assertEqual(lb_kwargs[const.VIP_ADDRESS],
lb[const.VIP_ADDRESS])
self.assertEqual(lb_kwargs[const.VIP_SUBNET_ID],
lb[const.VIP_SUBNET_ID])
new_name = data_utils.rand_name("lb_member_lb1-update")
new_description = data_utils.arbitrary_string(size=255,
base_text='new')
# Test that a user, without the load balancer member role, cannot
# use this command
if CONF.load_balancer.RBAC_test_type == const.ADVANCED:
self.assertRaises(
exceptions.Forbidden,
self.os_primary.loadbalancer_client.update_loadbalancer,
lb[const.ID], admin_state_up=True)
# Assert we didn't go into PENDING_*
lb_check = self.mem_lb_client.show_loadbalancer(lb[const.ID])
self.assertEqual(const.ACTIVE, lb_check[const.PROVISIONING_STATUS])
self.assertFalse(lb_check[const.ADMIN_STATE_UP])
# Test that a user, without the load balancer member role, cannot
# update this load balancer
if not CONF.load_balancer.RBAC_test_type == const.NONE:
member2_client = self.os_roles_lb_member2.loadbalancer_client
self.assertRaises(exceptions.Forbidden,
member2_client.update_loadbalancer,
lb[const.ID], admin_state_up=True)
# Assert we didn't go into PENDING_*
lb_check = self.mem_lb_client.show_loadbalancer(lb[const.ID])
self.assertEqual(const.ACTIVE, lb_check[const.PROVISIONING_STATUS])
self.assertFalse(lb_check[const.ADMIN_STATE_UP])
lb = self.mem_lb_client.update_loadbalancer(
lb[const.ID],
admin_state_up=True,
description=new_description,
# TODO(johnsom) Add QoS
# vip_qos_policy_id=lb_qos_policy_id)
name=new_name)
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
self.assertTrue(lb[const.ADMIN_STATE_UP])
self.assertEqual(new_description, lb[const.DESCRIPTION])
self.assertEqual(new_name, lb[const.NAME])
# TODO(johnsom) Add QoS
# Attempt to clean up so that one full test run doesn't start 10+
# amps before the cleanup phase fires
try:
self.mem_lb_client.delete_loadbalancer(lb[const.ID])
waiters.wait_for_deleted_status_or_not_found(
self.mem_lb_client.show_loadbalancer, lb[const.ID],
const.PROVISIONING_STATUS,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
except Exception:
pass
@decorators.idempotent_id('105afcba-4dd6-46d6-8fa4-bd7330aa1259')
def test_load_balancer_show_stats(self):
"""Tests load balancer show statistics API.
* Create a load balancer.
* Validates that other accounts cannot see the stats for the
* load balancer.
* Show load balancer statistics.
* Validate the show reflects the expected values.
"""
lb_name = data_utils.rand_name("lb_member_lb1-show_stats")
lb = self.mem_lb_client.create_loadbalancer(
name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
# Test that a user, without the load balancer member role, cannot
# use this command
if CONF.load_balancer.RBAC_test_type == const.ADVANCED:
self.assertRaises(
exceptions.Forbidden,
self.os_primary.loadbalancer_client.get_loadbalancer_stats,
lb[const.ID])
# Test that a different user, with the load balancer role, cannot see
# the load balancer stats
if not CONF.load_balancer.RBAC_test_type == const.NONE:
member2_client = self.os_roles_lb_member2.loadbalancer_client
self.assertRaises(exceptions.Forbidden,
member2_client.get_loadbalancer_stats,
lb[const.ID])
stats = self.mem_lb_client.get_loadbalancer_stats(lb[const.ID])
self.assertEqual(5, len(stats))
self.assertEqual(0, stats[const.ACTIVE_CONNECTIONS])
self.assertEqual(0, stats[const.BYTES_IN])
self.assertEqual(0, stats[const.BYTES_OUT])
self.assertEqual(0, stats[const.REQUEST_ERRORS])
self.assertEqual(0, stats[const.TOTAL_CONNECTIONS])
# Attempt to clean up so that one full test run doesn't start 10+
# amps before the cleanup phase fires
try:
self.mem_lb_client.delete_loadbalancer(lb[const.ID])
waiters.wait_for_deleted_status_or_not_found(
self.mem_lb_client.show_loadbalancer, lb[const.ID],
const.PROVISIONING_STATUS,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
except Exception:
pass
@decorators.idempotent_id('60acc1b0-fa46-41f8-b526-c81ae2f42c30')
def test_load_balancer_show_status(self):
"""Tests load balancer show status tree API.
* Create a load balancer.
* Validates that other accounts cannot see the status for the
* load balancer.
* Show load balancer status tree.
* Validate the show reflects the expected values.
"""
lb_name = data_utils.rand_name("lb_member_lb1-status")
lb = self.mem_lb_client.create_loadbalancer(
name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
# Test that a user, without the load balancer member role, cannot
# use this method
if CONF.load_balancer.RBAC_test_type == const.ADVANCED:
self.assertRaises(
exceptions.Forbidden,
self.os_primary.loadbalancer_client.get_loadbalancer_status,
lb[const.ID])
# Test that a different user, with load balancer role, cannot see
# the load balancer status
if not CONF.load_balancer.RBAC_test_type == const.NONE:
member2_client = self.os_roles_lb_member2.loadbalancer_client
self.assertRaises(exceptions.Forbidden,
member2_client.get_loadbalancer_status,
lb[const.ID])
status = self.mem_lb_client.get_loadbalancer_status(lb[const.ID])
self.assertEqual(1, len(status))
lb_status = status[const.LOADBALANCER]
self.assertEqual(5, len(lb_status))
self.assertEqual(lb[const.ID], lb_status[const.ID])
self.assertEqual([], lb_status[const.LISTENERS])
self.assertEqual(lb_name, lb_status[const.NAME])
# Operating status is a measured status, so no-op will not go online
if CONF.load_balancer.test_with_noop:
self.assertEqual(const.OFFLINE, lb_status[const.OPERATING_STATUS])
else:
self.assertEqual(const.ONLINE, lb_status[const.OPERATING_STATUS])
self.assertEqual(const.ACTIVE, lb_status[const.PROVISIONING_STATUS])
# Attempt to clean up so that one full test run doesn't start 10+
# amps before the cleanup phase fires
try:
self.mem_lb_client.delete_loadbalancer(lb[const.ID])
waiters.wait_for_deleted_status_or_not_found(
self.mem_lb_client.show_loadbalancer, lb[const.ID],
const.PROVISIONING_STATUS,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
except Exception:
pass
@decorators.idempotent_id('fc2e07a6-9776-4559-90c9-141170d4c397')
def test_load_balancer_failover(self):
"""Tests load balancer failover API.
* Create a load balancer.
* Validates that other accounts cannot failover the load balancer
* Wait for the load balancer to go ACTIVE.
* Failover the load balancer.
* Wait for the load balancer to go ACTIVE.
"""
lb_name = data_utils.rand_name("lb_member_lb1-failover")
lb = self.mem_lb_client.create_loadbalancer(
name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
# Test RBAC not authorized for non-admin role
if not CONF.load_balancer.RBAC_test_type == const.NONE:
self.assertRaises(exceptions.Forbidden,
self.mem_lb_client.failover_loadbalancer,
lb[const.ID])
# Assert we didn't go into PENDING_*
lb = self.mem_lb_client.show_loadbalancer(lb[const.ID])
self.assertEqual(const.ACTIVE, lb[const.PROVISIONING_STATUS])
self.os_roles_lb_admin.loadbalancer_client.failover_loadbalancer(
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
# TODO(johnsom) Assert the amphora ID has changed when amp client
# is available.
# Attempt to clean up so that one full test run doesn't start 10+
# amps before the cleanup phase fires
try:
self.mem_lb_client.delete_loadbalancer(lb[const.ID])
waiters.wait_for_deleted_status_or_not_found(
self.mem_lb_client.show_loadbalancer, lb[const.ID],
const.PROVISIONING_STATUS,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
except Exception:
pass

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2010-2011 OpenStack Foundation
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# 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 oslotest import base
class TestCase(base.BaseTestCase):
"""Test case base class for all unit tests."""

View File

@ -0,0 +1,120 @@
# Copyright 2018 Rackspace US 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.
import testtools
from uuid import UUID
from dateutil import parser
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from octavia_tempest_plugin.common import constants as const
from octavia_tempest_plugin.tests import test_base
from octavia_tempest_plugin.tests import waiters
CONF = config.CONF
class LoadBalancerScenarioTest(test_base.LoadBalancerBaseTest):
@decorators.idempotent_id('a5e2e120-4f7e-4c8b-8aac-cf09cb56711c')
def test_load_balancer_ipv4_CRUD(self):
self._test_load_balancer_CRUD(4)
@decorators.idempotent_id('86ffecc4-dce8-46f9-936e-8a4c6bcf3959')
@testtools.skipUnless(CONF.load_balancer.test_with_ipv6,
'IPv6 testing is disabled')
def test_load_balancer_ipv6_CRUD(self):
self._test_load_balancer_CRUD(6)
def _test_load_balancer_CRUD(self, ip_version):
"""Tests load balancer create, read, update, delete
* Create a fully populated load balancer.
* Show load balancer details.
* Update the load balancer.
* Delete the load balancer.
"""
lb_name = data_utils.rand_name("lb_member_lb1-CRUD")
lb_description = data_utils.arbitrary_string(size=255)
lb_kwargs = {const.ADMIN_STATE_UP: False,
const.DESCRIPTION: lb_description,
const.PROVIDER: CONF.load_balancer.provider,
const.NAME: lb_name}
self._setup_lb_network_kwargs(lb_kwargs, ip_version)
lb = self.mem_lb_client.create_loadbalancer(**lb_kwargs)
self.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
self.mem_lb_client.delete_loadbalancer,
lb[const.ID])
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
self.assertFalse(lb[const.ADMIN_STATE_UP])
parser.parse(lb[const.CREATED_AT])
parser.parse(lb[const.UPDATED_AT])
self.assertEqual(lb_description, lb[const.DESCRIPTION])
UUID(lb[const.ID])
self.assertEqual(lb_name, lb[const.NAME])
self.assertEqual(const.OFFLINE, lb[const.OPERATING_STATUS])
self.assertEqual(self.os_roles_lb_member.credentials.project_id,
lb[const.PROJECT_ID])
self.assertEqual(CONF.load_balancer.provider, lb[const.PROVIDER])
self.assertEqual(self.lb_member_vip_net[const.ID],
lb[const.VIP_NETWORK_ID])
self.assertIsNotNone(lb[const.VIP_PORT_ID])
if lb_kwargs[const.VIP_SUBNET_ID]:
self.assertEqual(lb_kwargs[const.VIP_ADDRESS],
lb[const.VIP_ADDRESS])
self.assertEqual(lb_kwargs[const.VIP_SUBNET_ID],
lb[const.VIP_SUBNET_ID])
# Load balancer update
new_name = data_utils.rand_name("lb_member_lb1-update")
new_description = data_utils.arbitrary_string(size=255,
base_text='new')
lb = self.mem_lb_client.update_loadbalancer(
lb[const.ID],
admin_state_up=True,
description=new_description,
name=new_name)
lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.ACTIVE,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)
self.assertTrue(lb[const.ADMIN_STATE_UP])
self.assertEqual(new_description, lb[const.DESCRIPTION])
self.assertEqual(new_name, lb[const.NAME])
# Load balancer delete
self.mem_lb_client.delete_loadbalancer(lb[const.ID], cascade=True)
waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
lb[const.ID], const.PROVISIONING_STATUS,
const.DELETED,
CONF.load_balancer.lb_build_interval,
CONF.load_balancer.lb_build_timeout)

View File

@ -0,0 +1,683 @@
# Copyright 2018 Rackspace US 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.
import ipaddress
import pkg_resources
import random
import shlex
import six
import string
import subprocess
import tempfile
from oslo_log import log as logging
from oslo_utils import uuidutils
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils.linux import remote_client
from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions
from tempest import test
from octavia_tempest_plugin import clients
from octavia_tempest_plugin.common import constants as const
from octavia_tempest_plugin.tests import validators
from octavia_tempest_plugin.tests import waiters
CONF = config.CONF
LOG = logging.getLogger(__name__)
class LoadBalancerBaseTest(test.BaseTestCase):
"""Base class for load balancer tests."""
# Setup cls.os_roles_lb_member. cls.os_primary, cls.os_roles_lb_member,
# and cls.os_roles_lb_admin credentials.
credentials = ['admin', 'primary',
['lb_member', CONF.load_balancer.member_role],
['lb_member2', CONF.load_balancer.member_role],
['lb_admin', CONF.load_balancer.admin_role]]
client_manager = clients.ManagerV2
@classmethod
def skip_checks(cls):
"""Check if we should skip all of the children tests."""
super(LoadBalancerBaseTest, cls).skip_checks()
service_list = {
'load_balancer': CONF.service_available.load_balancer,
}
live_service_list = {
'compute': CONF.service_available.nova,
'image': CONF.service_available.glance,
'neutron': CONF.service_available.neutron
}
if not CONF.load_balancer.test_with_noop:
service_list.update(live_service_list)
for service, available in service_list.items():
if not available:
skip_msg = ("{0} skipped as {1} serivce is not "
"available.".format(cls.__name__, service))
raise cls.skipException(skip_msg)
# We must be able to reach our VIP and instances
if not (CONF.network.project_networks_reachable
or CONF.network.public_network_id):
msg = ('Either project_networks_reachable must be "true", or '
'public_network_id must be defined.')
raise cls.skipException(msg)
@classmethod
def setup_credentials(cls):
"""Setup test credentials and network resources."""
# Do not auto create network resources
cls.set_network_resources()
super(LoadBalancerBaseTest, cls).setup_credentials()
@classmethod
def setup_clients(cls):
"""Setup client aliases."""
super(LoadBalancerBaseTest, cls).setup_clients()
cls.lb_mem_float_ip_client = cls.os_roles_lb_member.floating_ips_client
cls.lb_mem_keypairs_client = cls.os_roles_lb_member.keypairs_client
cls.lb_mem_net_client = cls.os_roles_lb_member.networks_client
cls.lb_mem_ports_client = cls.os_roles_lb_member.ports_client
cls.lb_mem_routers_client = cls.os_roles_lb_member.routers_client
cls.lb_mem_SG_client = cls.os_roles_lb_member.security_groups_client
cls.lb_mem_SGr_client = (
cls.os_roles_lb_member.security_group_rules_client)
cls.lb_mem_servers_client = cls.os_roles_lb_member.servers_client
cls.lb_mem_subnet_client = cls.os_roles_lb_member.subnets_client
cls.mem_lb_client = cls.os_roles_lb_member.loadbalancer_client
@classmethod
def resource_setup(cls):
"""Setup resources needed by the tests."""
super(LoadBalancerBaseTest, cls).resource_setup()
conf_lb = CONF.load_balancer
if conf_lb.test_subnet_override and not conf_lb.test_network_override:
raise exceptions.InvalidConfiguration(
"Configuration value test_network_override must be "
"specified if test_subnet_override is used.")
show_subnet = cls.lb_mem_subnet_client.show_subnet
if CONF.load_balancer.test_with_noop:
cls.lb_member_vip_net = {'id': uuidutils.generate_uuid()}
cls.lb_member_vip_subnet = {'id': uuidutils.generate_uuid()}
cls.lb_member_1_net = {'id': uuidutils.generate_uuid()}
cls.lb_member_1_subnet = {'id': uuidutils.generate_uuid()}
cls.lb_member_2_net = {'id': uuidutils.generate_uuid()}
cls.lb_member_2_subnet = {'id': uuidutils.generate_uuid()}
if CONF.load_balancer.test_with_ipv6:
cls.lb_member_vip_ipv6_subnet = {'id':
uuidutils.generate_uuid()}
cls.lb_member_1_ipv6_subnet = {'id': uuidutils.generate_uuid()}
cls.lb_member_2_ipv6_subnet = {'id': uuidutils.generate_uuid()}
return
elif CONF.load_balancer.test_network_override:
if conf_lb.test_subnet_override:
override_subnet = show_subnet(conf_lb.test_subnet_override)
else:
override_subnet = None
show_net = cls.lb_mem_net_client.show_network
override_network = show_net(conf_lb.test_network_override)
override_network = override_network.get('network')
cls.lb_member_vip_net = override_network
cls.lb_member_vip_subnet = override_subnet
cls.lb_member_1_net = override_network
cls.lb_member_1_subnet = override_subnet
cls.lb_member_2_net = override_network
cls.lb_member_2_subnet = override_subnet
if (CONF.load_balancer.test_with_ipv6 and
conf_lb.test_IPv6_subnet_override):
override_ipv6_subnet = show_subnet(
conf_lb.test_IPv6_subnet_override)
cls.lb_member_vip_ipv6_subnet = override_ipv6_subnet
cls.lb_member_1_ipv6_subnet = override_ipv6_subnet
cls.lb_member_2_ipv6_subnet = override_ipv6_subnet
else:
cls.lb_member_vip_ipv6_subnet = None
cls.lb_member_1_ipv6_subnet = None
cls.lb_member_2_ipv6_subnet = None
else:
cls._create_networks()
LOG.debug('Octavia Setup: lb_member_vip_net = {}'.format(
cls.lb_member_vip_net[const.ID]))
if cls.lb_member_vip_subnet:
LOG.debug('Octavia Setup: lb_member_vip_subnet = {}'.format(
cls.lb_member_vip_subnet[const.ID]))
LOG.debug('Octavia Setup: lb_member_1_net = {}'.format(
cls.lb_member_1_net[const.ID]))
if cls.lb_member_1_subnet:
LOG.debug('Octavia Setup: lb_member_1_subnet = {}'.format(
cls.lb_member_1_subnet[const.ID]))
LOG.debug('Octavia Setup: lb_member_2_net = {}'.format(
cls.lb_member_2_net[const.ID]))
if cls.lb_member_2_subnet:
LOG.debug('Octavia Setup: lb_member_2_subnet = {}'.format(
cls.lb_member_2_subnet[const.ID]))
if cls.lb_member_vip_ipv6_subnet:
LOG.debug('Octavia Setup: lb_member_vip_ipv6_subnet = {}'.format(
cls.lb_member_vip_ipv6_subnet[const.ID]))
if cls.lb_member_1_ipv6_subnet:
LOG.debug('Octavia Setup: lb_member_1_ipv6_subnet = {}'.format(
cls.lb_member_1_ipv6_subnet[const.ID]))
if cls.lb_member_2_ipv6_subnet:
LOG.debug('Octavia Setup: lb_member_2_ipv6_subnet = {}'.format(
cls.lb_member_2_ipv6_subnet[const.ID]))
# If validation is disabled in this cloud, we won't be able to
# start the webservers, so don't even boot them.
if not CONF.validation.run_validation:
return
# Create a keypair for the webservers
keypair_name = data_utils.rand_name('lb_member_keypair')
result = cls.lb_mem_keypairs_client.create_keypair(
name=keypair_name)
cls.lb_member_keypair = result['keypair']
LOG.info('lb_member_keypair: {}'.format(cls.lb_member_keypair))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_keypairs_client.delete_keypair,
cls.lb_mem_keypairs_client.show_keypair,
keypair_name)
if (CONF.load_balancer.enable_security_groups and
CONF.network_feature_enabled.port_security):
# Set up the security group for the webservers
SG_name = data_utils.rand_name('lb_member_SG')
cls.lb_member_sec_group = (
cls.lb_mem_SG_client.create_security_group(
name=SG_name)['security_group'])
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_SG_client.delete_security_group,
cls.lb_mem_SG_client.show_security_group,
cls.lb_member_sec_group['id'])
# Create a security group rule to allow 80-81 (test webservers)
SGr = cls.lb_mem_SGr_client.create_security_group_rule(
direction='ingress',
security_group_id=cls.lb_member_sec_group['id'],
protocol='tcp',
ethertype='IPv4',
port_range_min=80,
port_range_max=81)['security_group_rule']
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_SGr_client.delete_security_group_rule,
cls.lb_mem_SGr_client.show_security_group_rule,
SGr['id'])
# Create a security group rule to allow 22 (ssh)
SGr = cls.lb_mem_SGr_client.create_security_group_rule(
direction='ingress',
security_group_id=cls.lb_member_sec_group['id'],
protocol='tcp',
ethertype='IPv4',
port_range_min=22,
port_range_max=22)['security_group_rule']
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_SGr_client.delete_security_group_rule,
cls.lb_mem_SGr_client.show_security_group_rule,
SGr['id'])
if CONF.load_balancer.test_with_ipv6:
# Create a security group rule to allow 80-81 (test webservers)
SGr = cls.lb_mem_SGr_client.create_security_group_rule(
direction='ingress',
security_group_id=cls.lb_member_sec_group['id'],
protocol='tcp',
ethertype='IPv6',
port_range_min=80,
port_range_max=81)['security_group_rule']
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_SGr_client.delete_security_group_rule,
cls.lb_mem_SGr_client.show_security_group_rule,
SGr['id'])
# Create a security group rule to allow 22 (ssh)
SGr = cls.lb_mem_SGr_client.create_security_group_rule(
direction='ingress',
security_group_id=cls.lb_member_sec_group['id'],
protocol='tcp',
ethertype='IPv6',
port_range_min=22,
port_range_max=22)['security_group_rule']
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_SGr_client.delete_security_group_rule,
cls.lb_mem_SGr_client.show_security_group_rule,
SGr['id'])
LOG.info('lb_member_sec_group: {}'.format(cls.lb_member_sec_group))
# Create webserver 1 instance
server_details = cls._create_webserver('lb_member_webserver1',
cls.lb_member_1_net)
cls.lb_member_webserver1 = server_details['server']
cls.webserver1_ip = server_details.get('ipv4_address')
cls.webserver1_ipv6 = server_details.get('ipv6_address')
cls.webserver1_public_ip = server_details['public_ipv4_address']
LOG.debug('Octavia Setup: lb_member_webserver1 = {}'.format(
cls.lb_member_webserver1[const.ID]))
LOG.debug('Octavia Setup: webserver1_ip = {}'.format(
cls.webserver1_ip))
LOG.debug('Octavia Setup: webserver1_ipv6 = {}'.format(
cls.webserver1_ipv6))
LOG.debug('Octavia Setup: webserver1_public_ip = {}'.format(
cls.webserver1_public_ip))
cls._install_start_webserver(cls.webserver1_public_ip,
cls.lb_member_keypair['private_key'], 1)
# Validate webserver 1
cls._validate_webserver(cls.webserver1_public_ip, 1)
# Create webserver 2 instance
server_details = cls._create_webserver('lb_member_webserver2',
cls.lb_member_2_net)
cls.lb_member_webserver2 = server_details['server']
cls.webserver2_ip = server_details.get('ipv4_address')
cls.webserver2_ipv6 = server_details.get('ipv6_address')
cls.webserver2_public_ip = server_details['public_ipv4_address']
LOG.debug('Octavia Setup: lb_member_webserver2 = {}'.format(
cls.lb_member_webserver2[const.ID]))
LOG.debug('Octavia Setup: webserver2_ip = {}'.format(
cls.webserver2_ip))
LOG.debug('Octavia Setup: webserver2_ipv6 = {}'.format(
cls.webserver2_ipv6))
LOG.debug('Octavia Setup: webserver2_public_ip = {}'.format(
cls.webserver2_public_ip))
cls._install_start_webserver(cls.webserver2_public_ip,
cls.lb_member_keypair['private_key'], 5)
# Validate webserver 2
cls._validate_webserver(cls.webserver2_public_ip, 5)
@classmethod
def _install_start_webserver(cls, ip_address, ssh_key, start_id):
local_file = pkg_resources.resource_filename(
'octavia_tempest_plugin.contrib.httpd', 'httpd.bin')
dest_file = '/dev/shm/httpd.bin'
linux_client = remote_client.RemoteClient(
ip_address, CONF.validation.image_ssh_user, pkey=ssh_key)
linux_client.validate_authentication()
with tempfile.NamedTemporaryFile() as key:
key.write(ssh_key.encode('utf-8'))
key.flush()
cmd = ("scp -v -o UserKnownHostsFile=/dev/null "
"-o StrictHostKeyChecking=no "
"-o ConnectTimeout={0} -o ConnectionAttempts={1} "
"-i {2} {3} {4}@{5}:{6}").format(
CONF.load_balancer.scp_connection_timeout,
CONF.load_balancer.scp_connection_attempts,
key.name, local_file, CONF.validation.image_ssh_user,
ip_address, dest_file)
args = shlex.split(cmd)
subprocess_args = {'stdout': subprocess.PIPE,
'stderr': subprocess.STDOUT,
'cwd': None}
proc = subprocess.Popen(args, **subprocess_args)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
raise exceptions.CommandFailed(proc.returncode, cmd,
stdout, stderr)
linux_client.exec_command('sudo screen -d -m {0} -port 80 '
'-id {1}'.format(dest_file, start_id))
linux_client.exec_command('sudo screen -d -m {0} -port 81 '
'-id {1}'.format(dest_file, start_id + 1))
@classmethod
def _create_networks(cls):
"""Creates networks, subnets, and routers used in tests.
The following are expected to be defined and available to the tests:
cls.lb_member_vip_net
cls.lb_member_vip_subnet
cls.lb_member_vip_ipv6_subnet (optional)
cls.lb_member_1_net
cls.lb_member_1_subnet
cls.lb_member_1_ipv6_subnet (optional)
cls.lb_member_2_net
cls.lb_member_2_subnet
cls.lb_member_2_ipv6_subnet (optional)
"""
# Create tenant VIP network
network_kwargs = {
'name': data_utils.rand_name("lb_member_vip_network")}
if CONF.network_feature_enabled.port_security:
# Note: Allowed Address Pairs requires port security
network_kwargs['port_security_enabled'] = True
result = cls.lb_mem_net_client.create_network(**network_kwargs)
cls.lb_member_vip_net = result['network']
LOG.info('lb_member_vip_net: {}'.format(cls.lb_member_vip_net))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_net_client.delete_network,
cls.lb_mem_net_client.show_network,
cls.lb_member_vip_net['id'])
# Create tenant VIP subnet
subnet_kwargs = {
'name': data_utils.rand_name("lb_member_vip_subnet"),
'network_id': cls.lb_member_vip_net['id'],
'cidr': CONF.load_balancer.vip_subnet_cidr,
'ip_version': 4}
result = cls.lb_mem_subnet_client.create_subnet(**subnet_kwargs)
cls.lb_member_vip_subnet = result['subnet']
LOG.info('lb_member_vip_subnet: {}'.format(cls.lb_member_vip_subnet))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_subnet_client.delete_subnet,
cls.lb_mem_subnet_client.show_subnet,
cls.lb_member_vip_subnet['id'])
# Create tenant VIP IPv6 subnet
if CONF.load_balancer.test_with_ipv6:
subnet_kwargs = {
'name': data_utils.rand_name("lb_member_vip_ipv6_subnet"),
'network_id': cls.lb_member_vip_net['id'],
'cidr': CONF.load_balancer.vip_ipv6_subnet_cidr,
'ip_version': 6}
result = cls.lb_mem_subnet_client.create_subnet(**subnet_kwargs)
cls.lb_member_vip_ipv6_subnet = result['subnet']
LOG.info('lb_member_vip_ipv6_subnet: {}'.format(
cls.lb_member_vip_ipv6_subnet))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_subnet_client.delete_subnet,
cls.lb_mem_subnet_client.show_subnet,
cls.lb_member_vip_ipv6_subnet['id'])
# Create tenant member 1 network
network_kwargs = {
'name': data_utils.rand_name("lb_member_1_network")}
if CONF.network_feature_enabled.port_security:
if CONF.load_balancer.enable_security_groups:
network_kwargs['port_security_enabled'] = True
else:
network_kwargs['port_security_enabled'] = False
result = cls.lb_mem_net_client.create_network(**network_kwargs)
cls.lb_member_1_net = result['network']
LOG.info('lb_member_1_net: {}'.format(cls.lb_member_1_net))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_net_client.delete_network,
cls.lb_mem_net_client.show_network,
cls.lb_member_1_net['id'])
# Create tenant member 1 subnet
subnet_kwargs = {
'name': data_utils.rand_name("lb_member_1_subnet"),
'network_id': cls.lb_member_1_net['id'],
'cidr': CONF.load_balancer.member_1_ipv4_subnet_cidr,
'ip_version': 4}
result = cls.lb_mem_subnet_client.create_subnet(**subnet_kwargs)
cls.lb_member_1_subnet = result['subnet']
LOG.info('lb_member_1_subnet: {}'.format(cls.lb_member_1_subnet))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_subnet_client.delete_subnet,
cls.lb_mem_subnet_client.show_subnet,
cls.lb_member_1_subnet['id'])
# Create tenant member 1 ipv6 subnet
if CONF.load_balancer.test_with_ipv6:
subnet_kwargs = {
'name': data_utils.rand_name("lb_member_1_ipv6_subnet"),
'network_id': cls.lb_member_1_net['id'],
'cidr': CONF.load_balancer.member_1_ipv6_subnet_cidr,
'ip_version': 6}
result = cls.lb_mem_subnet_client.create_subnet(**subnet_kwargs)
cls.lb_member_1_ipv6_subnet = result['subnet']
LOG.info('lb_member_1_ipv6_subnet: {}'.format(
cls.lb_member_1_ipv6_subnet))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_subnet_client.delete_subnet,
cls.lb_mem_subnet_client.show_subnet,
cls.lb_member_1_ipv6_subnet['id'])
# Create tenant member 2 network
network_kwargs = {
'name': data_utils.rand_name("lb_member_2_network")}
if CONF.network_feature_enabled.port_security:
if CONF.load_balancer.enable_security_groups:
network_kwargs['port_security_enabled'] = True
else:
network_kwargs['port_security_enabled'] = False
result = cls.lb_mem_net_client.create_network(**network_kwargs)
cls.lb_member_2_net = result['network']
LOG.info('lb_member_2_net: {}'.format(cls.lb_member_2_net))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_net_client.delete_network,
cls.lb_mem_net_client.show_network,
cls.lb_member_2_net['id'])
# Create tenant member 2 subnet
subnet_kwargs = {
'name': data_utils.rand_name("lb_member_2_subnet"),
'network_id': cls.lb_member_2_net['id'],
'cidr': CONF.load_balancer.member_2_ipv4_subnet_cidr,
'ip_version': 4}
result = cls.lb_mem_subnet_client.create_subnet(**subnet_kwargs)
cls.lb_member_2_subnet = result['subnet']
LOG.info('lb_member_2_subnet: {}'.format(cls.lb_member_2_subnet))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_subnet_client.delete_subnet,
cls.lb_mem_subnet_client.show_subnet,
cls.lb_member_2_subnet['id'])
# Create tenant member 2 ipv6 subnet
if CONF.load_balancer.test_with_ipv6:
subnet_kwargs = {
'name': data_utils.rand_name("lb_member_2_ipv6_subnet"),
'network_id': cls.lb_member_2_net['id'],
'cidr': CONF.load_balancer.member_2_ipv6_subnet_cidr,
'ip_version': 6}
result = cls.lb_mem_subnet_client.create_subnet(**subnet_kwargs)
cls.lb_member_2_ipv6_subnet = result['subnet']
LOG.info('lb_member_2_ipv6_subnet: {}'.format(
cls.lb_member_2_ipv6_subnet))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_subnet_client.delete_subnet,
cls.lb_mem_subnet_client.show_subnet,
cls.lb_member_2_ipv6_subnet['id'])
# Create a router for the subnets (required for the floating IP)
router_name = data_utils.rand_name("lb_member_router")
result = cls.lb_mem_routers_client.create_router(
name=router_name, admin_state_up=True,
external_gateway_info=dict(
network_id=CONF.network.public_network_id))
cls.lb_member_router = result['router']
LOG.info('lb_member_router: {}'.format(cls.lb_member_router))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_routers_client.delete_router,
cls.lb_mem_routers_client.show_router,
cls.lb_member_router['id'])
# Add VIP subnet to router
cls.lb_mem_routers_client.add_router_interface(
cls.lb_member_router['id'],
subnet_id=cls.lb_member_vip_subnet['id'])
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_routers_client.remove_router_interface,
cls.lb_mem_routers_client.remove_router_interface,
cls.lb_member_router['id'],
subnet_id=cls.lb_member_vip_subnet['id'])
# Add member subnet 1 to router
cls.lb_mem_routers_client.add_router_interface(
cls.lb_member_router['id'],
subnet_id=cls.lb_member_1_subnet['id'])
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
test_utils.call_and_ignore_notfound_exc,
cls.lb_mem_routers_client.remove_router_interface,
cls.lb_mem_routers_client.remove_router_interface,
cls.lb_member_router['id'], subnet_id=cls.lb_member_1_subnet['id'])
# Add member subnet 2 to router
cls.lb_mem_routers_client.add_router_interface(
cls.lb_member_router['id'],
subnet_id=cls.lb_member_2_subnet['id'])
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_routers_client.remove_router_interface,
cls.lb_mem_routers_client.remove_router_interface,
cls.lb_member_router['id'], subnet_id=cls.lb_member_2_subnet['id'])
@classmethod
def _create_webserver(cls, name, network):
"""Creates a webserver with two ports.
webserver_details dictionary contains:
server - The compute server object
ipv4_address - The IPv4 address for the server (optional)
ipv6_address - The IPv6 address for the server (optional)
public_ipv4_address - The publicly accessible IPv4 address for the
server, this may be a floating IP (optional)
:param name: The name of the server to create.
:param network: The network to boot the server on.
:returns: webserver_details dictionary.
"""
server_kwargs = {
'name': data_utils.rand_name(name),
'flavorRef': CONF.compute.flavor_ref,
'imageRef': CONF.compute.image_ref,
'key_name': cls.lb_member_keypair['name']}
if (CONF.load_balancer.enable_security_groups and
CONF.network_feature_enabled.port_security):
server_kwargs['security_groups'] = [
{'name': cls.lb_member_sec_group['name']}]
if not CONF.load_balancer.disable_boot_network:
server_kwargs['networks'] = [{'uuid': network['id']}]
# Replace the name for clouds that have limitations
if CONF.load_balancer.random_server_name_length:
r = random.SystemRandom()
server_kwargs['name'] = "m{}".format("".join(
[r.choice(string.ascii_uppercase + string.digits)
for _ in range(
CONF.load_balancer.random_server_name_length - 1)]
))
if CONF.load_balancer.availability_zone:
server_kwargs['availability_zone'] = (
CONF.load_balancer.availability_zone)
server = cls.lb_mem_servers_client.create_server(
**server_kwargs)['server']
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_servers_client.delete_server,
cls.lb_mem_servers_client.show_server,
server['id'])
server = waiters.wait_for_status(
cls.lb_mem_servers_client.show_server,
server['id'], 'status', 'ACTIVE',
CONF.load_balancer.build_interval,
CONF.load_balancer.build_timeout,
root_tag='server')
webserver_details = {'server': server}
LOG.info('Created server: {}'.format(server))
addresses = server['addresses']
if CONF.load_balancer.disable_boot_network:
instance_network = addresses.values()[0]
else:
instance_network = addresses[network['name']]
for addr in instance_network:
if addr['version'] == 4:
webserver_details['ipv4_address'] = addr['addr']
if addr['version'] == 6:
webserver_details['ipv6_address'] = addr['addr']
if CONF.validation.connect_method == 'floating':
result = cls.lb_mem_ports_client.list_ports(
network_id=network['id'],
mac_address=instance_network[0]['OS-EXT-IPS-MAC:mac_addr'])
port_id = result['ports'][0]['id']
result = cls.lb_mem_float_ip_client.create_floatingip(
floating_network_id=CONF.network.public_network_id,
port_id=port_id)
floating_ip = result['floatingip']
LOG.info('webserver1_floating_ip: {}'.format(floating_ip))
cls.addClassResourceCleanup(
waiters.wait_for_not_found,
cls.lb_mem_float_ip_client.delete_floatingip,
cls.lb_mem_float_ip_client.show_floatingip,
floatingip_id=floating_ip['id'])
webserver_details['public_ipv4_address'] = (
floating_ip['floating_ip_address'])
else:
webserver_details['public_ipv4_address'] = (
instance_network[0]['addr'])
return webserver_details
@classmethod
def _validate_webserver(cls, ip_address, start_id):
URL = 'http://{0}'.format(ip_address)
validators.validate_URL_response(URL, expected_body=str(start_id))
URL = 'http://{0}:81'.format(ip_address)
validators.validate_URL_response(URL, expected_body=str(start_id + 1))
@classmethod
def _setup_lb_network_kwargs(cls, lb_kwargs, ip_version):
if cls.lb_member_vip_subnet:
ip_index = data_utils.rand_int_id(start=10, end=100)
if ip_version == 4:
network = ipaddress.IPv4Network(
six.u(CONF.load_balancer.vip_subnet_cidr))
lb_vip_address = str(network[ip_index])
subnet_id = cls.lb_member_vip_subnet[const.ID]
else:
network = ipaddress.IPv6Network(
six.u(CONF.load_balancer.vip_ipv6_subnet_cidr))
lb_vip_address = str(network[ip_index])
subnet_id = cls.lb_member_vip_ipv6_subnet[const.ID]
lb_kwargs[const.VIP_SUBNET_ID] = subnet_id
lb_kwargs[const.VIP_ADDRESS] = lb_vip_address
if CONF.load_balancer.test_with_noop:
lb_kwargs[const.VIP_NETWORK_ID] = (
cls.lb_member_vip_net[const.ID])
else:
lb_kwargs[const.VIP_NETWORK_ID] = cls.lb_member_vip_net[const.ID]
lb_kwargs[const.VIP_SUBNET_ID] = None

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# 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.
"""
test_octavia_tempest_plugin
----------------------------------
Tests for `octavia_tempest_plugin` module.
"""
from octavia_tempest_plugin.tests import base
class TestOctavia_tempest_plugin(base.TestCase):
def test_something(self):
pass

View File

@ -0,0 +1,87 @@
# Copyright 2017 GoDaddy
# Copyright 2017 Catalyst IT Ltd
# Copyright 2018 Rackspace US 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.
import requests
import time
from oslo_log import log as logging
from tempest import config
from tempest.lib import exceptions
CONF = config.CONF
LOG = logging.getLogger(__name__)
def validate_URL_response(URL, expected_status_code=200,
expected_body=None, HTTPS_verify=True,
client_cert_path=None, CA_certs_path=None,
request_interval=CONF.load_balancer.build_interval,
request_timeout=CONF.load_balancer.build_timeout):
"""Check a URL response (HTTP or HTTPS).
:param URL: The URL to query.
:param expected_status_code: The expected HTTP status code.
:param expected_body: The expected response text, None will not compare.
:param HTTPS_verify: Should we verify the HTTPS server.
:param client_cert_path: Filesystem path to a file with the client private
key and certificate.
:param CA_certs_path: Filesystem path to a file containing CA certificates
to use for HTTPS validation.
:param request_interval: Time, in seconds, to timeout a request.
:param request_timeout: The maximum time, in seconds, to attempt requests.
Failed validation of expected results does not
result in a retry.
:raises InvalidHttpSuccessCode: The expected_status_code did not match.
:raises InvalidHTTPResponseBody: The response body did not match the
expected content.
:raises TimeoutException: The request timed out.
:returns: None
"""
with requests.Session() as session:
session_kwargs = {}
if not HTTPS_verify:
session_kwargs['verify'] = False
if CA_certs_path:
session_kwargs['verify'] = CA_certs_path
if client_cert_path:
session_kwargs['cert'] = client_cert_path
session_kwargs['timeout'] = request_interval
start = time.time()
while time.time() - start < request_timeout:
try:
response = session.get(URL, **session_kwargs)
if response.status_code != expected_status_code:
raise exceptions.InvalidHttpSuccessCode(
'{0} is not the expected code {1}'.format(
response.status_code, expected_status_code))
if expected_body and response.text != expected_body:
details = '{} does not match expected {}'.format(
response.text, expected_body)
raise exceptions.InvalidHTTPResponseBody(
resp_body=details)
return
except requests.exceptions.Timeout:
# Don't sleep as we have already waited the interval.
LOG.info('Request for () timed out. Retrying.'.format(URL))
except (exceptions.InvalidHttpSuccessCode,
exceptions.InvalidHTTPResponseBody,
requests.exceptions.SSLError):
raise
except Exception as e:
LOG.info('Validate URL got exception: {0}. '
'Retrying.'.format(e))
time.sleep(request_interval)
raise exceptions.TimeoutException()

View File

@ -0,0 +1,174 @@
# Copyright 2017 GoDaddy
# Copyright 2018 Rackspace US 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.
import time
from oslo_log import log as logging
from tempest import config
from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions
from octavia_tempest_plugin.common import constants as const
CONF = config.CONF
LOG = logging.getLogger(__name__)
def wait_for_status(show_client, id, status_key, status,
check_interval, check_timeout, root_tag=None):
"""Waits for an object to reach a specific status.
:param show_client: The tempest service client show method.
Ex. cls.os_primary.servers_client.show_server
:param id: The id of the object to query.
:param status_key: The key of the status field in the response.
Ex. provisioning_status
:param status: The status to wait for. Ex. "ACTIVE"
:check_interval: How often to check the status, in seconds.
:check_timeout: The maximum time, in seconds, to check the status.
:root_tag: The root tag on the response to remove, if any.
:raises CommandFailed: Raised if the object goes into ERROR and ERROR was
not the desired status.
:raises TimeoutException: The object did not achieve the status or ERROR in
the check_timeout period.
:returns: The object details from the show client.
"""
start = int(time.time())
LOG.info('Waiting for {name} status to update to {status}'.format(
name=show_client.__name__, status=status))
while True:
response = show_client(id)
if root_tag:
object_details = response[root_tag]
else:
object_details = response
if object_details[status_key] == status:
LOG.info('{name}\'s status updated to {status}.'.format(
name=show_client.__name__, status=status))
return object_details
elif object_details[status_key] == 'ERROR':
message = ('{name} {field} updated to an invalid state of '
'ERROR'.format(name=show_client.__name__,
field=status_key))
caller = test_utils.find_test_caller()
if caller:
message = '({caller}) {message}'.format(caller=caller,
message=message)
raise exceptions.UnexpectedResponseCode(message)
elif int(time.time()) - start >= check_timeout:
message = (
'{name} {field} failed to update to {expected_status} within '
'the required time {timeout}. Current status of {name}: '
'{status}'.format(
name=show_client.__name__,
timeout=check_timeout,
status=object_details[status_key],
expected_status=status,
field=status_key
))
caller = test_utils.find_test_caller()
if caller:
message = '({caller}) {message}'.format(caller=caller,
message=message)
raise exceptions.TimeoutException(message)
time.sleep(check_interval)
def wait_for_not_found(delete_func, show_func, *args, **kwargs):
"""Call the delete function, then wait for it to be 'NotFound'
:param delete_func: The delete function to call.
:param show_func: The show function to call looking for 'NotFound'.
:param ID: The ID of the object to delete/show.
:raises TimeoutException: The object did not achieve the status or ERROR in
the check_timeout period.
:returns: None
"""
try:
return delete_func(*args, **kwargs)
except exceptions.NotFound:
return
start = int(time.time())
LOG.info('Waiting for object to be NotFound')
while True:
try:
show_func(*args, **kwargs)
except exceptions.NotFound:
return
if int(time.time()) - start >= CONF.load_balancer.check_timeout:
message = ('{name} did not raise NotFound in {timeout} '
'seconds.'.format(
name=show_func.__name__,
timeout=CONF.load_balancer.check_timeout))
raise exceptions.TimeoutException(message)
time.sleep(CONF.load_balancer.check_interval)
def wait_for_deleted_status_or_not_found(
show_client, id, status_key, check_interval, check_timeout,
root_tag=None):
"""Waits for an object to reach a DELETED status or be not found (404).
:param show_client: The tempest service client show method.
Ex. cls.os_primary.servers_client.show_server
:param id: The id of the object to query.
:param status_key: The key of the status field in the response.
Ex. provisioning_status
:check_interval: How often to check the status, in seconds.
:check_timeout: The maximum time, in seconds, to check the status.
:root_tag: The root tag on the response to remove, if any.
:raises CommandFailed: Raised if the object goes into ERROR and ERROR was
not the desired status.
:raises TimeoutException: The object did not achieve the status or ERROR in
the check_timeout period.
:returns: None
"""
start = int(time.time())
LOG.info('Waiting for {name} status to update to DELETED or be not '
'found(404)'.format(name=show_client.__name__))
while True:
try:
response = show_client(id)
except exceptions.NotFound:
return
if root_tag:
object_details = response[root_tag]
else:
object_details = response
if object_details[status_key] == const.DELETED:
LOG.info('{name}\'s status updated to DELETED.'.format(
name=show_client.__name__))
return
elif int(time.time()) - start >= check_timeout:
message = (
'{name} {field} failed to update to DELETED or become not '
'found (404) within the required time {timeout}. Current '
'status of {name}: {status}'.format(
name=show_client.__name__,
timeout=check_timeout,
status=object_details[status_key],
field=status_key
))
caller = test_utils.find_test_caller()
if caller:
message = '({caller}) {message}'.format(caller=caller,
message=message)
raise exceptions.TimeoutException(message)
time.sleep(check_interval)

View File

@ -0,0 +1,11 @@
- hosts: all
name: Octavia DSVM jobs pre-run playbook
tasks:
- shell:
cmd: |
set -e
set -x
if $(egrep --quiet '(vmx|svm)' /proc/cpuinfo) && [[ ! $(hostname) =~ "ovh" ]]; then
export DEVSTACK_GATE_LIBVIRT_TYPE=kvm
fi

View File

@ -2,7 +2,14 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
python-dateutil>=2.5.3 # BSD
ipaddress>=1.0.17;python_version<'3.3' # PSF
pbr!=2.1.0,>=2.0.0 # Apache-2.0
oslo.config>=5.2.0 # Apache-2.0
oslo.log>=3.36.0 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0
oslotest>=3.2.0 # Apache-2.0
requests>=2.14.2 # Apache-2.0
six>=1.10.0 # MIT
tempest>=17.1.0 # Apache-2.0
tenacity>=4.4.0 # Apache-2.0

View File

@ -18,6 +18,10 @@ classifier =
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
[global]
setup-hooks =
pbr.hooks.setup_hook
[files]
packages =
octavia_tempest_plugin
@ -48,3 +52,7 @@ output_file = octavia_tempest_plugin/locale/octavia_tempest_plugin.pot
all_files = 1
build-dir = releasenotes/build
source-dir = releasenotes/source
[entry_points]
tempest.test_plugins =
octavia-tempest-plugin = octavia_tempest_plugin.plugin:OctaviaTempestPlugin

View File

@ -6,12 +6,7 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
python-subunit>=1.0.0 # Apache-2.0/BSD
sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
oslosphinx>=4.7.0 # Apache-2.0
oslotest>=3.2.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=2.2.0 # MIT
# releasenotes
reno>=2.5.0 # Apache-2.0

20
tox.ini
View File

@ -22,9 +22,20 @@ commands = {posargs}
commands = python setup.py test --coverage --testr-args='{posargs}'
[testenv:docs]
commands = python setup.py build_sphinx
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/doc/requirements.txt
whitelist_externals = rm
commands =
rm -rf doc/build
sphinx-build -W -b html doc/source doc/build/html
[testenv:releasenotes]
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/doc/requirements.txt
commands =
sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
@ -38,3 +49,10 @@ show-source = True
ignore = E123,E125
builtins = _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
[testenv:genconfig]
whitelist_externals = mkdir
commands =
mkdir -p etc
oslo-config-generator --output-file etc/octavia.tempest.conf.sample \
--namespace tempest.config

View File

@ -1,23 +1,27 @@
- job:
name: octavia-v2-dsvm-scenario
name: octavia-dsvm-base
parent: devstack-tempest
timeout: 7800
required-projects:
- openstack/barbican
- openstack/diskimage-builder
- openstack/octavia
- openstack/octavia-tempest-plugin
- openstack/python-barbicanclient
- openstack/python-octaviaclient
pre-run: playbooks/Octavia-DSVM/pre.yaml
irrelevant-files:
- ^.*\.rst$
- ^api-ref/.*$
- ^doc/.*$
- ^etc/.*$
- ^releasenotes/.*$
vars:
devstack_localrc:
TEMPEST_PLUGINS: "'{{ ansible_user_dir }}/src/git.openstack.org/openstack/octavia-tempest-plugin'"
devstack_local_conf:
post-config:
$OCTAVIA_CONF:
DEFAULT:
debug: True
devstack_services:
barbican: true
c-bak: false
ceilometer-acentral: false
ceilometer-acompute: false
@ -41,19 +45,82 @@
s-object: false
s-proxy: false
tempest: true
devstack_plugins:
octavia: https://github.com/openstack/octavia.git
- job:
name: octavia-dsvm-live-base
parent: octavia-dsvm-base
required-projects:
- openstack/barbican
- openstack/diskimage-builder
- openstack/python-barbicanclient
vars:
devstack_services:
barbican: true
neutron-qos: true
devstack_plugins:
barbican: https://github.com/openstack/barbican.git
octavia: https://github.com/openstack/octavia.git
neutron: https://github.com/openstack/neutron.git
- job:
name: octavia-dsvm-noop-base
parent: octavia-dsvm-base
vars:
devstack_localrc:
DISABLE_AMP_IMAGE_BUILD: True
devstack_local_conf:
test-config:
"$TEMPEST_CONFIG":
load_balancer:
test_with_noop: True
post-config:
$OCTAVIA_CONF:
controller_worker:
amphora_driver: amphora_noop_driver
compute_driver: compute_noop_driver
network_driver: network_noop_driver
certificates:
cert_manager: local_cert_manager
devstack_services:
barbican: false
- job:
name: octavia-v2-dsvm-noop-api
parent: octavia-dsvm-noop-base
vars:
devstack_local_conf:
post-config:
$OCTAVIA_CONF:
api_settings:
api_v1_enabled: False
tempest_concurrency: 2
tempest_test_regex: ^octavia_tempest_plugin
tempest_test_regex: ^octavia_tempest_plugin.tests.api.v2
tox_envlist: all
- job:
name: octavia-v2-dsvm-noop-py35-api
parent: octavia-v2-dsvm-noop-api
vars:
devstack_localrc:
USE_PYTHON3: true
- job:
name: octavia-v2-dsvm-scenario
parent: octavia-dsvm-base
vars:
devstack_local_conf:
post-config:
$OCTAVIA_CONF:
api_settings:
api_v1_enabled: False
tempest_concurrency: 2
tempest_test_regex: ^octavia_tempest_plugin.tests.scenario.v2
tox_envlist: all
- job:
name: octavia-v2-dsvm-py35-scenario
parent: octavia-v2-dsvm-scenario
timeout: 7800
vars:
devstack_localrc:
USE_PYTHON3: true

16
zuul.d/projects.yaml Normal file
View File

@ -0,0 +1,16 @@
# Note: Some official OpenStack wide jobs are still defined in the
# project-config repository
- project:
check:
jobs:
- octavia-v2-dsvm-noop-api
- octavia-v2-dsvm-noop-py35-api
- octavia-v2-dsvm-scenario
- octavia-v2-dsvm-py35-scenario
gate:
queue: octavia
jobs:
- octavia-v2-dsvm-noop-api
- octavia-v2-dsvm-noop-py35-api
- octavia-v2-dsvm-scenario
- octavia-v2-dsvm-py35-scenario