From 6daf82777ec72337cc0afa6a702e66b64b622488 Mon Sep 17 00:00:00 2001 From: Paul Glass Date: Mon, 24 Aug 2015 15:41:17 +0000 Subject: [PATCH] Layout some functional tests for the V2 CLI Change-Id: Ib2d1261bcb0362c586c0aae4b9c5a8a563f07c71 --- .gitignore | 1 + .testr.conf | 8 +- designateclient/functionaltests/__init__.py | 7 ++ designateclient/functionaltests/base.py | 26 ++++++ designateclient/functionaltests/client.py | 86 +++++++++++++++++++ designateclient/functionaltests/config.py | 64 ++++++++++++++ designateclient/functionaltests/datagen.py | 22 +++++ designateclient/functionaltests/models.py | 80 +++++++++++++++++ .../functionaltests/v2/__init__.py | 0 .../functionaltests/v2/test_zone.py | 47 ++++++++++ doc/source/functional-tests.rst | 67 +++++++++++++++ doc/source/index.rst | 1 + test-requirements.txt | 1 + tox.ini | 10 +++ 14 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 designateclient/functionaltests/__init__.py create mode 100644 designateclient/functionaltests/base.py create mode 100644 designateclient/functionaltests/client.py create mode 100644 designateclient/functionaltests/config.py create mode 100644 designateclient/functionaltests/datagen.py create mode 100644 designateclient/functionaltests/models.py create mode 100644 designateclient/functionaltests/v2/__init__.py create mode 100644 designateclient/functionaltests/v2/test_zone.py create mode 100644 doc/source/functional-tests.rst diff --git a/.gitignore b/.gitignore index 8f113f0..a297ef3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ doc/build/* dist designateclient/versioninfo .testrepository +*.log diff --git a/.testr.conf b/.testr.conf index bd46664..5325b9e 100644 --- a/.testr.conf +++ b/.testr.conf @@ -1,4 +1,10 @@ [DEFAULT] -test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./designateclient/tests $LISTOPT $IDOPTION +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ + OS_LOG_CAPTURE=${OS_LOG_CAPTURE:-1} \ + OS_DEBUG=${OS_DEBUG:-1} \ + OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-160} \ + ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./designateclient/tests} $LISTOPT $IDOPTION + test_id_option=--load-list $IDFILE test_list_option=--list diff --git a/designateclient/functionaltests/__init__.py b/designateclient/functionaltests/__init__.py new file mode 100644 index 0000000..f96945f --- /dev/null +++ b/designateclient/functionaltests/__init__.py @@ -0,0 +1,7 @@ +import logging + +logging.basicConfig( + filename='functional-tests.log', + filemode='w', + level=logging.DEBUG, +) diff --git a/designateclient/functionaltests/base.py b/designateclient/functionaltests/base.py new file mode 100644 index 0000000..52e293d --- /dev/null +++ b/designateclient/functionaltests/base.py @@ -0,0 +1,26 @@ +""" +Copyright 2015 Rackspace + +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_lib.cli import base + +from designateclient.functionaltests import client +from designateclient.functionaltests import config + + +class BaseDesignateTest(base.ClientTestBase): + + def _get_clients(self): + config.read_config() + return client.DesignateCLI.as_user('default') diff --git a/designateclient/functionaltests/client.py b/designateclient/functionaltests/client.py new file mode 100644 index 0000000..01db686 --- /dev/null +++ b/designateclient/functionaltests/client.py @@ -0,0 +1,86 @@ +""" +Copyright 2015 Rackspace + +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 logging + +from tempest_lib.cli import base + +from designateclient.functionaltests.config import cfg +from designateclient.functionaltests.models import FieldValueModel +from designateclient.functionaltests.models import ListModel + + +LOG = logging.getLogger(__name__) + + +class ZoneCommands(object): + """This is a mixin that provides zone commands to DesignateCLI""" + + def zone_list(self, *args, **kwargs): + return self.parsed_cmd('zone list', ListModel, *args, **kwargs) + + def zone_show(self, id, *args, **kwargs): + return self.parsed_cmd('zone show %s' % id, FieldValueModel, *args, + **kwargs) + + def zone_delete(self, id, *args, **kwargs): + return self.parsed_cmd('zone delete %s' % id, *args, **kwargs) + + def zone_create(self, name, email, *args, **kwargs): + cmd = 'zone create %s --email %s' % (name, email) + return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs) + + +class DesignateCLI(base.CLIClient, ZoneCommands): + + @classmethod + def get_clients(cls): + return { + 'default': DesignateCLI( + cli_dir=cfg.CONF.designateclient.directory, + username=cfg.CONF.identity.username, + password=cfg.CONF.identity.password, + tenant_name=cfg.CONF.identity.tenant_name, + uri=cfg.CONF.identity.uri, + ), + 'alt': DesignateCLI( + cli_dir=cfg.CONF.designateclient.directory, + username=cfg.CONF.identity.alt_username, + password=cfg.CONF.identity.alt_password, + tenant_name=cfg.CONF.identity.alt_tenant_name, + uri=cfg.CONF.identity.uri, + ), + 'admin': DesignateCLI( + cli_dir=cfg.CONF.designateclient.directory, + username=cfg.CONF.identity.admin_username, + password=cfg.CONF.identity.admin_password, + tenant_name=cfg.CONF.identity.admin_tenant_name, + uri=cfg.CONF.identity.uri, + ) + } + + @classmethod + def as_user(self, user): + clients = self.get_clients() + if user in clients: + return clients[user] + raise Exception("User '{0}' does not exist".format(user)) + + def parsed_cmd(self, cmd, model=None, *args, **kwargs): + out = self.openstack(cmd, *args, **kwargs) + LOG.debug(out) + if model is not None: + return model(out) + return out diff --git a/designateclient/functionaltests/config.py b/designateclient/functionaltests/config.py new file mode 100644 index 0000000..edf65d4 --- /dev/null +++ b/designateclient/functionaltests/config.py @@ -0,0 +1,64 @@ +""" +Copyright 2015 Rackspace + +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 oslo_config import cfg + +cfg.CONF.register_group(cfg.OptGroup( + name='identity', title="Configuration for Keystone auth" +)) + +cfg.CONF.register_group(cfg.OptGroup( + name='designateclient', title="Configuration for the Designate client" +)) + +cfg.CONF.register_opts([ + cfg.StrOpt('uri', help="The Keystone v2 endpoint"), + cfg.StrOpt('uri_v3', help="The Keystone v3 endpoint"), + cfg.StrOpt('auth_version', default='v2'), + cfg.StrOpt('region', default='RegionOne'), + + cfg.StrOpt('username'), + cfg.StrOpt('tenant_name'), + cfg.StrOpt('password', secret=True), + cfg.StrOpt('domain_name'), + + cfg.StrOpt('alt_username'), + cfg.StrOpt('alt_tenant_name'), + cfg.StrOpt('alt_password', secret=True), + cfg.StrOpt('alt_domain_name'), + + cfg.StrOpt('admin_username'), + cfg.StrOpt('admin_tenant_name'), + cfg.StrOpt('admin_password', secret=True), + cfg.StrOpt('admin_domain_name'), +], group='identity') + + +cfg.CONF.register_opts([ + cfg.StrOpt('directory', + help='the directory containing the client executable'), +], group='designateclient') + + +def find_config_file(): + return os.environ.get( + 'TEMPEST_CONFIG', '/opt/stack/tempest/etc/tempest.conf') + + +def read_config(): + cfg.CONF(args=[], default_config_files=[find_config_file()]) diff --git a/designateclient/functionaltests/datagen.py b/designateclient/functionaltests/datagen.py new file mode 100644 index 0000000..9fd8bde --- /dev/null +++ b/designateclient/functionaltests/datagen.py @@ -0,0 +1,22 @@ +""" +Copyright 2015 Rackspace + +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 random +import string + + +def random_zone_name(name='testdomain'): + digits = "".join([random.choice(string.digits) for _ in range(8)]) + return "{0}{1}.com.".format(name, digits) diff --git a/designateclient/functionaltests/models.py b/designateclient/functionaltests/models.py new file mode 100644 index 0000000..e85709d --- /dev/null +++ b/designateclient/functionaltests/models.py @@ -0,0 +1,80 @@ +""" +Copyright 2015 Rackspace + +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_lib.cli import output_parser + + +class Model(object): + + def __str__(self): + return str(self.__dict__) + + +class FieldValueModel(Model): + """This converts cli output from messy lists/dicts to neat attributes.""" + + def __init__(self, out): + """This parses output with fields and values like: + + +----------------+------------------------------+ + | Field | Value | + +----------------+------------------------------+ + | action | CREATE | + | created_at | 2015-08-20T17:22:17.000000 | + | description | None | + +----------------+------------------------------+ + + These are then accessible as: + + model.action + model.created_at + model.description + + """ + table = output_parser.table(out) + for field, value in table['values']: + setattr(self, field, value) + + +class ListEntryModel(Model): + + def __init__(self, fields, values): + for k, v in zip(fields, values): + setattr(self, k, v) + + +class ListModel(Model, list): + + def __init__(self, out): + """This parses an output table with any number of headers, and any + number of entries: + + +--------------------------------------+----------+---------+ + | id | name | type | + +--------------------------------------+----------+---------+ + | e658a875-1024-4f88-a347-e5b244ec5a10 | aaa.com. | PRIMARY | + +--------------------------------------+----------+---------+ + | 98d1fb5f-2954-448e-988e-6f1df0f24c52 | bbb.com. | PRIMARY | + +--------------------------------------+----------+---------+ + + These are then accessible as: + + model[0].name == 'aaa.com.' + model[1].name == 'bbb.com.' + + """ + table = output_parser.table(out) + for entry in table['values']: + self.append(ListEntryModel(table['headers'], entry)) diff --git a/designateclient/functionaltests/v2/__init__.py b/designateclient/functionaltests/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designateclient/functionaltests/v2/test_zone.py b/designateclient/functionaltests/v2/test_zone.py new file mode 100644 index 0000000..0d30a22 --- /dev/null +++ b/designateclient/functionaltests/v2/test_zone.py @@ -0,0 +1,47 @@ +""" +Copyright 2015 Rackspace + +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_lib.exceptions import CommandFailed + +from designateclient.functionaltests.base import BaseDesignateTest +from designateclient.functionaltests.datagen import random_zone_name + + +class TestZone(BaseDesignateTest): + + def setUp(self): + super(TestZone, self).setUp() + self.zone_name = random_zone_name() + zone = self.clients.zone_create(name=self.zone_name, + email='test@example.com') + self.zone_id = zone.id + + def test_zone_list(self): + zones = self.clients.zone_list() + self.assertGreater(len(zones), 0) + + def test_zone_create_and_show(self): + zone = self.clients.zone_show(self.zone_id) + self.assertEqual(zone.name, self.zone_name) + self.assertEqual(zone.id, self.zone_id) + + def test_zone_delete(self): + self.clients.zone_delete(self.zone_id) + self.assertRaises(CommandFailed, self.clients.zone_show, self.zone_id) + + def tearDown(self): + if hasattr(self, 'zone_id'): + self.clients.zone_delete(self.zone_id, fail_ok=True) + super(TestZone, self).tearDown() diff --git a/doc/source/functional-tests.rst b/doc/source/functional-tests.rst new file mode 100644 index 0000000..3726eb9 --- /dev/null +++ b/doc/source/functional-tests.rst @@ -0,0 +1,67 @@ +================ +Functional Tests +================ + +The functional tests invoke the client executable to see that it actually works +with a running Designate. WARNING: these tests will create and delete zones, +recordsets, and other resources in Designate. + +Installation +------------ + +.. code-block:: shell-session + + cd python-designateclient + pip install python-openstackclient + pip install -r requirements.txt -r test-requirements.txt + pip install -e . + +Configuration +------------- + +The functional tests look for a variable ``TEMPEST_CONFIG`` which specifies a +config file for the test. + +.. code-block:: shell-session + + export TEMPEST_CONFIG=tempest.conf + +The tests will use Keystone to grab the Designate endpoint to test against. +They need at least three users (two regular users, and one admin) for all the +tests to run. + +.. code-block:: shell-session + + [identity] + uri = http://localhost:5000/v2.0 + uri_v3 = http://localhost:5000/v3 + auth_version = v2 + region = RegionOne + + username = demo + tenant_name = demo + password = password + domain_name = Default + + alt_username = alt_demo + alt_tenant_name = alt_demo + alt_password = password + alt_domain_name = Default + + admin_username = admin + admin_tenant_name = admin + admin_password = password + admin_domain_name = Default + + [designateclient] + # the directory containing the openstack executable + directory=/root/python-designateclient/.venv/bin + +Running the tests +----------------- + +The functional tests are run with tox (installed with ``pip install tox``): + +.. code-block:: shell-session + + tox -e functional diff --git a/doc/source/index.rst b/doc/source/index.rst index b252f5b..dd89358 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -21,6 +21,7 @@ Contents shell-examples shell-v2 contributing + functional-tests Indices and tables ================== diff --git a/test-requirements.txt b/test-requirements.txt index bbaed9e..ba8bad5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,3 +12,4 @@ requests-mock>=0.6.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 testrepository>=0.0.18 oslosphinx>=2.5.0 # Apache-2.0 +tempest-lib>=0.8.0 diff --git a/tox.ini b/tox.ini index 92fa9e3..0271b89 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,16 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' [testenv:venv] commands = {posargs} +[testenv:functional] +usedevelop = False +setenv = VIRTUAL_ENV={envdir} + OS_TEST_PATH=designateclient/functionaltests/ +passenv = OS_STDOUT_CAPTURE + OS_STDERR_CAPTURE + OS_LOG_CAPTURE + OS_DEBUG + TEMPEST_CONFIG + [flake8] # ignored flake8 codes: # H302 import only modules