murano/murano/tests/functional/common/utils.py

547 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 contextlib
import json
import os
import random
import re
import socket
import telnetlib
import time
from heatclient import client as heatclient
from keystoneclient import exceptions as ks_exceptions
import keystoneclient.v2_0 as keystoneclientv2
import keystoneclient.v3 as keystoneclientv3
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
import yaml
from murano.services import states
import murano.tests.functional.common.zip_utils_mixin as zip_utils
import murano.tests.functional.engine.config as cfg
CONF = cfg.cfg.CONF
LOG = logging.getLogger(__name__)
@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):
cfg.load_config()
# -----------------------------Clients methods---------------------------------
@staticmethod
@memoize
def keystone_client():
region = CONF.murano.region_name
if re.match(".*/v3/?$", CONF.murano.auth_url):
ksclient = keystoneclientv3
else:
ksclient = keystoneclientv2
return ksclient.Client(username=CONF.murano.user,
password=CONF.murano.password,
tenant_name=CONF.murano.tenant,
auth_url=CONF.murano.auth_url,
region_name=region)
@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.murano.packages_service == "glare":
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 states.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)