From 6aa5f13cab0cbe215d3777eec5429e4704b5d2d7 Mon Sep 17 00:00:00 2001 From: zhurong Date: Sat, 28 Jul 2018 15:41:21 +0800 Subject: [PATCH] Move functional test to murano-tempest-plugin Change-Id: I19debb3ca8cceb99d21a5bc500264b18e6bce8ce --- .../tests/functional/__init__.py | 0 .../tests/functional/common/__init__.py | 0 .../tests/functional/common/tempest_utils.py | 49 ++ .../tests/functional/common/utils.py | 550 ++++++++++++++++++ .../functional/common/zip_utils_mixin.py | 30 + .../tests/functional/integration/__init__.py | 0 .../integration/integration_base.py | 157 +++++ .../Classes/MistralShowcaseApp.yaml | 32 + .../Resources/TestEcho_MistralWorkflow.yaml | 24 + .../manifest.yaml | 10 + .../Classes/PolicyEnforcementTestApp.yaml | 48 ++ .../manifest.yaml | 10 + .../integration/rules_murano_action.txt | 18 + .../integration/rules_murano_system.txt | 7 + .../functional/integration/test_mistral.py | 62 ++ .../functional/integration/test_policy_enf.py | 89 +++ requirements.txt | 9 + 17 files changed, 1095 insertions(+) create mode 100644 murano_tempest_tests/tests/functional/__init__.py create mode 100644 murano_tempest_tests/tests/functional/common/__init__.py create mode 100644 murano_tempest_tests/tests/functional/common/tempest_utils.py create mode 100644 murano_tempest_tests/tests/functional/common/utils.py create mode 100644 murano_tempest_tests/tests/functional/common/zip_utils_mixin.py create mode 100644 murano_tempest_tests/tests/functional/integration/__init__.py create mode 100644 murano_tempest_tests/tests/functional/integration/integration_base.py create mode 100644 murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/Classes/MistralShowcaseApp.yaml create mode 100644 murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/Resources/TestEcho_MistralWorkflow.yaml create mode 100644 murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/manifest.yaml create mode 100644 murano_tempest_tests/tests/functional/integration/io.murano.apps.test.PolicyEnforcementTestApp/Classes/PolicyEnforcementTestApp.yaml create mode 100644 murano_tempest_tests/tests/functional/integration/io.murano.apps.test.PolicyEnforcementTestApp/manifest.yaml create mode 100644 murano_tempest_tests/tests/functional/integration/rules_murano_action.txt create mode 100644 murano_tempest_tests/tests/functional/integration/rules_murano_system.txt create mode 100644 murano_tempest_tests/tests/functional/integration/test_mistral.py create mode 100644 murano_tempest_tests/tests/functional/integration/test_policy_enf.py diff --git a/murano_tempest_tests/tests/functional/__init__.py b/murano_tempest_tests/tests/functional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano_tempest_tests/tests/functional/common/__init__.py b/murano_tempest_tests/tests/functional/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano_tempest_tests/tests/functional/common/tempest_utils.py b/murano_tempest_tests/tests/functional/common/tempest_utils.py new file mode 100644 index 0000000..3675eac --- /dev/null +++ b/murano_tempest_tests/tests/functional/common/tempest_utils.py @@ -0,0 +1,49 @@ +# Copyright (c) 2015 OpenStack Foundation +# +# 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 congressclient.v1.client as cclient +from keystoneauth1 import identity +from keystoneauth1 import session as ksasession +import keystoneclient.v3 as ksclient +from tempest import config + +import murano_tempest_tests.tests.functional.common.utils as common_utils + +CONF = config.CONF + + +class TempestDeployTestMixin(common_utils.DeployTestMixin): + """Overrides methods to use tempest configuration.""" + + @staticmethod + @common_utils.memoize + def keystone_client(): + return ksclient.Client(username=CONF.auth.admin_username, + password=CONF.auth.admin_password, + tenant_name=CONF.auth.admin_project_name, + auth_url=CONF.identity.uri_v3) + + @staticmethod + @common_utils.memoize + def congress_client(): + auth = identity.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, + user_domain_name=CONF.auth.admin_domain_name, + project_domain_name=CONF.auth.admin_domain_name) + session = ksasession.Session(auth=auth) + return cclient.Client(session=session, + service_type='policy') diff --git a/murano_tempest_tests/tests/functional/common/utils.py b/murano_tempest_tests/tests/functional/common/utils.py new file mode 100644 index 0000000..487be0d --- /dev/null +++ b/murano_tempest_tests/tests/functional/common/utils.py @@ -0,0 +1,550 @@ +# Copyright (c) 2015 OpenStack Foundation +# +# 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 collections +import contextlib +import json +import os +import random +import socket +import telnetlib +import time + +from heatclient import client as heatclient +from keystoneclient import exceptions as ks_exceptions +import keystoneclient.v3 as ksclient +from muranoclient import client as mclient +import muranoclient.common.exceptions as exceptions +from muranoclient.glance import client as glare_client +from oslo_log import log as logging +from tempest import config +import yaml + +import murano_tempest_tests.tests.functional.common.zip_utils_mixin \ + as zip_utils + + +CONF = config.CONF + +LOG = logging.getLogger(__name__) + + +SessionState = collections.namedtuple('SessionState', [ + 'OPENED', 'DEPLOYING', 'DEPLOYED', 'DEPLOY_FAILURE', 'DELETING', + 'DELETE_FAILURE' +])( + OPENED='opened', + DEPLOYING='deploying', + DEPLOYED='deployed', + DEPLOY_FAILURE='deploy failure', + DELETING='deleting', + DELETE_FAILURE='delete failure' +) + + +@contextlib.contextmanager +def ignored(*exceptions): + try: + yield + except exceptions: + pass + + +def memoize(f): + """Saves result of decorated function to cache + + Decorator, which saves result of a decorated function + to cache. + TTL for cache is 1800 sec + + :param f: decorated function + :return: saved result of a decorated function + """ + cache = {} + + def decorated_function(*args): + if args in cache: + if time.time() - cache[args][1] < 1800: + return cache[args][0] + else: + cache[args] = (f(*args), time.time()) + return cache[args][0] + else: + cache[args] = (f(*args), time.time()) + return cache[args][0] + + return decorated_function + + +class DeployTestMixin(zip_utils.ZipUtilsMixin): + + @staticmethod + @memoize + def keystone_client(): + return ksclient.Client(username=CONF.auth.admin_username, + password=CONF.auth.admin_password, + tenant_name=CONF.auth.admin_project_name, + auth_url=CONF.identity.uri_v3) + + @classmethod + @memoize + def heat_client(cls): + heat_url = cls.keystone_client().service_catalog.url_for( + service_type='orchestration', endpoint_type='publicURL') + return heatclient.Client('1', + endpoint=heat_url, + token=cls.keystone_client().auth_token) + + @classmethod + @memoize + def murano_client(cls): + murano_url = cls.get_murano_url() + if CONF.application_catalog.glare_backend: + glare_endpoint = "http://127.0.0.1:9494" + artifacts_client = glare_client.Client( + endpoint=glare_endpoint, + token=cls.keystone_client().auth_token, + insecure=False, key_file=None, ca_file=None, cert_file=None, + type_name="murano", type_version=1) + else: + artifacts_client = None + return mclient.Client('1', + artifacts_client=artifacts_client, + endpoint=murano_url, + token=cls.keystone_client().auth_token) + +# --------------------------Specific test methods------------------------------ + + @classmethod + def deploy_apps(cls, name, *apps): + """Create and deploy environment. + + :param name: Murano environment name + :param apps: App(s), described in JSON format + :return: Murano environment + """ + environment = cls.murano_client().environments.create({'name': name}) + cls.init_list("_environments") + cls._environments.append(environment) + session = cls.murano_client().sessions.configure(environment.id) + for app in apps: + cls.murano_client().services.post( + environment.id, + path='/', + data=app, + session_id=session.id) + cls.murano_client().sessions.deploy(environment.id, session.id) + return environment + + @classmethod + def wait_for_final_status(cls, environment, timeout=300): + """Function for wait final status of environment. + + :param environment: Murano environment. + :param timeout: Timeout for waiting environment to get any status + excluding DEPLOYING state + """ + start_time = time.time() + status = environment.manager.get(environment.id).status + while SessionState.DEPLOYING == status: + if time.time() - start_time > timeout: + err_msg = ('Deployment not finished in {amount} seconds' + .format(amount=timeout)) + LOG.error(err_msg) + raise RuntimeError(err_msg) + time.sleep(5) + status = environment.manager.get(environment.id).status + dep = cls.murano_client().deployments.list(environment.id) + reports = cls.murano_client().deployments.reports(environment.id, + dep[0].id) + return status, ", ".join([r.text for r in reports]) + +# -----------------------------Reports methods--------------------------------- + + @classmethod + def get_last_deployment(cls, environment): + """Gets last deployment of Murano environment. + + :param environment: Murano environment + :return: + """ + deployments = cls.murano_client().deployments.list(environment.id) + return deployments[0] + + @classmethod + def get_deployment_report(cls, environment, deployment): + """Gets reports for environment with specific deployment. + + :param environment: Murano environment. + :param deployment: Murano deployment for certain environment + :return: + """ + history = '' + report = cls.murano_client().deployments.reports( + environment.id, deployment.id) + for status in report: + history += '\t{0} - {1}\n'.format(status.created, status.text) + return history + + @classmethod + def _log_report(cls, environment): + """Used for logging reports on failures. + + :param environment: Murano environment. + """ + deployment = cls.get_last_deployment(environment) + try: + details = deployment.result['result']['details'] + LOG.warning('Details:\n {details}'.format(details=details)) + except Exception as e: + LOG.error(e) + report = cls.get_deployment_report(environment, deployment) + LOG.debug('Report:\n {report}\n'.format(report=report)) + +# -----------------------------Service methods--------------------------------- + + @classmethod + def add_service(cls, environment, data, session, to_dict=False): + """This function adds a specific service to environment. + + :param environment: Murano environment + :param data: JSON with specific servive to add into + :param session: Session that is open for environment + :param to_dict: If True - returns a JSON object with service + If False - returns a specific class + """ + + LOG.debug('Added service:\n {data}'.format(data=data)) + service = cls.murano_client().services.post(environment.id, + path='/', data=data, + session_id=session.id) + if to_dict: + return cls._convert_service(service) + else: + return service + + @classmethod + def services_list(cls, environment): + """Get a list of environment services. + + :param environment: Murano environment + :return: List of objects + """ + return cls.murano_client().services.list(environment.id) + + @classmethod + def get_service(cls, environment, service_name, to_dict=True): + """Get a service with specific name from environment. + + :param to_dict: Convert service to JSON or not to convert + :param environment: Murano environment + :param service_name: Service name + :return: JSON or object + """ + for service in cls.services_list(environment): + if service.name == service_name: + return cls._convert_service(service) if to_dict else service + + @classmethod + def _convert_service(cls, service): + """Converts a to JSON object. + + :param service: object + :return: JSON object + """ + component = service.to_dict() + component = json.dumps(component) + return yaml.safe_load(component) + + @classmethod + def get_service_id(cls, service): + """Gets id on object. + + :param service: object + :return: ID of the Service + """ + serv = cls._convert_service(service) + serv_id = serv['?']['id'] + return serv_id + + @classmethod + def delete_service(cls, environment, session, service): + """This function removes a specific service from environment. + + :param environment: Murano environment + :param session: Session fir urano environment + :param service: object + :return: Updated murano environment + """ + cls.murano_client().services.delete( + environment.id, path='/{0}'.format(cls.get_service_id(service)), + session_id=session.id) + LOG.debug('Service with name {0} from environment {1} successfully ' + 'removed'.format(environment.name, service.name)) + updated_env = cls.get_environment(environment) + return updated_env + + +# -----------------------------Packages methods-------------------------------- + + @classmethod + def upload_package(cls, package_name, body, app): + """Uploads a .zip package with parameters to Murano. + + :param package_name: Package name in Murano repository + :param body: Categories, tags, etc. + e.g. { + "categories": ["Application Servers"], + "tags": ["tag"] + } + :param app: Correct .zip archive with the application + :return: Package + """ + files = {'{0}'.format(package_name): open(app, 'rb')} + package = cls.murano_client().packages.create(body, files) + cls.init_list("_packages") + cls._packages.append(package) + return package + +# ------------------------------Common methods--------------------------------- + + @classmethod + def rand_name(cls, name='murano'): + """Generates random string. + + :param name: Basic name + :return: + """ + return name + str(random.randint(1, 0x7fffffff)) + + @classmethod + def init_list(cls, list_name): + if not hasattr(cls, list_name): + setattr(cls, list_name, []) + + @classmethod + def get_murano_url(cls): + try: + url = cls.keystone_client().service_catalog.url_for( + service_type='application-catalog', endpoint_type='publicURL') + except ks_exceptions.EndpointNotFound: + url = CONF.murano.murano_url + LOG.warning("Murano endpoint not found in Keystone. " + "Using CONF.") + return url if 'v1' not in url else "/".join( + url.split('/')[:url.split('/').index('v1')]) + + @classmethod + def verify_connection(cls, ip, port): + """Try to connect to specific ip:port with telnet. + + :param ip: Ip that you want to check + :param port: Port that you want to check + :return: :raise RuntimeError: + """ + tn = telnetlib.Telnet(ip, port) + tn.write('GET / HTTP/1.0\n\n') + try: + buf = tn.read_all() + LOG.debug('Data:\n {data}'.format(data=buf)) + if len(buf) != 0: + tn.sock.sendall(telnetlib.IAC + telnetlib.NOP) + return + else: + raise RuntimeError('Resource at {0}:{1} not exist'. + format(ip, port)) + except socket.error as e: + LOG.error('Socket Error: {error}'.format(error=e)) + + @classmethod + def get_ip_by_appname(cls, environment, appname): + """Returns ip of instance with a deployed application using app name. + + :param environment: Murano environment + :param appname: Application name or substring of application name + :return: + """ + for service in environment.services: + if appname in service['name']: + return service['instance']['floatingIpAddress'] + + @classmethod + def get_ip_by_instance_name(cls, environment, inst_name): + """Returns ip of instance using instance name. + + :param environment: Murano environment + :param name: String, which is substring of name of instance or name of + instance + :return: + """ + for service in environment.services: + if inst_name in service['instance']['name']: + return service['instance']['floatingIpAddress'] + + @classmethod + def get_k8s_ip_by_instance_name(cls, environment, inst_name, service_name): + """Returns ip of specific kubernetes node (gateway, master, minion). + + Search depends on service name of kubernetes and names of spawned + instances + :param environment: Murano environment + :param inst_name: Name of instance or substring of instance name + :param service_name: Name of Kube Cluster application in Murano + environment + :return: Ip of Kubernetes instances + """ + for service in environment.services: + if service_name in service['name']: + if "gateway" in inst_name: + for gateway in service['gatewayNodes']: + if inst_name in gateway['instance']['name']: + LOG.debug(gateway['instance']['floatingIpAddress']) + return gateway['instance']['floatingIpAddress'] + elif "master" in inst_name: + LOG.debug(service['masterNode']['instance'][ + 'floatingIpAddress']) + return service['masterNode']['instance'][ + 'floatingIpAddress'] + elif "minion" in inst_name: + for minion in service['minionNodes']: + if inst_name in minion['instance']['name']: + LOG.debug(minion['instance']['floatingIpAddress']) + return minion['instance']['floatingIpAddress'] + +# -----------------------------Cleanup methods--------------------------------- + + @classmethod + def purge_uploaded_packages(cls): + """Cleanup for uploaded packages.""" + cls.init_list("_packages") + try: + for pkg in cls._packages: + with ignored(Exception): + cls.murano_client().packages.delete(pkg.id) + finally: + cls._packages = [] + cls.init_list("_package_files") + try: + for pkg_file in cls._package_files: + os.remove(pkg_file) + finally: + cls._package_files = [] + + @classmethod + def purge_environments(cls): + """Cleanup for created environments.""" + cls.init_list("_environments") + try: + for env in cls._environments: + with ignored(Exception): + LOG.debug('Processing cleanup for environment {0} ({1})'. + format(env.name, env.id)) + cls.environment_delete(env.id) + cls.purge_stacks(env.id) + time.sleep(5) + finally: + cls._environments = [] + + @classmethod + def purge_stacks(cls, environment_id): + stack = cls._get_stack(environment_id) + if not stack: + return + else: + cls.heat_client().stacks.delete(stack.id) + +# -----------------------Methods for environment CRUD-------------------------- + + @classmethod + def create_environment(cls, name=None): + """Creates Murano environment with random name. + + + :param name: Environment name + :return: Murano environment + """ + if not name: + name = cls.rand_name('MuranoTe') + environment = cls.murano_client().environments.create({'name': name}) + cls._environments.append(environment) + return environment + + @classmethod + def get_environment(cls, environment): + """Refresh variable. + + :param environment: Murano environment. + :return: Murano environment. + """ + return cls.murano_client().environments.get(environment.id) + + @classmethod + def environment_delete(cls, environment_id, timeout=180): + """Remove Murano environment. + + :param environment_id: ID of Murano environment + :param timeout: Timeout to environment get deleted + :return: :raise RuntimeError: + """ + try: + cls.murano_client().environments.delete(environment_id) + + start_time = time.time() + while time.time() - start_time < timeout: + try: + cls.murano_client().environments.get(environment_id) + except exceptions.HTTPNotFound: + LOG.debug('Environment with id {0} successfully deleted.'. + format(environment_id)) + return + err_msg = ('Environment {0} was not deleted in {1} seconds'. + format(environment_id, timeout)) + LOG.error(err_msg) + raise RuntimeError(err_msg) + except Exception as exc: + LOG.debug('Environment with id {0} going to be abandoned.'. + format(environment_id)) + LOG.exception(exc) + cls.murano_client().environments.delete(environment_id, + abandon=True) + +# -----------------------Methods for session actions--------------------------- + + @classmethod + def create_session(cls, environment): + return cls.murano_client().sessions.configure(environment.id) + + @classmethod + def delete_session(cls, environment, session): + return cls.murano_client().sessions.delete(environment.id, session.id) + + +# -------------------------------Heat methods---------------------------------- + + @classmethod + def _get_stack(cls, environment_id): + + for stack in cls.heat_client().stacks.list(): + stack_description = ( + cls.heat_client().stacks.get(stack.id).description) + if not stack_description: + err_msg = ("Stack {0} description is empty".format(stack.id)) + LOG.error(err_msg) + raise RuntimeError(err_msg) + if environment_id in stack_description: + return stack + + @classmethod + def get_stack_template(cls, stack): + return cls.heat_client().stacks.template(stack.stack_name) diff --git a/murano_tempest_tests/tests/functional/common/zip_utils_mixin.py b/murano_tempest_tests/tests/functional/common/zip_utils_mixin.py new file mode 100644 index 0000000..464f398 --- /dev/null +++ b/murano_tempest_tests/tests/functional/common/zip_utils_mixin.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015 OpenStack Foundation +# +# 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 +import zipfile + + +class ZipUtilsMixin(object): + @staticmethod + def zip_dir(parent_dir, dir): + abs_path = os.path.join(parent_dir, dir) + path_len = len(abs_path) + 1 + zip_file = abs_path + ".zip" + with zipfile.ZipFile(zip_file, "w") as zf: + for dir_name, _, files in os.walk(abs_path): + for filename in files: + fn = os.path.join(dir_name, filename) + zf.write(fn, fn[path_len:]) + return zip_file diff --git a/murano_tempest_tests/tests/functional/integration/__init__.py b/murano_tempest_tests/tests/functional/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano_tempest_tests/tests/functional/integration/integration_base.py b/murano_tempest_tests/tests/functional/integration/integration_base.py new file mode 100644 index 0000000..7f5a0df --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/integration_base.py @@ -0,0 +1,157 @@ +# Copyright (c) 2015 OpenStack Foundation, 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 os +import uuid + +from keystoneclient import exceptions as keystone_exceptions +import mistralclient.api.client as mistralclient +import testresources +import testtools + +import murano_tempest_tests.tests.functional.common.tempest_utils \ + as tempest_utils +import murano_tempest_tests.tests.functional.common.utils as utils + + +class MistralIntegration(testtools.TestCase, testtools.testcase.WithAttributes, + testresources.ResourcedTestCase, + tempest_utils.TempestDeployTestMixin): + + @classmethod + @utils.memoize + def mistral_client(cls): + keystone_client = cls.keystone_client() + + endpoint_type = 'publicURL' + service_type = 'workflowv2' + + mistral_url = keystone_client.service_catalog.url_for( + service_type=service_type, + endpoint_type=endpoint_type) + + auth_token = keystone_client.auth_token + + return mistralclient.client(mistral_url=mistral_url, + auth_url=keystone_client.auth_url, + project_id=keystone_client.tenant_id, + endpoint_type=endpoint_type, + service_type=service_type, + auth_token=auth_token, + user_id=keystone_client.user_id) + + @classmethod + def upload_mistral_showcase_app(cls): + app_dir = 'io.murano.apps.test.MistralShowcaseApp' + zip_file_path = cls.zip_dir(os.path.dirname(__file__), app_dir) + cls.init_list("_package_files") + cls._package_files.append(zip_file_path) + return cls.upload_package( + 'MistralShowcaseApp', + {"categories": ["Web"], "tags": ["tag"]}, + zip_file_path) + + @staticmethod + def _create_env_body(): + return { + "name": "Mistral_environment", + "?": { + "type": "io.murano.apps.test.MistralShowcaseApp", + "id": str(uuid.uuid4()) + } + } + + +class CongressIntegration(testtools.TestCase, + testtools.testcase.WithAttributes, + testresources.ResourcedTestCase, + tempest_utils.TempestDeployTestMixin): + + @classmethod + def _create_policy_req(cls, policy_name): + return {'abbreviation': None, 'kind': None, + 'name': policy_name, + 'description': None} + + @classmethod + def _upload_policy_enf_app(cls): + app_dir = 'io.murano.apps.test.PolicyEnforcementTestApp' + zip_file_path = cls.zip_dir(os.path.dirname(__file__), app_dir) + cls.init_list("_package_files") + cls._package_files.append(zip_file_path) + return cls.upload_package( + 'PolicyEnforcementTestApp', + {"categories": ["Web"], "tags": ["tag"]}, + zip_file_path) + + @classmethod + def _create_policy(cls, policy_names, kind=None): + for name in policy_names: + policy_req = {"name": name} + if kind: + policy_req["kind"] = kind + with utils.ignored(keystone_exceptions.Conflict): + cls.congress_client().create_policy(policy_req) + + rules = [] + rules_file = os.path.join( + os.path.dirname(__file__), + "rules_" + name + ".txt") + + if os.path.isfile(rules_file): + with open(rules_file) as f: + rules = [rule.strip() for rule in f.readlines() + if rule.strip()] + for rule in rules: + with utils.ignored(keystone_exceptions.Conflict): + cls.congress_client().create_policy_rule(name, + {'rule': rule}) + + def _create_test_app(self, flavor, key): + """Application create request body + + Deployment is expected to fail earlier due to policy violation. + Not existing image prevents real deployment to happen + in case that test goes wrong way. + + :param flavor: instance image flavor + :param key: key name + """ + + return { + "instance": { + "flavor": flavor, + "keyname": key, + "image": "not_existing_image", + "assignFloatingIp": True, + "?": { + "type": "io.murano.resources.LinuxMuranoInstance", + "id": str(uuid.uuid4()) + }, + "name": "testMurano" + }, + "name": "teMurano", + "?": { + "type": "io.murano.apps.test.PolicyEnforcementTestApp", + "id": str(uuid.uuid4()) + } + } + + def _check_deploy_failure(self, post_body, expected_text): + environment_name = 'PolicyEnfTestEnv' + uuid.uuid4().hex[:5] + env = self.deploy_apps(environment_name, post_body) + status = self.wait_for_final_status(env) + self.assertIn("failure", status[0], "Unexpected status : " + status[0]) + self.assertIn(expected_text, status[1].lower(), + "Unexpected status : " + status[1]) diff --git a/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/Classes/MistralShowcaseApp.yaml b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/Classes/MistralShowcaseApp.yaml new file mode 100644 index 0000000..cfe4a07 --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/Classes/MistralShowcaseApp.yaml @@ -0,0 +1,32 @@ +Namespaces: + =: io.murano.apps.test + std: io.murano + sys: io.murano.system + + +Name: MistralShowcaseApp + +Extends: std:Application + +Properties: + name: + Contract: $.string().notNull() + + mistralClient: + Contract: $.class(sys:MistralClient) + Usage: Runtime + + +Methods: + initialize: + Body: + - $environment: $.find(std:Environment).require() + - $this.mistralClient: new(sys:MistralClient, $environment) + + deploy: + Body: + - $resources: new('io.murano.system.Resources') + - $workflow: $resources.string('TestEcho_MistralWorkflow.yaml') + - $.mistralClient.upload(definition => $workflow) + - $output: $.mistralClient.run(name => 'test_echo', inputs => dict(input_1 => input_1_value)) + - $this.find(std:Environment).reporter.report($this, $output.get('out_3')) \ No newline at end of file diff --git a/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/Resources/TestEcho_MistralWorkflow.yaml b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/Resources/TestEcho_MistralWorkflow.yaml new file mode 100644 index 0000000..b09959b --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/Resources/TestEcho_MistralWorkflow.yaml @@ -0,0 +1,24 @@ +version: '2.0' + +test_echo: + type: direct + input: + - input_1 + output: + out_1: <% $.task1_output_1 %> + out_2: <% $.task2_output_2 %> + out_3: <% $.input_1 %> + tasks: + my_echo_test: + action: std.echo output='just a string' + publish: + task1_output_1: 'task1_output_1_value' + task1_output_2: 'task1_output_2_value' + on-success: + - my_echo_test_2 + + my_echo_test_2: + action: std.echo output='just a string' + publish: + task2_output_1: 'task2_output_1_value' + task2_output_2: 'task2_output_2_value' \ No newline at end of file diff --git a/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/manifest.yaml b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/manifest.yaml new file mode 100644 index 0000000..3eb290a --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.MistralShowcaseApp/manifest.yaml @@ -0,0 +1,10 @@ +Format: 1.0 +Type: Application +FullName: io.murano.apps.test.MistralShowcaseApp +Name: MistralShowcaseApp +Description: | + MistralShowcaseApp. +Author: 'Mirantis, Inc' +Tags: [Servlets, Server, Pages, Java] +Classes: + io.murano.apps.test.MistralShowcaseApp: MistralShowcaseApp.yaml diff --git a/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.PolicyEnforcementTestApp/Classes/PolicyEnforcementTestApp.yaml b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.PolicyEnforcementTestApp/Classes/PolicyEnforcementTestApp.yaml new file mode 100644 index 0000000..c08c38b --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.PolicyEnforcementTestApp/Classes/PolicyEnforcementTestApp.yaml @@ -0,0 +1,48 @@ +Namespaces: + =: io.murano.apps.test + std: io.murano + res: io.murano.resources + sys: io.murano.system + + +Name: PolicyEnforcementTestApp + +Extends: std:Application + +Properties: + name: + Contract: $.string().notNull() + + instance: + Contract: $.class(res:Instance).notNull() + + host: + Contract: $.string() + Usage: Out + + user: + Contract: $.string() + Usage: Out + +Methods: + initialize: + Body: + - $._environment: $.find(std:Environment).require() + + deploy: + Body: + - If: not $.getAttr(deployed, false) + Then: + - $._environment.reporter.report($this, 'Creating VM') + - $securityGroupIngress: + - ToPort: 22 + FromPort: 22 + IpProtocol: tcp + External: true + - $._environment.securityGroupManager.addGroupIngress($securityGroupIngress) + - $.instance.deploy() + - $resources: new(sys:Resources) + - $._environment.reporter.report($this, 'Test VM is installed') + - $.host: $.instance.ipAddresses[0] + - $.user: 'root' + - $.setAttr(deployed, true) diff --git a/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.PolicyEnforcementTestApp/manifest.yaml b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.PolicyEnforcementTestApp/manifest.yaml new file mode 100644 index 0000000..5eac9c6 --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/io.murano.apps.test.PolicyEnforcementTestApp/manifest.yaml @@ -0,0 +1,10 @@ +Format: 1.0 +Type: Application +FullName: io.murano.apps.test.PolicyEnforcementTestApp +Name: PolicyEnforcementTestApp +Description: | + This is a simple test app with a single VM for policy enforcement testing purposes. +Author: 'Hewlett-Packard' +Tags: [test] +Classes: + io.murano.apps.test.PolicyEnforcementTestApp: PolicyEnforcementTestApp.yaml diff --git a/murano_tempest_tests/tests/functional/integration/rules_murano_action.txt b/murano_tempest_tests/tests/functional/integration/rules_murano_action.txt new file mode 100644 index 0000000..442c277 --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/rules_murano_action.txt @@ -0,0 +1,18 @@ +action("deleteEnv") + +murano:states-(eid,st) :- deleteEnv(eid), murano:states( eid, st) + +murano:parent_types-(tid, type) :- deleteEnv(eid), murano:connected(eid, tid),murano:parent_types(tid,type) +murano:parent_types-(eid, type) :- deleteEnv(eid), murano:parent_types(eid,type) + +murano:properties-(oid, pn, pv) :- deleteEnv(eid), murano:connected( eid, oid),murano:properties(oid, pn, pv) +murano:properties-(eid, pn, pv) :- deleteEnv(eid), murano:properties(eid, pn, pv) + +murano:objects-(oid, pid, ot) :- deleteEnv(eid), murano:connected(eid, oid), murano:objects(oid, pid, ot) +murano:objects-(eid, tnid, ot) :- deleteEnv(eid), murano:objects(eid, tnid, ot) + +murano:relationships-(sid,tid, rt) :- deleteEnv(eid), murano:connected( eid, sid), murano:relationships( sid, tid, rt) +murano:relationships-(eid,tid, rt) :- deleteEnv(eid), murano:relationships( eid, tid, rt) + +murano:connected-(tid, tid2) :- deleteEnv(eid), murano:connected(eid, tid), murano:connected(tid,tid2) +murano:connected-(eid,tid) :- deleteEnv(eid), murano:connected(eid,tid) \ No newline at end of file diff --git a/murano_tempest_tests/tests/functional/integration/rules_murano_system.txt b/murano_tempest_tests/tests/functional/integration/rules_murano_system.txt new file mode 100644 index 0000000..640a62e --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/rules_murano_system.txt @@ -0,0 +1,7 @@ +missing_key("") +invalid_flavor_name("really.bad.flavor") +predeploy_errors(eid, obj_id, msg):-murano:objects(obj_id, pid, type), murano_env_of_object(obj_id, eid), murano:properties(obj_id, "flavor", flavor_name), invalid_flavor_name(flavor_name), murano:properties(obj_id, "name", obj_name), concat(obj_name, ": bad flavor", msg) +predeploy_errors(eid, obj_id, msg):-murano:objects(obj_id, pid, type), murano_env_of_object(obj_id, eid), murano:properties(obj_id, "keyname", key_name), missing_key(key_name), murano:properties(obj_id, "name", obj_name), concat(obj_name, ": missing key", msg) +murano_env_of_object(oid,eid):-murano:connected(eid,oid), murano:objects(eid,tid,"io.murano.Environment") +bad_flavor_synonyms("horrible.flavor") +predeploy_modify(eid, obj_id, action):-murano:objects(obj_id, pid, type), murano_env_of_object(obj_id, eid), murano:properties(obj_id, "flavor", flavor_name), bad_flavor_synonyms(flavor_name), concat("set-property: {object_id: ", obj_id, first_part ), concat(first_part, ", prop_name: flavor, value: really.bad.flavor}", action) diff --git a/murano_tempest_tests/tests/functional/integration/test_mistral.py b/murano_tempest_tests/tests/functional/integration/test_mistral.py new file mode 100644 index 0000000..b855a25 --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/test_mistral.py @@ -0,0 +1,62 @@ +# Copyright (c) 2015 OpenStack Foundation +# +# 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 uuid + +from nose.plugins.attrib import attr as tag + +import murano_tempest_tests.tests.functional.common.utils as common_utils +import murano_tempest_tests.tests.functional.integration.integration_base \ + as core + + +class MistralTest(core.MistralIntegration): + + @classmethod + def setUpClass(cls): + super(MistralTest, cls).setUpClass() + + try: + # Upload the Murano test package. + cls.upload_mistral_showcase_app() + + except Exception: + cls.tearDownClass() + raise + + @classmethod + def tearDownClass(cls): + with common_utils.ignored(Exception): + cls.purge_environments() + with common_utils.ignored(Exception): + cls.purge_uploaded_packages() + + @tag('all', 'coverage') + def test_deploy_package_success(self): + # Test expects successful deployment and one output: input_1_value. + + # Create env json string. + post_body = self._create_env_body() + + environment_name = 'Mistral_environment' + uuid.uuid4().hex[:5] + + # Deploy the environment. + env = self.deploy_apps(environment_name, post_body) + + status = self.wait_for_final_status(env) + + self.assertIn("ready", status[0], + "Unexpected status : " + status[0]) + self.assertIn("input_1_value", status[1], + "Unexpected output value: " + status[1]) diff --git a/murano_tempest_tests/tests/functional/integration/test_policy_enf.py b/murano_tempest_tests/tests/functional/integration/test_policy_enf.py new file mode 100644 index 0000000..dbcfde4 --- /dev/null +++ b/murano_tempest_tests/tests/functional/integration/test_policy_enf.py @@ -0,0 +1,89 @@ +# Copyright (c) 2015 OpenStack Foundation +# +# 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 muranoclient.common.exceptions as murano_exceptions +from nose.plugins.attrib import attr as tag + +import murano_tempest_tests.tests.functional.common.utils as common_utils +import murano_tempest_tests.tests.functional.integration.integration_base \ + as core + + +class PolicyEnforcementTest(core.CongressIntegration): + + @classmethod + def setUpClass(cls): + super(PolicyEnforcementTest, cls).setUpClass() + + cls._create_policy(["murano", "murano_system"]) + cls._create_policy(["murano_action"], kind="action") + + with common_utils.ignored(murano_exceptions.HTTPInternalServerError): + cls._upload_policy_enf_app() + + @classmethod + def tearDownClass(cls): + cls.purge_uploaded_packages() + + def tearDown(self): + super(PolicyEnforcementTest, self).tearDown() + self.purge_environments() + + @tag('all', 'coverage') + def test_deploy_policy_fail_key(self): + """Test expects failure due to empty key name. + + In rules_murano_system.txt file are defined congress + rules preventing deploy environment where instances + have empty keyname property. In other words admin + prevented spawn instance without assigned key pair. + """ + + self._check_deploy_failure( + self._create_test_app(key='', + flavor='m1.small'), + 'missing key') + + @tag('all', 'coverage') + def test_deploy_policy_fail_flavor(self): + """Test expects failure due to blacklisted flavor + + In rules_murano_system.txt file are defined congress + rules preventing deploy environment where instances + have flavor property set to 'really.bad.flavor'. + """ + + self._check_deploy_failure( + self._create_test_app(flavor='really.bad.flavor', + key='test-key'), + 'bad flavor') + + @tag('all', 'coverage') + def test_set_property_policy(self): + """Tests environment modification by policy + + In rules_murano_system.txt file are defined congress + rules changing flavor property. There are defined + synonyms for 'really.bad.flavor'. One of such synonyms + is 'horrible.flavor' Environment is modified prior deployment. + The synonym name 'horrible.flavor' is set to original + value 'really.bad.flavor' and then deployment is aborted + because instances of 'really.bad.flavor' are prevented + to be deployed like for the test above. + """ + + self._check_deploy_failure( + self._create_test_app(key="test-key", + flavor="horrible.flavor"), + "bad flavor") diff --git a/requirements.txt b/requirements.txt index 5595dd1..662777d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,12 @@ oslo.utils>=3.33.0 # Apache-2.0 testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 +nose>=1.3.7 # LGPL +testresources>=2.0.0 # Apache-2.0/BSD + +python-keystoneclient>=3.8.0 # Apache-2.0 +python-heatclient>=1.10.0 # Apache-2.0 +python-neutronclient>=6.7.0 # Apache-2.0 +python-muranoclient>=0.8.2 # Apache-2.0 +python-congressclient<2000,>=1.9.0 # Apache-2.0 +python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0