diff --git a/MANIFEST.in b/MANIFEST.in index 75dc99dc95..7649ada879 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -48,6 +48,8 @@ include sahara/service/edp/resources/*.xml include sahara/service/edp/resources/*.jar include sahara/service/edp/resources/launch_command.py include sahara/swift/resources/*.xml +include sahara/tests/scenario/testcase.py.mako +recursive-include sahara/tests/scenario/templates *.json include sahara/tests/unit/plugins/vanilla/hadoop2/resources/*.txt include sahara/tests/unit/plugins/mapr/utils/resources/*.topology include sahara/tests/unit/plugins/mapr/utils/resources/*.json diff --git a/etc/scenario/simple-testcase.yaml b/etc/scenario/simple-testcase.yaml new file mode 100644 index 0000000000..1070b98d9f --- /dev/null +++ b/etc/scenario/simple-testcase.yaml @@ -0,0 +1,17 @@ +credentials: + os_username: admin + os_password: nova + os_tenant: admin + os_auth_url: http://localhost:5000/v2.0 + +network: + private_network: private + public_network: public + +clusters: + - plugin_name: vanilla + plugin_version: 2.6.0 + image: sahara-juno-vanilla-2.6.0-ubuntu-14.04 + - plugin_name: hdp + plugin_version: 2.0.6 + image: f3c4a228-9ba4-41f1-b100-a0587689d4dd diff --git a/sahara/tests/scenario/__init__.py b/sahara/tests/scenario/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sahara/tests/scenario/base.py b/sahara/tests/scenario/base.py new file mode 100644 index 0000000000..3aa46cb223 --- /dev/null +++ b/sahara/tests/scenario/base.py @@ -0,0 +1,196 @@ +# Copyright (c) 2015 Mirantis 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 __future__ import print_function +import functools +import glob +import json +import os +import time + +import fixtures +from oslo_utils import excutils +from tempest_lib import base +from tempest_lib import exceptions as exc + +from sahara.tests.scenario import clients +from sahara.tests.scenario import utils + +DEFAULT_TEMPLATES_PATH = ( + 'sahara/tests/scenario/templates/%(plugin_name)s/%(hadoop_version)s') + + +def errormsg(message): + def decorator(fct): + @functools.wraps(fct) + def wrapper(*args, **kwargs): + try: + return fct(*args, **kwargs) + except Exception: + with excutils.save_and_reraise_exception(): + print(message) + return wrapper + return decorator + + +class BaseTestCase(base.BaseTestCase): + @classmethod + def setUpClass(cls): + super(BaseTestCase, cls).setUpClass() + cls.network = None + cls.credentials = None + cls.testcase = None + + def setUp(self): + super(BaseTestCase, self).setUp() + self._init_clients() + self.plugin_opts = { + 'plugin_name': self.testcase['plugin_name'], + 'hadoop_version': self.testcase['plugin_version'] + } + self.template_path = DEFAULT_TEMPLATES_PATH % self.plugin_opts + + def _init_clients(self): + username = self.credentials['os_username'] + password = self.credentials['os_password'] + tenant_name = self.credentials['os_tenant'] + auth_url = self.credentials['os_auth_url'] + sahara_url = self.credentials['sahara_url'] + + self.sahara = clients.SaharaClient(username=username, + api_key=password, + project_name=tenant_name, + auth_url=auth_url, + sahara_url=sahara_url) + self.nova = clients.NovaClient(username=username, + api_key=password, + project_id=tenant_name, + auth_url=auth_url) + self.neutron = clients.NeutronClient(username=username, + password=password, + tenant_name=tenant_name, + auth_url=auth_url) + + def create_cluster(self): + ngs = self._create_node_group_templates() + cl_tmpl_id = self._create_cluster_template(ngs) + cl_id = self._create_cluster(cl_tmpl_id) + self._poll_cluster_status(cl_id) + + @errormsg("Create node group templates failed") + def _create_node_group_templates(self): + ng_id_map = {} + floating_ip_pool = None + if self.network['type'] == 'neutron': + floating_ip_pool = self.neutron.get_network_id( + self.network['public_network']) + elif not self.network['auto_assignment_floating_ip']: + floating_ip_pool = self.network['public_network'] + + node_groups = [] + if self.testcase.get('node_group_templates'): + for ng in self.testcase['node_group_templates']: + node_groups.append(ng) + else: + templates_path = os.path.join(self.template_path, + 'node_group_template_*.json') + for template_file in glob.glob(templates_path): + with open(template_file) as data: + node_groups.append(json.load(data)) + + for ng in node_groups: + kwargs = dict(ng) + kwargs.update(self.plugin_opts) + kwargs['name'] = utils.rand_name(kwargs['name']) + kwargs['floating_ip_pool'] = floating_ip_pool + ng_id = self.__create_node_group_template(**kwargs) + ng_id_map[ng['name']] = ng_id + + return ng_id_map + + @errormsg("Create cluster template failed") + def _create_cluster_template(self, node_groups): + template = None + if self.testcase.get('cluster_template'): + template = self.testcase['cluster_template'] + else: + template_path = os.path.join(self.template_path, + 'cluster_template.json') + with open(template_path) as data: + template = json.load(data) + + kwargs = dict(template) + ngs = kwargs['node_group_templates'] + del kwargs['node_group_templates'] + kwargs['node_groups'] = [] + for ng, count in ngs.items(): + kwargs['node_groups'].append({ + 'name': utils.rand_name(ng), + 'node_group_template_id': node_groups[ng], + 'count': count}) + + kwargs.update(self.plugin_opts) + kwargs['name'] = utils.rand_name(kwargs['name']) + if self.network['type'] == 'neutron': + kwargs['net_id'] = self.neutron.get_network_id( + self.network['private_network']) + + return self.__create_cluster_template(**kwargs) + + @errormsg("Create cluster failed") + def _create_cluster(self, cluster_template_id): + if self.testcase.get('cluster'): + kwargs = dict(self.testcase['cluster']) + else: + kwargs = {} # default template + + kwargs.update(self.plugin_opts) + kwargs['name'] = utils.rand_name(kwargs.get('name', 'test')) + kwargs['cluster_template_id'] = cluster_template_id + kwargs['default_image_id'] = self.nova.get_image_id( + self.testcase['image']) + + return self.__create_cluster(**kwargs) + + def _poll_cluster_status(self, cluster_id): + # TODO(sreshetniak): make timeout configurable + with fixtures.Timeout(1800, gentle=True): + while True: + status = self.sahara.get_cluster_status(cluster_id) + if status == 'Active': + break + if status == 'Error': + raise exc.TempestException("Cluster in %s state" % status) + time.sleep(3) + + # sahara client ops + + def __create_node_group_template(self, *args, **kwargs): + id = self.sahara.create_node_group_template(*args, **kwargs) + if not self.testcase['retain_resources']: + self.addCleanup(self.sahara.delete_node_group_template, id) + return id + + def __create_cluster_template(self, *args, **kwargs): + id = self.sahara.create_cluster_template(*args, **kwargs) + if not self.testcase['retain_resources']: + self.addCleanup(self.sahara.delete_cluster_template, id) + return id + + def __create_cluster(self, *args, **kwargs): + id = self.sahara.create_cluster(*args, **kwargs) + if not self.testcase['retain_resources']: + self.addCleanup(self.sahara.delete_cluster, id) + return id diff --git a/sahara/tests/scenario/clients.py b/sahara/tests/scenario/clients.py new file mode 100644 index 0000000000..3d96eb476e --- /dev/null +++ b/sahara/tests/scenario/clients.py @@ -0,0 +1,109 @@ +# Copyright (c) 2015 Mirantis 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. + +import time + +import fixtures +from neutronclient.neutron import client as neutron_client +from novaclient import client as nova_client +from oslo_utils import uuidutils +from saharaclient.api import base as saharaclient_base +from saharaclient import client as sahara_client +from tempest_lib import exceptions as exc + + +class Client(object): + def is_resource_deleted(self, method, *args, **kwargs): + raise NotImplementedError + + def delete_resource(self, method, *args, **kwargs): + # TODO(sreshetniak): make timeout configurable + with fixtures.Timeout(300, gentle=True): + while True: + if self.is_resource_deleted(method, *args, **kwargs): + break + time.sleep(5) + + +class SaharaClient(Client): + def __init__(self, *args, **kwargs): + self.sahara_client = sahara_client.Client('1.1', *args, **kwargs) + + def create_node_group_template(self, *args, **kwargs): + data = self.sahara_client.node_group_templates.create(*args, **kwargs) + return data.id + + def delete_node_group_template(self, node_group_template_id): + return self.delete_resource( + self.sahara_client.node_group_templates.delete, + node_group_template_id) + + def create_cluster_template(self, *args, **kwargs): + data = self.sahara_client.cluster_templates.create(*args, **kwargs) + return data.id + + def delete_cluster_template(self, cluster_template_id): + return self.delete_resource( + self.sahara_client.cluster_templates.delete, + cluster_template_id) + + def create_cluster(self, *args, **kwargs): + data = self.sahara_client.clusters.create(*args, **kwargs) + return data.id + + def delete_cluster(self, cluster_id): + return self.delete_resource( + self.sahara_client.clusters.delete, + cluster_id) + + def get_cluster_status(self, cluster_id): + data = self.sahara_client.clusters.get(cluster_id) + return str(data.status) + + def is_resource_deleted(self, method, *args, **kwargs): + try: + method(*args, **kwargs) + except saharaclient_base.APIException as ex: + return ex.error_code == 404 + + return False + + +class NovaClient(Client): + def __init__(self, *args, **kwargs): + self.nova_client = nova_client.Client('1.1', *args, **kwargs) + + def get_image_id(self, image_name): + if uuidutils.is_uuid_like(image_name): + return image_name + for image in self.nova_client.images.list(): + if image.name == image_name: + return image.id + + raise exc.NotFound(image_name) + + +class NeutronClient(Client): + def __init__(self, *args, **kwargs): + self.neutron_client = neutron_client.Client('2.0', *args, **kwargs) + + def get_network_id(self, network_name): + if uuidutils.is_uuid_like(network_name): + return network_name + networks = self.neutron_client.list_networks(name=network_name) + networks = networks['networks'] + if len(networks) < 1: + raise exc.NotFound(network_name) + return networks[0]['id'] diff --git a/sahara/tests/scenario/runner.py b/sahara/tests/scenario/runner.py new file mode 100755 index 0000000000..7f717e140f --- /dev/null +++ b/sahara/tests/scenario/runner.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +# Copyright (c) 2015 Mirantis 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 __future__ import print_function +import argparse +import os +import sys +import tempfile + +from mako import template as mako_template +import yaml + +from sahara.openstack.common import fileutils + + +TEST_TEMPLATE_PATH = 'sahara/tests/scenario/testcase.py.mako' + + +def set_defaults(config): + # set up credentials + config['credentials'] = config.get('credentials', {}) + creds = config['credentials'] + creds['os_username'] = creds.get('os_username', 'admin') + creds['os_password'] = creds.get('os_password', 'nova') + creds['os_tenant'] = creds.get('os_tenant', 'admin') + creds['os_auth_url'] = creds.get('os_auth_url', + 'http://localhost:5000/v2.0') + creds['sahara_url'] = creds.get('sahara_url', None) + + # set up network + config['network'] = config.get('network', {}) + net = config['network'] + net['type'] = net.get('type', 'neutron') + net['private_network'] = net.get('private_network', 'private') + net['auto_assignment_floating_ip'] = net.get('auto_assignment_floating_ip', + False) + net['public_network'] = net.get('public_network', 'public') + + # set up tests parameters + for testcase in config['clusters']: + testcase['class_name'] = "".join([ + testcase['plugin_name'], + testcase['plugin_version'].replace('.', '_')]) + testcase['retain_resources'] = testcase.get('retain_resources', False) + + +def main(): + # parse args + parser = argparse.ArgumentParser(description="Scenario tests runner.") + parser.add_argument('scenario_file', help="Path to scenario file.") + args = parser.parse_args() + scenario_file = args.scenario_file + + # parse config + with open(scenario_file, 'r') as yaml_file: + config = yaml.load(yaml_file) + + set_defaults(config) + credentials = config['credentials'] + network = config['network'] + testcases = config['clusters'] + + # create testcase file + test_template = mako_template.Template(filename=TEST_TEMPLATE_PATH) + testcase_data = test_template.render(testcases=testcases, + credentials=credentials, + network=network) + + test_dir_path = tempfile.mkdtemp() + print("The generated test file located at: %s" % test_dir_path) + fileutils.write_to_tempfile(testcase_data, prefix='test_', suffix='.py', + path=test_dir_path) + + # run tests + os.environ['DISCOVER_DIRECTORY'] = test_dir_path + return_code = os.system('bash tools/pretty_tox.sh') + sys.exit(return_code) + + +if __name__ == '__main__': + main() diff --git a/sahara/tests/scenario/templates/hdp/2.0.6/cluster_template.json b/sahara/tests/scenario/templates/hdp/2.0.6/cluster_template.json new file mode 100644 index 0000000000..6a160c1104 --- /dev/null +++ b/sahara/tests/scenario/templates/hdp/2.0.6/cluster_template.json @@ -0,0 +1,7 @@ +{ + "name": "hdp-206", + "node_group_templates": { + "hdp-master": 1, + "hdp-worker": 3 + } +} diff --git a/sahara/tests/scenario/templates/hdp/2.0.6/node_group_template_master.json b/sahara/tests/scenario/templates/hdp/2.0.6/node_group_template_master.json new file mode 100644 index 0000000000..8dc2c8c26e --- /dev/null +++ b/sahara/tests/scenario/templates/hdp/2.0.6/node_group_template_master.json @@ -0,0 +1,16 @@ +{ + "name": "hdp-master", + "flavor_id": "3", + "node_processes": [ + "NAMENODE", + "SECONDARY_NAMENODE", + "ZOOKEEPER_SERVER", + "AMBARI_SERVER", + "HISTORYSERVER", + "RESOURCEMANAGER", + "GANGLIA_SERVER", + "NAGIOS_SERVER", + "OOZIE_SERVER" + ], + "auto_security_group" : true +} diff --git a/sahara/tests/scenario/templates/hdp/2.0.6/node_group_template_worker.json b/sahara/tests/scenario/templates/hdp/2.0.6/node_group_template_worker.json new file mode 100644 index 0000000000..6c14a0cc64 --- /dev/null +++ b/sahara/tests/scenario/templates/hdp/2.0.6/node_group_template_worker.json @@ -0,0 +1,15 @@ +{ + "name": "hdp-worker", + "flavor_id": "3", + "node_processes": [ + "HDFS_CLIENT", + "DATANODE", + "ZOOKEEPER_CLIENT", + "MAPREDUCE2_CLIENT", + "YARN_CLIENT", + "NODEMANAGER", + "PIG", + "OOZIE_CLIENT" + ], + "auto_security_group" : true +} diff --git a/sahara/tests/scenario/templates/vanilla/2.6.0/cluster_template.json b/sahara/tests/scenario/templates/vanilla/2.6.0/cluster_template.json new file mode 100644 index 0000000000..9e5b67a70b --- /dev/null +++ b/sahara/tests/scenario/templates/vanilla/2.6.0/cluster_template.json @@ -0,0 +1,7 @@ +{ + "name": "vanilla-26", + "node_group_templates": { + "vanilla-master": 1, + "vanilla-worker": 3 + } +} diff --git a/sahara/tests/scenario/templates/vanilla/2.6.0/node_group_template_master.json b/sahara/tests/scenario/templates/vanilla/2.6.0/node_group_template_master.json new file mode 100644 index 0000000000..bc86815fbb --- /dev/null +++ b/sahara/tests/scenario/templates/vanilla/2.6.0/node_group_template_master.json @@ -0,0 +1,11 @@ +{ + "name": "vanilla-master", + "flavor_id": "3", + "node_processes": [ + "namenode", + "resourcemanager", + "historyserver", + "oozie" + ], + "auto_security_group": true +} diff --git a/sahara/tests/scenario/templates/vanilla/2.6.0/node_group_template_worker.json b/sahara/tests/scenario/templates/vanilla/2.6.0/node_group_template_worker.json new file mode 100644 index 0000000000..80491dedbe --- /dev/null +++ b/sahara/tests/scenario/templates/vanilla/2.6.0/node_group_template_worker.json @@ -0,0 +1,9 @@ +{ + "name": "vanilla-worker", + "flavor_id": "3", + "node_processes": [ + "datanode", + "nodemanager" + ], + "auto_security_group": true +} diff --git a/sahara/tests/scenario/testcase.py.mako b/sahara/tests/scenario/testcase.py.mako new file mode 100644 index 0000000000..700b7ff1e0 --- /dev/null +++ b/sahara/tests/scenario/testcase.py.mako @@ -0,0 +1,18 @@ +from sahara.tests.scenario import base + +% for testcase in testcases: + ${make_testcase(testcase)} +% endfor + +<%def name="make_testcase(testcase)"> +class ${testcase['class_name']}TestCase(base.BaseTestCase): + @classmethod + def setUpClass(cls): + super(${testcase['class_name']}TestCase, cls).setUpClass() + cls.credentials = ${credentials} + cls.network = ${network} + cls.testcase = ${testcase} + + def test_plugin(self): + self.create_cluster() + diff --git a/sahara/tests/scenario/utils.py b/sahara/tests/scenario/utils.py new file mode 100644 index 0000000000..fcc2fb6c1f --- /dev/null +++ b/sahara/tests/scenario/utils.py @@ -0,0 +1,24 @@ +# Copyright (c) 2015 Mirantis 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_utils import uuidutils + + +def rand_name(name=''): + rand_data = uuidutils.generate_uuid()[:8] + if name: + return '%s-%s' % (name, rand_data) + else: + return rand_data diff --git a/test-requirements.txt b/test-requirements.txt index 433d39357f..e1b4edabfb 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,6 +4,7 @@ hacking>=0.10.0,<0.11 +Mako>=0.4.0 MySQL-python bashate>=0.2 # Apache-2.0 coverage>=3.6 diff --git a/tox.ini b/tox.ini index 7e00b8afd2..a1d67bf864 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,10 @@ setenv = DISCOVER_DIRECTORY=sahara/tests/integration commands = bash tools/pretty_tox.sh '{posargs}' +[testenv:scenario] +setenv = VIRTUALENV={envdir} +commands = python {toxinidir}/sahara/tests/scenario/runner.py "{posargs}" + [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}'