fuel-qa/fuelweb_test/helpers/decorators.py
Dennis Dmitriev 69d1790be1 Increase timeout for getting diagnostic snapshot up to 10 min
5 min sometimes is not enough for preparing a diagnostic snapshot.

Change-Id: Ia0b6c647c57a26cce4dd0d58d14af1b547d48f79
Closes-Bug:#1410210
2015-07-27 19:24:51 +03:00

514 lines
21 KiB
Python

# Copyright 2013 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import functools
import inspect
import json
import os
from subprocess import call
import requests
import sys
import time
import traceback
from urlparse import urlparse
from devops.helpers import helpers
from fuelweb_test.helpers.checkers import check_action_logs
from fuelweb_test.helpers.checkers import check_repo_managment
from fuelweb_test.helpers.checkers import check_stats_on_collector
from fuelweb_test.helpers.checkers import check_stats_private_info
from fuelweb_test.helpers.checkers import count_stats_on_collector
from proboscis import SkipTest
from proboscis.asserts import assert_equal
from fuelweb_test import logger
from fuelweb_test import settings
from fuelweb_test.helpers.regenerate_repo import CustomRepo
from fuelweb_test.helpers.utils import get_current_env
from fuelweb_test.helpers.utils import pull_out_logs_via_ssh
from fuelweb_test.helpers.utils import store_astute_yaml
from fuelweb_test.helpers.utils import store_packages_json
from fuelweb_test.helpers.utils import timestat
def save_logs(url, path, auth_token=None, chunk_size=1024):
logger.info('Saving logs to "%s" file', path)
headers = {}
if auth_token is not None:
headers['X-Auth-Token'] = auth_token
stream = requests.get(url, headers=headers, stream=True)
if stream.status_code != 200:
logger.error("%s %s: %s", stream.status_code, stream.reason,
stream.content)
return
with open(path, 'wb') as fp:
for chunk in stream.iter_content(chunk_size=chunk_size):
if chunk:
fp.write(chunk)
fp.flush()
def log_snapshot_after_test(func):
"""Generate diagnostic snapshot after the end of the test.
- Show test case method name and scenario from docstring.
- Create a diagnostic snapshot of environment in cases:
- if the test case passed;
- if error occurred in the test case.
- Fetch logs from master node if creating the diagnostic
snapshot has failed.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
logger.info("\n" + "<" * 5 + "#" * 30 + "[ {} ]"
.format(func.__name__) + "#" * 30 + ">" * 5 + "\n{}"
.format(''.join(func.__doc__)))
try:
result = func(*args, **kwargs)
except SkipTest:
raise SkipTest()
except Exception as test_exception:
exc_trace = sys.exc_traceback
name = 'error_%s' % func.__name__
description = "Failed in method '%s'." % func.__name__
if args[0].env is not None:
try:
create_diagnostic_snapshot(args[0].env,
"fail", name)
except:
logger.error("Fetching of diagnostic snapshot failed: {0}".
format(traceback.format_exc()))
try:
admin_remote = args[0].env.d_env.get_admin_remote()
pull_out_logs_via_ssh(admin_remote, name)
except:
logger.error("Fetching of raw logs failed: {0}".
format(traceback.format_exc()))
finally:
logger.debug(args)
try:
args[0].env.make_snapshot(snapshot_name=name[-50:],
description=description,
is_make=True)
except:
logger.error("Error making the environment snapshot:"
" {0}".format(traceback.format_exc()))
raise test_exception, None, exc_trace
else:
if settings.ALWAYS_CREATE_DIAGNOSTIC_SNAPSHOT:
if args[0].env is None:
logger.warning("Can't get diagnostic snapshot: "
"unexpected class is decorated.")
return result
try:
args[0].env.resume_environment()
create_diagnostic_snapshot(args[0].env, "pass",
func.__name__)
except:
logger.error("Fetching of diagnostic snapshot failed: {0}".
format(traceback.format_exc()))
return result
return wrapper
def json_parse(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
response = func(*args, **kwargs)
return json.loads(response.read())
return wrapped
def upload_manifests(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
try:
if settings.UPLOAD_MANIFESTS:
logger.info("Uploading new manifests from %s" %
settings.UPLOAD_MANIFESTS_PATH)
environment = get_current_env(args)
if not environment:
logger.warning("Can't upload manifests: method of "
"unexpected class is decorated.")
return result
remote = environment.d_env.get_admin_remote()
remote.execute('rm -rf /etc/puppet/modules/*')
remote.upload(settings.UPLOAD_MANIFESTS_PATH,
'/etc/puppet/modules/')
logger.info("Copying new site.pp from %s" %
settings.SITEPP_FOR_UPLOAD)
remote.execute("cp %s /etc/puppet/manifests" %
settings.SITEPP_FOR_UPLOAD)
if settings.SYNC_DEPL_TASKS:
remote.execute("fuel release --sync-deployment-tasks"
" --dir /etc/puppet/")
except Exception:
logger.error("Could not upload manifests")
raise
return result
return wrapper
def update_packages(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if not settings.UPDATE_FUEL:
return result
try:
environment = get_current_env(args)
if not environment:
logger.warning("Can't update packages: method of "
"unexpected class is decorated.")
return result
remote = environment.d_env.get_admin_remote()
if settings.UPDATE_FUEL_MIRROR:
for url in settings.UPDATE_FUEL_MIRROR:
repo_url = urlparse(url)
cut_dirs = len(repo_url.path.strip('/').split('/'))
download_cmd = ('wget --recursive --no-parent'
' --no-verbose --reject "index'
'.html*,*.gif" --exclude-directories'
' "{pwd}/repocache" '
'--directory-prefix {path} -nH'
' --cut-dirs={cutd} {url}').\
format(pwd=repo_url.path.rstrip('/'),
path=settings.UPDATE_FUEL_PATH,
cutd=cut_dirs, url=repo_url.geturl())
return_code = call(download_cmd, shell=True)
assert_equal(return_code, 0, 'Mirroring of remote'
' packages '
'repository failed')
centos_files_count, ubuntu_files_count = \
environment.admin_actions.upload_packages(
local_packages_dir=settings.UPDATE_FUEL_PATH,
centos_repo_path=settings.LOCAL_MIRROR_CENTOS,
ubuntu_repo_path=settings.LOCAL_MIRROR_UBUNTU)
if centos_files_count == 0:
return result
# Add temporary repo with new packages to YUM configuration
conf_file = '/etc/yum.repos.d/temporary.repo'
cmd = ("echo -e '[temporary]\nname=temporary\nbaseurl=file://{0}/"
"\ngpgcheck=0\npriority=1' > {1}").format(
settings.LOCAL_MIRROR_CENTOS, conf_file)
environment.execute_remote_cmd(remote, cmd, exit_code=0)
update_command = 'yum clean expire-cache; yum update -y -d3'
result = remote.execute(update_command)
logger.debug('Result of "yum update" command on master node: '
'{0}'.format(result))
assert_equal(int(result['exit_code']), 0,
'Packages update failed, '
'inspect logs for details')
environment.execute_remote_cmd(remote,
cmd='rm -f {0}'.format(conf_file),
exit_code=0)
except Exception:
logger.error("Could not update packages")
raise
return result
return wrapper
def update_fuel(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if settings.UPDATE_FUEL:
logger.info("Update fuel's packages from directory {0}."
.format(settings.UPDATE_FUEL_PATH))
environment = get_current_env(args)
if not environment:
logger.warning("Decorator was triggered "
"from unexpected class.")
return result
centos_files_count, ubuntu_files_count = \
environment.admin_actions.upload_packages(
local_packages_dir=settings.UPDATE_FUEL_PATH,
centos_repo_path=settings.LOCAL_MIRROR_CENTOS,
ubuntu_repo_path=settings.LOCAL_MIRROR_UBUNTU)
remote = environment.d_env.get_admin_remote()
cluster_id = environment.fuel_web.get_last_created_cluster()
if centos_files_count > 0:
environment.docker_actions.execute_in_containers(
cmd='yum -y install yum-plugin-priorities')
# Update docker containers and restart them
environment.docker_actions.execute_in_containers(
cmd='yum clean expire-cache; yum update -y')
environment.docker_actions.restart_containers()
# Update packages on master node
remote.execute(
'yum -y install yum-plugin-priorities;'
'yum clean expire-cache; yum update -y')
# Add auxiliary repository to the cluster attributes
if settings.OPENSTACK_RELEASE_UBUNTU not in \
settings.OPENSTACK_RELEASE:
environment.fuel_web.add_local_centos_mirror(
cluster_id, name="Auxiliary",
path=settings.LOCAL_MIRROR_CENTOS,
priority=settings.AUX_RPM_REPO_PRIORITY)
if ubuntu_files_count > 0:
# Add auxiliary repository to the cluster attributes
if settings.OPENSTACK_RELEASE_UBUNTU in \
settings.OPENSTACK_RELEASE:
environment.fuel_web.add_local_ubuntu_mirror(
cluster_id, name="Auxiliary",
path=settings.LOCAL_MIRROR_UBUNTU,
priority=settings.AUX_DEB_REPO_PRIORITY)
else:
logger.error("{0} .DEB files uploaded but won't be used"
" because of deploying wrong release!"
.format(ubuntu_files_count))
if settings.SYNC_DEPL_TASKS:
remote.execute("fuel release --sync-deployment-tasks"
" --dir /etc/puppet/")
return result
return wrapper
def revert_info(snapshot_name, master_ip, description=""):
logger.info("<" * 5 + "*" * 100 + ">" * 5)
logger.info("{} Make snapshot: {}".format(description, snapshot_name))
command = ("dos.py revert-resume {env} --snapshot-name {name} "
"&& ssh root@{master_ip}".format(
env=settings.ENV_NAME,
name=snapshot_name,
master_ip=master_ip))
if settings.VIRTUAL_ENV:
command = ('source {venv}/bin/activate; {command}'
.format(venv=settings.VIRTUAL_ENV, command=command))
logger.info("You could revert and ssh to master node: [{command}]"
.format(command=command))
logger.info("<" * 5 + "*" * 100 + ">" * 5)
def update_ostf(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
try:
if settings.UPLOAD_PATCHSET:
if not settings.GERRIT_REFSPEC:
raise ValueError('REFSPEC should be set for CI tests.')
logger.info("Uploading new patchset from {0}"
.format(settings.GERRIT_REFSPEC))
remote = args[0].environment.d_env.get_admin_remote()
remote.upload(settings.PATCH_PATH.rstrip('/'),
'/var/www/nailgun/fuel-ostf')
remote.execute('dockerctl shell ostf '
'bash -c "cd /var/www/nailgun/fuel-ostf; '
'python setup.py develop"')
remote.execute('dockerctl shell ostf '
'bash -c "supervisorctl restart ostf"')
helpers.wait(
lambda: "0" in
remote.execute('dockerctl shell ostf '
'bash -c "pgrep [o]stf; echo $?"')
['stdout'][1], timeout=60)
logger.info("OSTF status: RUNNING")
except Exception as e:
logger.error("Could not upload patch set {e}".format(e=e))
raise
return result
return wrapper
def create_diagnostic_snapshot(env, status, name=""):
task = env.fuel_web.task_wait(env.fuel_web.client.generate_logs(), 60 * 10)
url = "http://{}:8000{}".format(
env.get_admin_node_ip(), task['message']
)
log_file_name = '{status}_{name}-{basename}'.format(
status=status,
name=name,
basename=os.path.basename(task['message']))
save_logs(url, os.path.join(settings.LOGS_DIR, log_file_name),
auth_token=env.fuel_web.client.client.token)
def retry(count=3, delay=30):
def wrapped(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
i = 0
while True:
try:
return func(*args, **kwargs)
except:
i += 1
if i >= count:
raise
time.sleep(delay)
return wrapper
return wrapped
def custom_repo(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
custom_pkgs = CustomRepo(args[0].environment.d_env.get_admin_remote())
try:
if settings.CUSTOM_PKGS_MIRROR:
custom_pkgs.prepare_repository()
except Exception:
logger.error("Unable to get custom packages from {0}\n{1}"
.format(settings.CUSTOM_PKGS_MIRROR,
traceback.format_exc()))
raise
try:
return func(*args, **kwargs)
except Exception:
custom_pkgs.check_puppet_logs()
raise
return wrapper
def check_fuel_statistics(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if not settings.FUEL_STATS_CHECK:
return result
logger.info('Test "{0}" passed. Checking stats.'.format(func.__name__))
fuel_settings = args[0].env.get_fuel_settings()
nailgun_actions = args[0].env.nailgun_actions
postgres_actions = args[0].env.postgres_actions
remote_collector = args[0].env.collector
master_uuid = args[0].env.get_masternode_uuid()
logger.info("Master Node UUID: '{0}'".format(master_uuid))
nailgun_actions.force_fuel_stats_sending()
if not settings.FUEL_STATS_ENABLED:
assert_equal(0, int(count_stats_on_collector(remote_collector,
master_uuid)),
"Sending of Fuel stats is disabled in test, but "
"usage info was sent to collector!")
assert_equal(args[0].env.postgres_actions.count_sent_action_logs(),
0, ("Sending of Fuel stats is disabled in test, but "
"usage info was sent to collector!"))
return result
test_scenario = inspect.getdoc(func)
if 'Scenario' not in test_scenario:
logger.warning(("Can't check that fuel statistics was gathered "
"and sent to collector properly because '{0}' "
"test doesn't contain correct testing scenario. "
"Skipping...").format(func.__name__))
return func(*args, **kwargs)
try:
check_action_logs(test_scenario, postgres_actions)
check_stats_private_info(remote_collector,
postgres_actions,
master_uuid,
fuel_settings)
check_stats_on_collector(remote_collector,
postgres_actions,
master_uuid)
return result
except Exception:
logger.error(traceback.format_exc())
raise
return wrapper
def download_astute_yaml(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if settings.STORE_ASTUTE_YAML:
environment = get_current_env(args)
if environment:
store_astute_yaml(environment)
else:
logger.warning("Can't download astute.yaml: "
"Unexpected class is decorated.")
return result
return wrapper
def download_packages_json(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
environment = get_current_env(args)
if environment:
store_packages_json(environment)
else:
logger.warning("Can't collect packages: "
"Unexpected class is decorated.")
return result
return wrapper
def duration(func):
"""Measuring execution time of the decorated method in context of a test.
settings.TIMESTAT_PATH_YAML contains file name for collected data.
Data are stored to YAML file in the following format:
<name_of_system_test_method>:
<name_of_decorated_method>_XX: <seconds>
, where:
- name_of_system_test_method: Name of the system test method started
by proboscis;
- name_of_decorated_method: Name of the method to which this decorator
is implemented. _XX is a number of the method
call while test is running, from _00 to _99
- seconds: Time in seconds with floating point, consumed by the
decorated method
Thus, different tests can call the same decorated method multiple times
and get the separate measurement for each call.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
with timestat(func.__name__):
return func(*args, **kwargs)
return wrapper
def check_repos_management(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
# FIXME: Enable me for all release after fix #1403088 and #1448114
if settings.OPENSTACK_RELEASE == settings.OPENSTACK_RELEASE_UBUNTU:
env = get_current_env(args)
nailgun_nodes = env.fuel_web.client.list_cluster_nodes(
env.fuel_web.get_last_created_cluster())
for n in nailgun_nodes:
check_repo_managment(
env.d_env.get_ssh_to_remote(n['ip']))
return result
return wrapper