diff --git a/.gitignore b/.gitignore index 963e589a..890fe27c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +releasenotes/build + +# Oslo config generator +etc/octavia.tempest.conf.sample diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 00000000..1ade022a --- /dev/null +++ b/doc/requirements.txt @@ -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 diff --git a/doc/source/conf.py b/doc/source/conf.py index c99e0b6d..1f18a18b 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -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 + # [exclude_pattern, ...] + apidoc.main([ + '--force', + '-o', + out_dir, + module, + ]) + + +def setup(app): + app.connect('builder-inited', run_apidoc) diff --git a/doc/source/configref.rst b/doc/source/configref.rst new file mode 100644 index 00000000..f9db0424 --- /dev/null +++ b/doc/source/configref.rst @@ -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 diff --git a/doc/source/index.rst b/doc/source/index.rst index fad3a364..494a0b63 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -14,11 +14,16 @@ Contents: readme installation contributing + configref Indices and tables ================== +.. toctree:: + :hidden: + + _build/modules/modules + * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/octavia_tempest_plugin/clients.py b/octavia_tempest_plugin/clients.py new file mode 100644 index 00000000..aa1949fa --- /dev/null +++ b/octavia_tempest_plugin/clients.py @@ -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) diff --git a/octavia_tempest_plugin/tests/v2/api/__init__.py b/octavia_tempest_plugin/common/__init__.py similarity index 100% rename from octavia_tempest_plugin/tests/v2/api/__init__.py rename to octavia_tempest_plugin/common/__init__.py diff --git a/octavia_tempest_plugin/common/constants.py b/octavia_tempest_plugin/common/constants.py new file mode 100644 index 00000000..1d63d0de --- /dev/null +++ b/octavia_tempest_plugin/common/constants.py @@ -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' diff --git a/octavia_tempest_plugin/config.py b/octavia_tempest_plugin/config.py new file mode 100644 index 00000000..70a43aab --- /dev/null +++ b/octavia_tempest_plugin/config.py @@ -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.'), +] diff --git a/octavia_tempest_plugin/contrib/__init__.py b/octavia_tempest_plugin/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_tempest_plugin/contrib/httpd/__init__.py b/octavia_tempest_plugin/contrib/httpd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_tempest_plugin/plugin.py b/octavia_tempest_plugin/plugin.py new file mode 100644 index 00000000..5aae7228 --- /dev/null +++ b/octavia_tempest_plugin/plugin.py @@ -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] diff --git a/octavia_tempest_plugin/services/__init__.py b/octavia_tempest_plugin/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_tempest_plugin/services/load_balancer/__init__.py b/octavia_tempest_plugin/services/load_balancer/__init__.py new file mode 100644 index 00000000..4de8bbb0 --- /dev/null +++ b/octavia_tempest_plugin/services/load_balancer/__init__.py @@ -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'] diff --git a/octavia_tempest_plugin/services/load_balancer/v2/__init__.py b/octavia_tempest_plugin/services/load_balancer/v2/__init__.py new file mode 100644 index 00000000..d31d6cf3 --- /dev/null +++ b/octavia_tempest_plugin/services/load_balancer/v2/__init__.py @@ -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'] diff --git a/octavia_tempest_plugin/services/load_balancer/v2/loadbalancer_client.py b/octavia_tempest_plugin/services/load_balancer/v2/loadbalancer_client.py new file mode 100644 index 00000000..55644763 --- /dev/null +++ b/octavia_tempest_plugin/services/load_balancer/v2/loadbalancer_client.py @@ -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) diff --git a/octavia_tempest_plugin/tests/api/__init__.py b/octavia_tempest_plugin/tests/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_tempest_plugin/tests/api/v2/__init__.py b/octavia_tempest_plugin/tests/api/v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py b/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py new file mode 100644 index 00000000..6a651af4 --- /dev/null +++ b/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py @@ -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 diff --git a/octavia_tempest_plugin/tests/base.py b/octavia_tempest_plugin/tests/base.py deleted file mode 100644 index 1c30cdb5..00000000 --- a/octavia_tempest_plugin/tests/base.py +++ /dev/null @@ -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.""" diff --git a/octavia_tempest_plugin/tests/scenario/__init__.py b/octavia_tempest_plugin/tests/scenario/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_tempest_plugin/tests/scenario/v2/__init__.py b/octavia_tempest_plugin/tests/scenario/v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_tempest_plugin/tests/scenario/v2/test_load_balancer.py b/octavia_tempest_plugin/tests/scenario/v2/test_load_balancer.py new file mode 100644 index 00000000..e56a9bb7 --- /dev/null +++ b/octavia_tempest_plugin/tests/scenario/v2/test_load_balancer.py @@ -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) diff --git a/octavia_tempest_plugin/tests/test_base.py b/octavia_tempest_plugin/tests/test_base.py new file mode 100644 index 00000000..978068a6 --- /dev/null +++ b/octavia_tempest_plugin/tests/test_base.py @@ -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 diff --git a/octavia_tempest_plugin/tests/test_octavia_tempest_plugin.py b/octavia_tempest_plugin/tests/test_octavia_tempest_plugin.py deleted file mode 100644 index 7805347c..00000000 --- a/octavia_tempest_plugin/tests/test_octavia_tempest_plugin.py +++ /dev/null @@ -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 diff --git a/octavia_tempest_plugin/tests/validators.py b/octavia_tempest_plugin/tests/validators.py new file mode 100644 index 00000000..2dc1d640 --- /dev/null +++ b/octavia_tempest_plugin/tests/validators.py @@ -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() diff --git a/octavia_tempest_plugin/tests/waiters.py b/octavia_tempest_plugin/tests/waiters.py new file mode 100644 index 00000000..7825782d --- /dev/null +++ b/octavia_tempest_plugin/tests/waiters.py @@ -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) diff --git a/playbooks/Octavia-DSVM/pre.yaml b/playbooks/Octavia-DSVM/pre.yaml new file mode 100644 index 00000000..1e7987cf --- /dev/null +++ b/playbooks/Octavia-DSVM/pre.yaml @@ -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 + diff --git a/requirements.txt b/requirements.txt index 432eb5b9..303b8968 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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>=3.2.1 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 993c4f7f..b4526143 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/test-requirements.txt b/test-requirements.txt index 9a608cb0..8ad59620 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -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 diff --git a/tox.ini b/tox.ini index 31717398..f168afd1 100644 --- a/tox.ini +++ b/tox.ini @@ -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 diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml index 994e25e4..c56bee8c 100644 --- a/zuul.d/jobs.yaml +++ b/zuul.d/jobs.yaml @@ -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 diff --git a/zuul.d/projects.yaml b/zuul.d/projects.yaml new file mode 100644 index 00000000..f09a634e --- /dev/null +++ b/zuul.d/projects.yaml @@ -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