Move functional test to murano-tempest-plugin
Change-Id: I19debb3ca8cceb99d21a5bc500264b18e6bce8ce
This commit is contained in:
parent
3b4376e848
commit
6aa5f13cab
|
@ -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')
|
|
@ -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 <Service>
|
||||
"""
|
||||
|
||||
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 <Service> 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 <Service> 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 <Service> to JSON object.
|
||||
|
||||
:param service: <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 <Service> object.
|
||||
|
||||
:param service: <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: <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 <Environment> 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)
|
|
@ -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
|
|
@ -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])
|
|
@ -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'))
|
|
@ -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'
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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])
|
|
@ -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")
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue