diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..9d0a594 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +branch = True +source = cloudkitty_tempest_plugin + +[report] +ignore_errors = True diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59b35f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg* +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +cover/ +.coverage* +!.coveragerc +.tox +nosetests.xml +.testrepository +.stestr +.venv + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +doc/build + +# pbr generates these +AUTHORS +ChangeLog + +# Editors +*~ +.*.swp +.*sw? + +# Files created by releasenotes build +releasenotes/build diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..516ae6f --- /dev/null +++ b/.mailmap @@ -0,0 +1,3 @@ +# Format is: +# +# diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..90265d7 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./cloudkitty_tempest_plugin/tests +top_dir=./ diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..c551119 --- /dev/null +++ b/.testr.conf @@ -0,0 +1,5 @@ +[DEFAULT] +test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./cloudkitty_tempest_plugin/tests $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list +group_regex=gabbi\.(suitemaker|driver)\.(test_[^_]+_[^_]+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..fa75a68 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Storyboard, not GitHub: + + https://storyboard.openstack.org/#!/project/890 diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..a9b88f0 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +cloudkitty-tempest-plugin Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..c1eb0c5 --- /dev/null +++ b/README.rst @@ -0,0 +1,25 @@ +================================= +Tempest integration of CloudKitty +================================= + +This project defines a tempest plugin containing tests used to verify the +functionality of a cloudkitty installation. The plugin will automatically load +these tests into tempest. + +Dependencies +------------ + +This plugin tests the CloudKitty API. This supposes that the 'rating' role +exists in your OpenStack installation. + +Developers +---------- +For more information on cloudkitty, refer to: +http://docs.openstack.org/developer/cloudkitty/ + +For more information on tempest plugins, refer to: +http://docs.openstack.org/developer/tempest/plugin.html#using-plugins + +Bugs +---- +Please report bugs to: https://storyboard.openstack.org/#!/project/890 diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..15cd6cb --- /dev/null +++ b/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] + diff --git a/cloudkitty_tempest_plugin/README.rst b/cloudkitty_tempest_plugin/README.rst new file mode 100644 index 0000000..9e19517 --- /dev/null +++ b/cloudkitty_tempest_plugin/README.rst @@ -0,0 +1,6 @@ +=============================================== +Tempest Integration of CloudKitty +=============================================== + +This directory contains Tempest tests to cover the CloudKitty project. + diff --git a/cloudkitty_tempest_plugin/__init__.py b/cloudkitty_tempest_plugin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudkitty_tempest_plugin/config.py b/cloudkitty_tempest_plugin/config.py new file mode 100644 index 0000000..fab2b57 --- /dev/null +++ b/cloudkitty_tempest_plugin/config.py @@ -0,0 +1,33 @@ +# Copyright 2017 Objectif Libre +# 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 oslo_config import cfg + +rating_group = cfg.OptGroup(name='rating_plugin', + title='Rating Service Options') + +RatingGroup = [ + cfg.StrOpt('service_name', + default='rating', + help="Service name of the Rating service."), + cfg.StrOpt('user_name', + default='cloudkitty', + help="User name for the Rating service."), + cfg.StrOpt('endpoint_type', + default='publicURL', + choices=['public', 'admin', 'internal', + 'publicURL', 'adminURL', 'internalURL'], + help="The endpoint type to use for the rating service."), +] diff --git a/cloudkitty_tempest_plugin/plugin.py b/cloudkitty_tempest_plugin/plugin.py new file mode 100644 index 0000000..8472876 --- /dev/null +++ b/cloudkitty_tempest_plugin/plugin.py @@ -0,0 +1,42 @@ +# Copyright 2017 Objectif Libre +# 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 os + +from tempest import config +from tempest.test_discover import plugins + +from cloudkitty_tempest_plugin import config as project_config + + +class CloudkittyTempestPlugin(plugins.TempestPlugin): + def load_tests(self): + base_path = os.path.split(os.path.dirname( + os.path.abspath(__file__)))[0] + test_dir = "cloudkitty_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.rating_group, + project_config.RatingGroup) + + def get_opt_lists(self): + return [ + (project_config.rating_group.name, + project_config.RatingGroup), + ] diff --git a/cloudkitty_tempest_plugin/services/__init__.py b/cloudkitty_tempest_plugin/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudkitty_tempest_plugin/services/client.py b/cloudkitty_tempest_plugin/services/client.py new file mode 100644 index 0000000..911d21d --- /dev/null +++ b/cloudkitty_tempest_plugin/services/client.py @@ -0,0 +1,455 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Objectif Libre +# +# 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 six + +from keystoneauth1.identity import v3 +from keystoneauth1 import session +from keystoneclient import client +from oslo_serialization import jsonutils as json +from tempest import config +from tempest.lib.common import rest_client +from tempest import manager + +CONF = config.CONF + +CLOUDKITTY_API_VERSION = 'v1' + + +class RatingClient(rest_client.RestClient): + """Implementation of cloudkittyclient for testing purposes""" + api_version = 'v1' + + @staticmethod + def deserialize(body): + return json.loads(body.replace("\n", "")) + + @staticmethod + def serialize(body): + return json.dumps(body) + + def _do_request(self, method, uri, body=None, expected_code=200): + resp, body = self.request(method, uri, body=body) + self.expected_success(expected_code, resp.status) + body = self.deserialize(body) if body else dict() + if not isinstance(body, dict) and not isinstance(body, list): + body = dict() + # ResponseBody inherits from dict, so lists must be converted + body = dict(body=body) if isinstance(body, list) else body + return rest_client.ResponseBody(resp, body) + + def get_collector_mappings(self, service=None): + uri = '/collector/mappings/' + if service: + uri += service + '/' + return self._do_request('GET', uri) + + def create_collector_mapping(self, collector='gnocchi', service='compute'): + uri = '/collector/mappings/' + request_body = { + 'collector': collector, + 'service': service, + } + return self._do_request('POST', uri, body=self.serialize(request_body)) + + def delete_collector_mapping(self, service='compute'): + uri = '/collector/mappings' + request_body = { + 'service': service, + } + return self._do_request('DELETE', uri, + body=self.serialize(request_body), + expected_code=204) + + def get_collector_state(self, collector="gnocchi"): + uri = '/collector/states/' + request_body = { + 'name': collector, + } + return self._do_request('GET', uri, body=self.serialize(request_body)) + + def set_collector_state(self, collector="gnocchi", enabled=True): + uri = '/collector/states/' + request_body = { + 'name': collector, + 'enabled': enabled, + } + return self._do_request('PUT', uri, body=self.serialize(request_body)) + + def get_config(self): + uri = '/info/config/' + return self._do_request('GET', uri) + + def get_service(self, service_name=None): + uri = '/info/services/' + if service_name: + uri += service_name + '/' + return self._do_request('GET', uri) + + def get_rating_module(self, module_name=None): + uri = '/rating/modules/' + if module_name: + uri += module_name + '/' + return self._do_request('GET', uri) + + def update_rating_module(self, module_name, description='', + enabled=False, hot_config=True, priority=1): + uri = '/rating/modules/' + request_body = { + 'module_id': module_name, + 'description': description, + 'enabled': enabled, + 'hot-config': hot_config, + 'priority': priority, + } + return self._do_request('PUT', uri, body=self.serialize(request_body), + expected_code=302) + + def reload_rating_modules(self): + uri = '/rating/reload_modules' + return self._do_request('GET', uri, expected_code=204) + + def get_report_summary(self): + uri = '/report/summary/' + return self._do_request('GET', uri) + + def get_rated_tenants(self): + uri = '/report/tenants/' + return self._do_request('GET', uri) + + def get_report_total(self): + uri = '/report/total/' + return self._do_request('GET', uri) + + def get_storage_dataframes(self): + uri = '/storage/dataframes/' + return self._do_request('GET', uri) + + def get_hashmap_mapping_types(self): + uri = '/rating/module_config/hashmap/types/' + return self._do_request('GET', uri) + + def get_hashmap_service(self, service_id=None): + uri = '/rating/module_config/hashmap/services/' + if service_id: + uri += service_id + '/' + return self._do_request('GET', uri) + + def create_hashmap_service(self, name, service_id=None): + uri = '/rating/module_config/hashmap/services/' + request_body = { + 'name': name, + } + if service_id: + request_body['service_id'] = service_id + return self._do_request('POST', uri, + body=self.serialize(request_body), + expected_code=201) + + def delete_hashmap_service(self, service_id): + uri = '/rating/module_config/hashmap/services/' + request_body = { + 'service_id': service_id, + } + return self._do_request('DELETE', uri, + body=self.serialize(request_body), + expected_code=204) + + def get_hashmap_fields(self, service_id): + uri = '/rating/module_config/hashmap/fields/' + request_body = { + 'service_id': service_id, + } + return self._do_request('GET', uri, + body=self.serialize(request_body)) + + def get_hashmap_field(self, field_id): + uri = '/rating/module_config/hashmap/fields/' + field_id + '/' + return self._do_request('GET', uri) + + def create_hashmap_field(self, field_name, service_id, field_id=None): + uri = '/rating/module_config/hashmap/fields/' + request_body = { + 'name': field_name, + 'service_id': service_id, + } + if field_id: + request_body['field_id'] = field_id + return self._do_request('POST', uri, + body=self.serialize(request_body), + expected_code=201) + + def delete_hashmap_field(self, field_id): + uri = '/rating/module_config/hashmap/fields/' + request_body = { + 'field_id': field_id, + } + return self._do_request('DELETE', uri, + body=self.serialize(request_body), + expected_code=204) + + def get_hashmap_mappings(self, service_id=None, field_id=None, + group_id=None, no_group=False, tenant_id=None, + filter_tenant=False): + args = locals() + args.pop('self') + uri = '/rating/module_config/hashmap/mappings/' + request_body = dict((k, v) + for k, v in six.iteritems(args) if v is not None) + return self._do_request('GET', uri, + body=self.serialize(request_body)) + + def get_hashmap_mapping(self, mapping_id): + uri = '/rating/module_config/hashmap/mappings/' + mapping_id + '/' + return self._do_request('GET', uri) + + def create_hashmap_mapping(self, cost=0, field_id=None, group_id=None, + map_type=None, mapping_id=None, service_id=None, + tenant_id=None, value=None): + args = locals() + args.pop('self') + uri = '/rating/module_config/hashmap/mappings/' + request_body = dict((k, v) + for k, v in six.iteritems(args) if v is not None) + return self._do_request('POST', uri, + body=self.serialize(request_body), + expected_code=201) + + def delete_hashmap_mapping(self, mapping_id): + uri = '/rating/module_config/hashmap/mappings/' + request_body = { + 'mapping_id': mapping_id, + } + return self._do_request('DELETE', uri, + body=self.serialize(request_body), + expected_code=204) + + def update_hashmap_mapping(self, mapping_id, cost=0, + field_id=None, group_id=None, map_type=None, + service_id=None, tenant_id=None, value=None): + args = locals() + args.pop('self') + uri = '/rating/module_config/hashmap/mappings/' + request_body = dict((k, v) + for k, v in six.iteritems(args) if v is not None) + return self._do_request('PUT', uri, + body=self.serialize(request_body), + expected_code=302) + + def get_hashmap_mapping_group(self, mapping_id): + uri = '/rating/module_config/hashmap/mappings/group/' + request_body = { + 'mapping_id': mapping_id, + } + return self._do_request('GET', uri, + body=self.serialize(request_body)) + + def get_hashmap_group(self, group_id=None): + uri = '/rating/module_config/hashmap/groups/' + if group_id: + uri += group_id + '/' + return self._do_request('GET', uri) + + def create_hashmap_group(self, group_name, group_id=None): + uri = '/rating/module_config/hashmap/groups' + request_body = { + 'name': group_name, + } + if group_id: + request_body['group_id'] = group_id + return self._do_request('POST', uri, + body=self.serialize(request_body), + expected_code=201) + + def delete_hashmap_group(self, group_id): + uri = '/rating/module_config/hashmap/groups' + request_body = { + 'group_id': group_id, + } + return self._do_request('DELETE', uri, + body=self.serialize(request_body), + expected_code=204) + + def get_hashmap_group_mappings(self, group_id): + uri = '/rating/module_config/hashmap/groups/mappings/' + request_body = { + 'group_id': group_id, + } + return self._do_request('GET', uri, + body=self.serialize(request_body)) + + def get_hashmap_group_threshold(self, group_id): + uri = '/rating/module_config/hashmap/groups/thresholds/' + request_body = { + 'group_id': group_id, + } + return self._do_request('GET', uri, + body=self.serialize(request_body)) + + def get_hashmap_threshold(self, threshold_id): + uri = '/rating/module_config/hashmap/thresholds/' + threshold_id + '/' + return self._do_request('GET', uri) + + def get_hashmap_thresholds(self, service_id=None, + field_id=None, group_id=None): + args = locals() + args.pop('self') + uri = '/rating/module_config/hashmap/thresholds/' + request_body = dict((k, v) + for k, v in six.iteritems(args) if v is not None) + return self._do_request('GET', uri, + body=self.serialize(request_body)) + + def create_hashmap_threshold(self, field_id=None, group_id=None, + threshold_id=None, map_type=None, cost=None, + service_id=None, tenant_id=None, level=None): + args = locals() + args.pop('self') + uri = '/rating/module_config/hashmap/thresholds/' + request_body = dict((k, v) + for k, v in six.iteritems(args) if v is not None) + return self._do_request('POST', uri, + body=self.serialize(request_body), + expected_code=201) + + def update_hashmap_threshold(self, threshold_id, field_id=None, + group_id=None, map_type=None, cost=None, + service_id=None, tenant_id=None, level=None): + args = locals() + args.pop('self') + uri = '/rating/module_config/hashmap/thresholds/' + request_body = dict((k, v) + for k, v in six.iteritems(args) if v is not None) + return self._do_request('PUT', uri, + body=self.serialize(request_body), + expected_code=302) + + def delete_hashmap_threshold(self, threshold_id): + uri = '/rating/module_config/hashmap/thresholds/' + request_body = { + 'threshold_id': threshold_id, + } + return self._do_request('DELETE', uri, + body=self.serialize(request_body), + expected_code=204) + + def get_pyscripts(self, no_data=False): + uri = '/rating/module_config/pyscripts/scripts/' + request_body = { + 'no_data': no_data, + } + return self._do_request('GET', uri, body=self.serialize(request_body)) + + def get_pyscript(self, script_id): + uri = '/rating/module_config/pyscripts/scripts/' + script_id + '/' + return self._do_request('GET', uri) + + @staticmethod + def _get_pyscript_request_body(name, data, checksum, script_id): + args = locals() + request_body = dict((k, v) + for k, v in six.iteritems(args) if v is not None) + return request_body + + def create_pyscript(self, name, data, checksum=None, script_id=None): + uri = '/rating/module_config/pyscripts/scripts/' + request_body = self._get_pyscript_request_body(name, data, + checksum, script_id) + return self._do_request('POST', uri, + body=self.serialize(request_body), + expected_code=201) + + def update_pyscript(self, script_id, name=None, data=None, checksum=None): + uri = '/rating/module_config/pyscripts/scripts/' + request_body = self._get_pyscript_request_body(name, data, + checksum, script_id) + return self._do_request('PUT', uri, + body=self.serialize(request_body), + expected_code=201) + + def delete_pyscript(self, script_id): + uri = '/rating/module_config/pyscripts/scripts/' + request_body = { + 'script_id': script_id, + } + return self._do_request('DELETE', uri, + body=self.serialize(request_body), + expected_code=204) + + +class Manager(manager.Manager): + + rating_params = { + 'service': CONF.rating_plugin.service_name, + 'region': CONF.identity.region, + 'endpoint_type': CONF.rating_plugin.endpoint_type, + } + + def __init__(self, credentials=None, service=None): + super(Manager, self).__init__(credentials) + self.set_rating_client() + + def set_rating_client(self): + self.rating_client = RatingClient(self.auth_provider, + **self.rating_params) + + +class CustomIdentityClient(object): + """Custom Keystone client + + Class used by the CK tempest plugin to add the 'rating' role to + the dynamically allocated test tenant + """ + + def __init__(self): + self.admin_auth = v3.Password( + auth_url=CONF.identity.uri_v3, + username=CONF.auth.admin_username, + password=CONF.auth.admin_password, + project_name=CONF.auth.admin_project_name, + project_domain_name=CONF.auth.admin_domain_name, + user_domain_name=CONF.auth.admin_domain_name, + ) + self.admin_session = session.Session(auth=self.admin_auth) + self.admin_client = client.Client(session=self.admin_session) + self.ck_user_id = self._get_ck_user_id() + self.rating_role_id = self._get_rating_role_id() + + def enable_rating(self, project_id): + """Assigns the 'rating' role to ck user on the given project""" + self.admin_client.roles.grant(self.rating_role_id, + user=self.ck_user_id, + project=project_id) + + @staticmethod + def _find_item(iterable, key, value): + item = None + for elem in iterable: + if getattr(elem, key, None) == value: + item = elem + return item + + def _get_ck_user_id(self): + users = self.admin_client.users.list() + return getattr( + self._find_item(users, 'name', CONF.rating_plugin.user_name), + 'id', None, + ) + + def _get_rating_role_id(self): + roles = self.admin_client.roles.list() + return getattr( + self._find_item(roles, 'name', 'rating'), 'id', None, + ) diff --git a/cloudkitty_tempest_plugin/tests/__init__.py b/cloudkitty_tempest_plugin/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudkitty_tempest_plugin/tests/api/__init__.py b/cloudkitty_tempest_plugin/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudkitty_tempest_plugin/tests/api/base.py b/cloudkitty_tempest_plugin/tests/api/base.py new file mode 100644 index 0000000..1836124 --- /dev/null +++ b/cloudkitty_tempest_plugin/tests/api/base.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Objectif Libre +# +# 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 six + +from tempest import config +from tempest.lib import exceptions +import tempest.test + +from cloudkitty_tempest_plugin.services import client + +CONF = config.CONF + + +def skipIf(flag, reason): + def decorator(f): + def wrapper(self, *args, **kwargs): + if getattr(self, flag): + self.skipTest(reason) + else: + f(self, *args, **kwargs) + return wrapper + return decorator + + +class BaseRatingTest(tempest.test.BaseTestCase): + """Base test class for all Rating API tests.""" + + client_manager = client.Manager + + @classmethod + def setup_clients(cls): + super(BaseRatingTest, cls).setup_clients() + os_var = 'os_{}'.format(cls.credentials[0]) + cls.rating_client = getattr(cls, os_var).rating_client + + @classmethod + def setup_credentials(cls): + super(BaseRatingTest, cls).setup_credentials() + os_var = 'os_{}'.format(cls.credentials[0]) + project_id = getattr(cls, os_var).credentials.project_id + cls.skip_rating_tests = False + try: + cls.custom_identity_client = client.CustomIdentityClient() + cls.custom_identity_client.enable_rating(project_id) + except Exception: + cls.skip_rating_tests = True + + @classmethod + def resource_setup(cls): + super(BaseRatingTest, cls).resource_setup() + cls._created_resources = { + 'collector_mapping': list(), + 'hashmap_service': list(), + 'hashmap_field': list(), + 'hashmap_mapping': list(), + 'hashmap_group': list(), + 'hashmap_threshold': list(), + 'pyscript': list(), + } + + @classmethod + def resource_cleanup(cls): + super(BaseRatingTest, cls).resource_cleanup() + for method, item_ids in six.iteritems(cls._created_resources): + delete_method = 'delete_' + method + delete_method = getattr(cls.rating_client, delete_method) + for item_id in item_ids: + try: + delete_method(item_id) + except exceptions.NotFound: + pass diff --git a/cloudkitty_tempest_plugin/tests/api/test_cloudkitty_api.py b/cloudkitty_tempest_plugin/tests/api/test_cloudkitty_api.py new file mode 100644 index 0000000..6cd6760 --- /dev/null +++ b/cloudkitty_tempest_plugin/tests/api/test_cloudkitty_api.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Objectif Libre +# +# 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.common.utils import data_utils +from tempest.lib import decorators + +from cloudkitty_tempest_plugin.tests.api import base + + +class CloudkittyAdminAPITest(base.BaseRatingTest): + + credentials = ['admin'] + + @decorators.idempotent_id('9c1d4c27-6e7c-42d7-b663-d88f097b7131') + def test_get_collector_mappings(self): + self.rating_client.get_collector_mappings() + + def _find_item(self, haystack, needle, key, assert_method): + found = False + for item in haystack: + try: + if item[key] == needle: + found = True + except KeyError: + continue + assert_method(found) + + @decorators.idempotent_id('af902c86-6022-4b94-a716-ec7932d5ae78') + def test_create_get_delete_collector_mapping(self): + mapping = self.rating_client.create_collector_mapping( + collector=data_utils.rand_name('gnocchi'), + service=data_utils.rand_name('compute'), + ) + self._created_resources['collector_mapping'].append( + mapping['service'], + ) + mappings = self.rating_client.get_collector_mappings() + self._find_item(mappings['mappings'], + mapping['service'], + 'service', + self.assertTrue) + self.rating_client.delete_collector_mapping(mapping['service']) + mappings = self.rating_client.get_collector_mappings() + self._find_item(mappings['mappings'], + mapping['service'], + 'service', + self.assertFalse) + + @decorators.idempotent_id('3fd83647-3058-4450-9588-a528557585c5') + def test_get_collector_state(self): + collector = self.rating_client.get_collector_state( + collector=data_utils.rand_name('gnocchi'), + ) + self.assertFalse(collector['enabled']) + + @decorators.idempotent_id('71131104-fdae-43ec-9bed-c8d1d5ba7eb0') + def test_set_collector_state(self): + collector_name = data_utils.rand_name('gnocchi') + self.rating_client.set_collector_state( + collector=collector_name, + enabled=True, + ) + collector = self.rating_client.get_collector_state(collector_name) + self.assertTrue(collector['enabled']) + self.rating_client.set_collector_state( + collector=collector_name, + enabled=False, + ) + collector = self.rating_client.get_collector_state(collector_name) + self.assertFalse(collector['enabled']) + + @decorators.idempotent_id('fba44b6a-6ca4-4155-b5c6-c4eb2465e4fb') + def test_get_rating_modules(self): + modules = self.rating_client.get_rating_module() + self._find_item(modules['modules'], + 'hashmap', + 'module_id', + self.assertTrue) + self._find_item(modules['modules'], + 'pyscripts', + 'module_id', + self.assertTrue) + self._find_item(modules['modules'], + 'noop', + 'module_id', + self.assertTrue) + self.assertEqual( + 'hashmap', + self.rating_client.get_rating_module('hashmap')['module_id'], + ) + self.assertEqual( + 'pyscripts', + self.rating_client.get_rating_module('pyscripts')['module_id'], + ) + + @decorators.idempotent_id('7fc9e020-9547-4a66-a691-94cab7181358') + def test_update_rating_module(self): + self.rating_client.update_rating_module('hashmap', enabled=True) + module = self.rating_client.get_rating_module('hashmap') + self.assertTrue(module['enabled']) + self.rating_client.update_rating_module('hashmap', enabled=False) + module = self.rating_client.get_rating_module('hashmap') + self.assertFalse(module['enabled']) + + @decorators.idempotent_id('daeef22b-d52d-4e89-abb0-ae492e4648d4') + def test_reload_rating_modules(self): + self.rating_client.reload_rating_modules() + + @base.skipIf('skip_rating_tests', + 'Rating role was not given to CloudKitty') + @decorators.idempotent_id('e439019e-9e8a-4bcd-aa83-95bdba6e6115') + def test_get_rated_tenants(self): + rated_tenants = self.rating_client.get_rated_tenants()['body'] + self.assertGreater(len(rated_tenants), 0) + + +class CloudkittyPrimaryAPITest(base.BaseRatingTest): + + credentials = ['primary'] + + @decorators.idempotent_id('3285bccf-d043-4ad1-b64f-af4db8317cf9') + def test_get_config(self): + self.rating_client.get_config() + + @decorators.idempotent_id('43b03099-0493-4291-9749-85cd8d512811') + def test_get_services(self): + self.rating_client.get_service() + + @decorators.idempotent_id('64ecae87-0138-41bd-829f-91302dae7802') + def test_get_service(self): + self.rating_client.get_service('compute') + + @decorators.idempotent_id('cccbff8a-24b2-4251-8f7b-ea941d048b9d') + def test_report_summary(self): + self.rating_client.get_report_summary() + + @decorators.idempotent_id('2492dfd7-0688-4957-93ac-8c91933c28f5') + def test_report_total(self): + self.rating_client.get_report_total() + + @decorators.idempotent_id('e233139b-3c75-4b70-b1f5-0776ef32c916') + def test_get_storage_dataframes(self): + self.rating_client.get_storage_dataframes() diff --git a/cloudkitty_tempest_plugin/tests/api/test_hashmap_api.py b/cloudkitty_tempest_plugin/tests/api/test_hashmap_api.py new file mode 100644 index 0000000..1f360ab --- /dev/null +++ b/cloudkitty_tempest_plugin/tests/api/test_hashmap_api.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Objectif Libre +# +# 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.common.utils import data_utils +from tempest.lib import decorators + +from cloudkitty_tempest_plugin.tests.api import base + + +class CloudkittyHashmapAPITest(base.BaseRatingTest): + + credentials = ['admin'] + + @decorators.idempotent_id('7037a3f8-b462-4243-a0bc-ffa3b4700397') + def test_get_hashmap_rating_types(self): + self.rating_client.get_hashmap_mapping_types() + + def _setup_dummy_service(self): + service = self.rating_client.create_hashmap_service( + data_utils.rand_name('service'), + ) + self._created_resources['hashmap_service'].append( + service['service_id']) + return service['service_id'] + + @decorators.idempotent_id('9e968284-7209-46e1-9742-4882b6e2cf2f') + def test_create_delete_hashmap_service(self): + service_id = self._setup_dummy_service() + self.rating_client.delete_hashmap_service(service_id) + + @decorators.idempotent_id('9e9a67d1-e53d-46cf-8e13-8a332c40c32f') + def test_get_hashmap_services(self): + self.rating_client.get_hashmap_service() + + @decorators.idempotent_id('6c4260c0-8701-4959-b00e-4789d31715a7') + def test_get_hashmap_service(self): + service_id = self._setup_dummy_service() + self.rating_client.get_hashmap_service(service_id=service_id) + + def _setup_dummy_fields(self, service_id): + field_ids = list() + for i in range(3): + field = self.rating_client.create_hashmap_field( + data_utils.rand_name('hashmap_field'), + service_id, + ) + field_ids.append(field['field_id']) + self._created_resources['hashmap_field'].append(field['field_id']) + return field_ids + + @decorators.idempotent_id('974669f9-392e-4aec-8e15-d24db23f08d4') + def test_create_delete_hashmap_field(self): + service_id = self._setup_dummy_service() + field_ids = self._setup_dummy_fields(service_id) + for field_id in field_ids: + self.rating_client.delete_hashmap_field(field_id) + self.rating_client.delete_hashmap_service(service_id) + + @decorators.idempotent_id('9ae98590-ff55-47f9-885e-baa4da1957d1') + def test_get_hashmap_field(self): + service_id = self._setup_dummy_service() + self._setup_dummy_fields(service_id) + fields = self.rating_client.get_hashmap_fields(service_id) + for field in fields['fields']: + field_info = self.rating_client.get_hashmap_field( + field['field_id'], + ) + self.assertEqual(field_info['field_id'], field['field_id']) + self.assertEqual(field_info['service_id'], field['service_id']) + self.assertEqual(field_info['name'], field['name']) + + def _find_item(self, haystack, needle, key): + found = False + for item in haystack: + try: + if item[key] == needle: + found = True + except KeyError: + continue + self.assertTrue(found) + + @decorators.idempotent_id('4deb6914-7ba0-4219-a119-61b39bd58807') + def test_get_hashmap_mappings(self): + service_id = self._setup_dummy_service() + field_ids = self._setup_dummy_fields(service_id) + field_mapping = self.rating_client.create_hashmap_mapping( + field_id=field_ids[0], + value='dummy mapping', + ) + service_mapping = self.rating_client.create_hashmap_mapping( + service_id=service_id, + ) + self._created_resources['hashmap_mapping'].append( + field_mapping['mapping_id'], + ) + self._created_resources['hashmap_mapping'].append( + service_mapping['mapping_id'], + ) + + service_filtered_mappings = self.rating_client.get_hashmap_mappings( + service_id=service_id, + ) + field_filtered_mappings = self.rating_client.get_hashmap_mappings( + field_id=field_ids[0], + ) + self._find_item(service_filtered_mappings['mappings'], + service_mapping['mapping_id'], + 'mapping_id') + self._find_item(field_filtered_mappings['mappings'], + field_mapping['mapping_id'], + 'mapping_id') + + @decorators.idempotent_id('b7ad24e5-c72c-469a-886b-db43aab8f328') + def test_create_delete_hashmap_mapping(self): + service_id = self._setup_dummy_service() + field_ids = self._setup_dummy_fields(service_id) + mapping_ids = list() + for field_id in field_ids: + mapping = self.rating_client.create_hashmap_mapping( + field_id=field_id, + value='dummy mapping', + ) + self.assertEqual('dummy mapping', mapping['value']) + mapping_ids.append(mapping['mapping_id']) + mapping = self.rating_client.create_hashmap_mapping( + service_id=service_id, + ) + mapping_ids.append(mapping['mapping_id']) + self._created_resources['hashmap_mapping'] += mapping_ids + for mapping_id in mapping_ids: + self.rating_client.delete_hashmap_mapping(mapping_id) + + @decorators.idempotent_id('6a04634d-2d3a-406e-980a-e7c7c9cc081b') + def test_update_hashmap_mapping(self): + service_id = self._setup_dummy_service() + field_ids = self._setup_dummy_fields(service_id) + mapping = self.rating_client.create_hashmap_mapping( + field_id=field_ids[0], + value='dummy field', + ) + self._created_resources['hashmap_mapping'].append( + mapping['mapping_id'], + ) + self.assertEqual('dummy field', mapping['value']) + self.rating_client.update_hashmap_mapping( + mapping['mapping_id'], + value='new value', + ) + mapping = self.rating_client.get_hashmap_mapping( + mapping['mapping_id'] + ) + self.assertEqual('new value', mapping['value']) + + @decorators.idempotent_id('0f9200ab-146b-4349-a579-ce12062f465b') + def test_create_delete_hashmap_group(self): + group = self.rating_client.create_hashmap_group( + data_utils.rand_name('dummy_group'), + ) + self._created_resources['hashmap_group'].append(group['group_id']) + self.rating_client.delete_hashmap_group(group['group_id']) + + @decorators.idempotent_id('858a019a-fb64-4656-b7a6-c92917f641ab') + def test_get_hashmap_group(self): + group = self.rating_client.create_hashmap_group( + data_utils.rand_name('dummy_group'), + ) + self._created_resources['hashmap_group'].append(group['group_id']) + group_name = group['name'] + groups = self.rating_client.get_hashmap_group() + self._find_item(groups['groups'], group_name, 'name') + group = self.rating_client.get_hashmap_group(group['group_id']) + self.assertEqual(group['name'], group_name) + + @decorators.idempotent_id('98ca42dd-a9e2-477a-8e42-16389aed1f44') + def test_get_hashmap_mapping_group(self): + service_id = self._setup_dummy_service() + field_ids = self._setup_dummy_fields(service_id) + group = self.rating_client.create_hashmap_group( + data_utils.rand_name('dummy_group'), + ) + group_name = group['name'] + self._created_resources['hashmap_group'].append(group['group_id']) + mapping = self.rating_client.create_hashmap_mapping( + field_id=field_ids[0], + group_id=group['group_id'], + value='dummy mapping', + ) + self._created_resources['hashmap_mapping'].append( + mapping['mapping_id'], + ) + group = self.rating_client.get_hashmap_mapping_group( + mapping['mapping_id'], + ) + self.assertEqual(group['name'], group_name) + + @decorators.idempotent_id('92860fc8-596a-42fd-b0d5-97e0f5a7bd2c') + def test_get_hashmap_group_mappings(self): + service_id = self._setup_dummy_service() + field_ids = self._setup_dummy_fields(service_id) + group = self.rating_client.create_hashmap_group( + data_utils.rand_name('dummy_group'), + ) + self._created_resources['hashmap_group'].append(group['group_id']) + for i in range(3): + mapping = self.rating_client.create_hashmap_mapping( + field_id=field_ids[i], + group_id=group['group_id'], + value='dummy mapping {}'.format(i), + ) + self._created_resources['hashmap_mapping'].append( + mapping['mapping_id'] + ) + mappings = self.rating_client.get_hashmap_group_mappings( + group['group_id'], + ) + for i in range(3): + self._find_item(mappings['mappings'], + 'dummy mapping {}'.format(i), + 'value') + + @decorators.idempotent_id('d2b3dba3-91df-4aa7-9ae2-96d971df2dbf') + def test_create_delete_update_hashmap_threshold(self): + service_id = self._setup_dummy_service() + field_ids = self._setup_dummy_fields(service_id) + thresholds = list() + thresholds.append(self.rating_client.create_hashmap_threshold( + service_id=service_id, + level=0.95, + cost=12, + )) + self._created_resources['hashmap_threshold'].append( + thresholds[-1]['threshold_id'] + ) + for idx, field_id in enumerate(field_ids): + thresholds.append(self.rating_client.create_hashmap_threshold( + field_id=field_id, + level=0.95 * (idx + 1), + cost=12 * (idx + 1), + )) + self._created_resources['hashmap_threshold'].append( + thresholds[-1]['threshold_id'] + ) + for threshold in thresholds: + self.rating_client.update_hashmap_threshold( + threshold['threshold_id'], + cost=42, + level=1.23, + ) + for threshold in thresholds: + self.rating_client.delete_hashmap_threshold( + threshold['threshold_id'], + ) + + @decorators.idempotent_id('dc463432-3b92-44ac-8caf-b789857f9db7') + def test_get_hashmap_threshold(self): + service_id = self._setup_dummy_service() + self._setup_dummy_fields(service_id) + created_threshold = self.rating_client.create_hashmap_threshold( + service_id=service_id, + level=1.95, + cost=42, + ) + self._created_resources['hashmap_threshold'].append( + created_threshold['threshold_id'], + ) + threshold = self.rating_client.get_hashmap_threshold( + created_threshold['threshold_id'], + ) + self.assertEqual(threshold['level'], created_threshold['level']) + self.rating_client.delete_hashmap_threshold( + created_threshold['threshold_id'], + ) + + @decorators.idempotent_id('d04caad5-eb18-40ce-817e-13c257633cca') + def test_get_hashmap_thresholds(self): + service_id = self._setup_dummy_service() + field_ids = self._setup_dummy_fields(service_id) + group = self.rating_client.create_hashmap_group( + data_utils.rand_name('dummy_group'), + ) + self._created_resources['hashmap_group'].append(group['group_id']) + field_threshold = self.rating_client.create_hashmap_threshold( + group_id=group['group_id'], + field_id=field_ids[0], + level=1.95, + cost=42, + ) + self._created_resources['hashmap_threshold'].append( + field_threshold['threshold_id'], + ) + service_threshold = self.rating_client.create_hashmap_threshold( + service_id=service_id, + group_id=group['group_id'], + level=1.95, + cost=42, + ) + self._created_resources['hashmap_threshold'].append( + service_threshold['threshold_id'], + ) + thresholds = self.rating_client.get_hashmap_thresholds( + service_id=service_id, + ) + self._find_item(thresholds['thresholds'], + group['group_id'], + 'group_id') + thresholds = self.rating_client.get_hashmap_thresholds( + field_id=field_ids[0], + ) + self._find_item(thresholds['thresholds'], + group['group_id'], + 'group_id') + thresholds = self.rating_client.get_hashmap_thresholds( + group_id=group['group_id'] + ) + self._find_item(thresholds['thresholds'], service_id, 'service_id') + self._find_item(thresholds['thresholds'], field_ids[0], 'field_id') diff --git a/cloudkitty_tempest_plugin/tests/api/test_pyscript_api.py b/cloudkitty_tempest_plugin/tests/api/test_pyscript_api.py new file mode 100644 index 0000000..deffe96 --- /dev/null +++ b/cloudkitty_tempest_plugin/tests/api/test_pyscript_api.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Objectif Libre +# +# 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.common.utils import data_utils +from tempest.lib import decorators +from tempest.lib import exceptions as lib_exc + +from cloudkitty_tempest_plugin.tests.api import base + +SCRIPT_DATA_ONE = """ +def dumbfunc(): + return 0 + +data = dumbfunc() +""" + +SCRIPT_DATA_TWO = """ +def dumbfunc(): + return 0 + +data = dumbfunc() + 2 +""" + + +class CloudkittyPyscriptAPITest(base.BaseRatingTest): + + credentials = ['admin'] + + @decorators.idempotent_id('2015c966-b707-40f7-b84d-9aa6550b9e41') + def test_get_pyscripts(self): + self.rating_client.get_pyscripts() + + @decorators.idempotent_id('9e78cccc-ca85-42ce-8648-4cd9682375df') + def test_create_update_delete_pyscript(self): + pyscript = self.rating_client.create_pyscript( + data_utils.rand_name('dummy_script'), + SCRIPT_DATA_ONE, + ) + self._created_resources['pyscript'].append(pyscript['script_id']) + self.assertEqual(pyscript['data'], SCRIPT_DATA_ONE) + self.rating_client.update_pyscript(pyscript['script_id'], + data=SCRIPT_DATA_TWO, + name=pyscript['name']) + pyscript = self.rating_client.get_pyscript(pyscript['script_id']) + self.assertEqual(pyscript['data'], SCRIPT_DATA_TWO) + self.rating_client.delete_pyscript(pyscript['script_id']) + + @decorators.idempotent_id('3fbaf8b4-c472-4509-8d73-55dc4a87a442') + def test_get_pyscript(self): + pyscript = self.rating_client.create_pyscript( + data_utils.rand_name('dummy_script'), + SCRIPT_DATA_ONE, + ) + self.assertEqual(pyscript['data'], SCRIPT_DATA_ONE) + self._created_resources['pyscript'].append(pyscript['script_id']) + pyscript = self.rating_client.get_pyscript(pyscript['script_id']) + + +class CloudkittyPyscriptAPITestNegative(base.BaseRatingTest): + + credentials = ['admin'] + + @decorators.idempotent_id('999c97cc-1d71-43b8-988f-d89b8fac4040') + @decorators.attr(type=['negative']) + def test_update_script_negative(self): + pyscript = self.rating_client.create_pyscript( + data_utils.rand_name('dummy_script'), + SCRIPT_DATA_ONE, + ) + self._created_resources['pyscript'].append(pyscript['script_id']) + fake_id = data_utils.rand_uuid() + self.assertRaises(lib_exc.NotFound, + self.rating_client.get_pyscript, + fake_id) diff --git a/cloudkitty_tempest_plugin/tests/scenario/__init__.py b/cloudkitty_tempest_plugin/tests/scenario/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..72497c1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +# 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. + +keystoneauth1>=2.18.0 # Apache-2.0 +oslo.config>=3.18.0 # Apache-2.0 +oslo.serialization>=2.14.0 # Apache-2.0 +pbr>=2.0 # Apache-2.0 +python-keystoneclient>=3.6.0 # Apache-2.0 +six>=1.9.0 # MIT +tempest>=15.0.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..212fe63 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +name = cloudkitty_tempest_plugin +summary = Tempest plugin for CloudKitty +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + +[files] +packages = + cloudkitty_tempest_plugin + +[entry_points] +tempest.test_plugins = + cloudkitty_tests = cloudkitty_tempest_plugin.plugin:CloudkittyTempestPlugin diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..056c16c --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +# 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..3887e0d --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,15 @@ +# 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. + +hacking>=0.12.0,<0.13 # Apache-2.0 + +coverage>=4.0,!=4.4 # Apache-2.0 +python-subunit>=0.0.18 # Apache-2.0/BSD +sphinx>=1.6.2 # BSD +oslotest>=1.10.0 # Apache-2.0 +stestr>=1.0.0 # Apache-2.0 +testtools>=1.4.0 # MIT +openstackdocstheme>=1.11.0 # Apache-2.0 +# releasenotes +reno>=1.8.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2d0b04a --- /dev/null +++ b/tox.ini @@ -0,0 +1,43 @@ +[tox] +minversion = 2.0 +envlist = py34,py27,pypy,pep8 +skipsdist = True + +[testenv] +usedevelop = True +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} + PYTHONWARNINGS=default::DeprecationWarning + OS_STDOUT_CAPTURE=1 + OS_STDERR_CAPTURE=1 + OS_TEST_TIMEOUT=60 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run {posargs} + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +setenv = + VIRTUAL_ENV={envdir} + PYTHON=coverage run --source cloudkitty_tempest_plugin --parallel-mode +commands = + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + +[testenv:debug] +commands = oslo_debug_helper {posargs} + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build