murano-tempest-plugin/murano_tempest_tests/tests/functional/common/utils.py

551 lines
19 KiB
Python

# 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 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 oslo_serialization import jsonutils as json
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)