First scenario test

This test performs a backup followed by a restore and compares
the restored data with the original data.

This code was previously in the freezer-api repo. It fits better
into the freezer repo because:

- It interacts mostly with freezer-scheduler
- The freezer devstack gate also includes the freezer-api so it is easier to run an end-to-end test

Moved from change: 319351

Implements bp: freezer-integration-tests

Change-Id: I74448e75905d2d950a5d0483a9fe27a212f7fe21
This commit is contained in:
Jonas Pfannschmidt 2016-05-24 18:17:24 +01:00
parent 569334f6fb
commit 8b8c766ed4
4 changed files with 464 additions and 11 deletions

View File

@ -13,9 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
set -ex
set -x
echo "Start Gate Hook"
# Install freezer devstack integration
export DEVSTACK_LOCAL_CONFIG="enable_plugin freezer https://git.openstack.org/openstack/freezer"
export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin freezer-api https://git.openstack.org/openstack/freezer-api"
# Swift is needed for some of the integration tests
export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_service s-proxy s-object s-container s-account"
$BASE/new/devstack-gate/devstack-vm-gate.sh
# Copy log file so it will be collected by the CI system
sudo cp /home/tempest/.freezer/freezer.log $BASE/logs/
echo "End Gate Hook"

View File

@ -1,9 +1,15 @@
<<<<<<< HEAD
==============================
Tempest Integration of Freezer
==============================
=======
Freezer Tempest Tests
=====================
>>>>>>> First scenario test
This directory contains Tempest tests to cover the freezer project.
Integration tests in freezer are implemented using tempest. There are typically two approaches to run these tests in a development environment:
<<<<<<< HEAD
Instructions for Running/Developing Tempest Tests with Freezer Project
#. Need to make sure that there is a Devstack or other environment for running Keystone and Swift.
@ -112,3 +118,142 @@ Mac OS X Instructions
For Mac OS X users you will need to install gnu-tar in ``/usr/local/bin`` and make sure that ``/usr/local/bin`` is in the PATH environment variable before any other directories where a different version of tar can be found. Gnu-tar can be installed as ``gtar`` or ``tar``, either name works.
Also, currently for Mac OS X users, the latest version of gnu-tar (1.29) will not allow ``--unlink-first`` and ``--overwrite`` options to be used together. Also, gnu-tar will complain about the ``--unlink-first`` argument. To get around these limitations, you will need to modify ``tar_builders.py`` and remove the ``--unlink-first`` option from the ``UNIX_TEMPLATE`` variable.
=======
* Run all tests inside a devstack VM
* Run the tests outside of a devstack VM (e.g. in PyCharm) but use services (keystone, swift, ...) inside a VM
For both approaches one needs a devstack VM with freezer an swift running.
Setting up a devstack VM
------------------------
Install devstack with swift and the freezer [1]_ as well as the freezer-api [2]_ plugins by adding the following lines to you `local.conf`:
::
enable_plugin freezer https://git.openstack.org/openstack/freezer master
enable_plugin freezer-api https://git.openstack.org/openstack/freezer-api master
enable_service s-proxy s-object s-container s-account
.. [1] https://github.com/openstack/freezer/blob/master/devstack/README.rst
.. [2] https://github.com/openstack/freezer-api/blob/master/devstack/README.rst
Run tests inside a devstack VM
-------------------------------
#. Create a devstack VM as described in `Setting up a devstack VM`_
#. Inside your devstack VM, navigate to `/opt/stack/tempest`.
#. Run `ostestr -r freezer`
Debugging tests inside a devstack VM
------------------------------------
Often a devstack VM is used via SSH without graphical interface. Python has multiple command line debuggers. The out-of-the-box pdb works fine but I recommend pudb [3]_ which looks a bit like the old Turbo-Pascal/C IDE. The following steps are necessary to get it running:
#. Follow the steps in `Run tests inside a devstack VM`_.
#. Log into the devstack VM
#. Install pudb:
::
pip install pudb
#. Open the test file were you want to set the first breakpoint (more breakpoints can be set interactively later) and add the following line
::
import pudb;pu.db
#. Navigate to `/opt/stack/tempest`.
#. `ostestr` runs tests in parallel which causes issues with debuggers. To work around that you need to run the relevant test directly. E.g.:
::
python -m unittest freezer.tests.freezer_tempest_plugin.tests.scenario.test_backups.TestFreezerScenario
#. It should drop you into the debugger!
.. [3] https://pypi.python.org/pypi/pudb
Run tests outside a devstack VM
-------------------------------
#. Create a devstack VM as described in `Setting up a devstack VM`_.
#. Create and activate a virtual environment for Tempest:
::
virtualenv --no-site-packages tempest-venv
. tempest-venv/bin/activate
#. Clone and install the Tempest project into the virtual environment:
::
git clone https://github.com/openstack/tempest
pip install tempest/
#. Clone and install the Freezer project into the virtual environment:
::
git clone https://github.com/openstack/freezer
pip install -e freezer/
#. Clone and install the Freezer API project into the virtual environment:
::
git clone https://github.com/openstack/freezer-api
pip install -e freezer-api/
#. Initialise a Tempest working directory:
::
mkdir tempest-working
cd tempest-working
tempest init .
#. Configure `tempest-working/etc/tempest.conf`. The easiest way to do this is to just copy the config from `/opt/stack/tempest/etc/tempest.conf` inside the devstack VM.
#. Run the freezer test inside the tempest working directory:
::
cd tempest-working
ostestr -r freezer
Run tests in PyCharm
--------------------
#. Set up the test environment as described in `Run tests outside a devstack VM`_.
#. Start PyCharm and open a new project pointing to the cloned freezer directory.
#. Click `File > Settings > Project: freezer > Project Interpreter`.
#. Click the gear-wheel icon next to `Project Interpreter` and choose `Add Local`.
#. Navigate to your virtual environment and select the Python interpreter under `bin/python` and confirm with `OK`
#. In the left pane, navigate to one of the test scripts in `freezer/tests/freezer_tempest_plugin/tests/[api or scenario]/*.py`.
#. Right-click the file and choose `Run 'Unittests in [..]'`
#. This test run will most likely fail because it is started from the wrong directory. To fix this, open the dropdown box next to the run button in the top-right corner. Choose `Edit Configurations ..`
#. Point `Working directory:` to your tempest working directory.
#. Run the test again, this time it should work!
Troubleshooting
---------------
If tests fail these are good places to check:
* freezer-api log: `/var/log/apache2/freezer-api.log`
* freezer-agent log: `$HOME/.freezer/freezer.log`
>>>>>>> First scenario test

View File

@ -24,24 +24,36 @@ class BaseFreezerTest(tempest.test.BaseTestCase):
super(BaseFreezerTest, self).__init__(*args, **kwargs)
def setUp(self):
def setUp(self):
super(BaseFreezerTest, self).setUp()
self.get_environ()
def tearDown(self):
super(BaseFreezerTest, self).tearDown()
def get_environ(self):
os.environ['OS_PASSWORD'] = self.os.credentials.password
os.environ['OS_USERNAME'] = self.os.credentials.username
os.environ['OS_PROJECT_NAME'] = self.os.credentials.tenant_name
os.environ['OS_TENANT_NAME'] = self.os.credentials.tenant_name
@classmethod
def get_auth_url(cls):
return cls.os_primary.auth_provider.auth_client.auth_url[:-len('/tokens')]
@classmethod
def setup_clients(cls):
super(BaseFreezerTest, cls).setup_clients()
cls.get_environ()
@classmethod
def get_environ(cls):
os.environ['OS_PASSWORD'] = cls.os_primary.credentials.password
os.environ['OS_USERNAME'] = cls.os_primary.credentials.username
os.environ['OS_PROJECT_NAME'] = cls.os_primary.credentials.tenant_name
os.environ['OS_TENANT_NAME'] = cls.os_primary.credentials.tenant_name
# Allow developers to set OS_AUTH_URL when developing so that
# Keystone may be on a host other than localhost.
if not 'OS_AUTH_URL' in os.environ:
os.environ['OS_AUTH_URL'] = 'http://localhost:5000/v2.0'
os.environ['OS_AUTH_URL'] = cls.get_auth_url()
# Mac OS X uses gtar located in /usr/local/bin
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
@ -68,4 +80,4 @@ class BaseFreezerTest(tempest.test.BaseTestCase):
self.assertEqual('', err,
fail_message + " Output: {0}. "
"Error: {1}".format(out, err))
"Error: {1}".format(out, err))

View File

@ -0,0 +1,287 @@
# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# 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 hashlib
import json
import os
import shutil
import tempfile
import re
import time
import subprocess
from tempest import test
from tempest.lib.cli import base as cli_base
from tempest.lib.cli import output_parser
from freezer.tests.freezer_tempest_plugin.tests.api import base
JOB_TABLE_RESULT_COLUMN = 6
JOB_TABLE_STATUS_COLUMN = 4
class BaseFreezerCliTest(base.BaseFreezerTest):
"""Base test case class for all Freezer API tests."""
credentials = ['primary']
@classmethod
def setup_clients(cls):
super(BaseFreezerCliTest, cls).setup_clients()
cls.cli = CLIClientWithFreezer(
username = cls.os_primary.credentials.username,
# fails if the password contains an unescaped $ sign
password = cls.os_primary.credentials.password.replace('$', '$$'),
tenant_name = cls.os_primary.credentials.tenant_name,
uri = cls.get_auth_url(),
cli_dir = '/usr/local/bin' # devstack default
)
def delete_job(self, job_id):
self.cli.freezer_scheduler(action='job-delete', flags='-c test_node -j {}'.format(job_id))
def create_job(self, job_json):
with tempfile.NamedTemporaryFile(delete=False) as job_file:
job_file.write(json.dumps(job_json))
job_file.flush()
output = self.cli.freezer_scheduler(action='job-create', flags='-c test_node --file {}'.format(job_file.name))
self.assertTrue(output.startswith('Created job'))
job_id = output[len('Created job '):]
self.addCleanup(self.delete_job, job_id)
return job_id
def find_job_in_job_list(self, job_id):
job_list = output_parser.table(self.cli.freezer_scheduler(action='job-list', flags='-c test_node'))
for row in job_list['values']:
if row[0].strip() == job_id.strip():
return row
self.fail('Could not find job: {}'.format(job_id))
def wait_for_job_status(self, job_id, status, timeout=360):
start = time.time()
while True:
row = self.find_job_in_job_list(job_id)
if row[JOB_TABLE_STATUS_COLUMN] == status:
return
elif time.time() - start > timeout:
self.fail("Status of job '{}' is '{}'. Expected '{}'".format(job_id, row[JOB_TABLE_STATUS_COLUMN], status))
else:
time.sleep(1)
def assertJobColumnEqual(self, job_id, column, expected):
row = self.find_job_in_job_list(job_id)
self.assertEqual(expected, row[column])
class CLIClientWithFreezer(cli_base.CLIClient):
def freezer_scheduler(self, action, flags='', params='', fail_ok=False,
endpoint_type='publicURL', merge_stderr=False):
"""Executes freezer-scheduler command for the given action.
:param action: the cli command to run using freezer-scheduler
:type action: string
:param flags: any optional cli flags to use
:type flags: string
:param params: any optional positional args to use :type params: string
:param fail_ok: if True an exception is not raised when the
cli return code is non-zero
:type fail_ok: boolean
:param endpoint_type: the type of endpoint for the service
:type endpoint_type: string
:param merge_stderr: if True the stderr buffer is merged into stdout
:type merge_stderr: boolean
"""
flags += ' --os-endpoint-type %s' % endpoint_type
return self.cmd_with_auth(
'freezer-scheduler', action, flags, params, fail_ok, merge_stderr)
# This class is just copied from the freezer repo. Depending on where the
# scenario tests end up we may need to refactore this.
class Temp_Tree(object):
def __init__(self, suffix='', dir=None, create=True):
self.create = create
if create:
self.path = tempfile.mkdtemp(dir=dir, prefix='__freezer_',
suffix=suffix)
else:
self.path = dir
self.files = []
def __enter__(self):
return self
def cleanup(self):
if self.create and self.path:
shutil.rmtree(self.path)
def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()
def add_random_data(self, ndir=5, nfile=5, size=1024):
"""
add some files containing randoma data
:param ndir: number of dirs to create
:param nfile: number of files to create in each dir
:param size: size of files
:return: None
"""
for x in range(ndir):
subdir_path = tempfile.mkdtemp(dir=self.path)
for y in range(nfile):
abs_pathname = self.create_file_with_random_data(
dir_path=subdir_path, size=size)
rel_path_name = abs_pathname[len(self.path)+1:]
self.files.append(rel_path_name)
def create_file_with_random_data(self, dir_path, size=1024):
handle, abs_pathname = tempfile.mkstemp(dir=dir_path)
with open(abs_pathname, 'wb') as fd:
fd.write(os.urandom(size))
return abs_pathname
def get_file_hash(self, rel_filepath):
filepath = os.path.join(self.path, rel_filepath)
if os.path.isfile(filepath):
return self._filehash(filepath)
else:
return ''
def _filehash(self, filepath):
"""
Get GIT style sha1 hash for a file
:param filepath: path of file to hash
:return: hash of the file
"""
filesize_bytes = os.path.getsize(filepath)
hash_obj = hashlib.sha1()
hash_obj.update(("blob %u\0" % filesize_bytes).encode('utf-8'))
with open(filepath, 'rb') as handle:
hash_obj.update(handle.read())
return hash_obj.hexdigest()
def get_file_list(self):
"""
walks the dir tree and creates a list of relative pathnames
:return: list of relative file paths
"""
self.files = []
for root, dirs, files in os.walk(self.path):
rel_base = root[len(self.path)+1:]
self.files.extend([os.path.join(rel_base, x) for x in files])
return self.files
def is_equal(self, other_tree):
"""
Checks whether two dir tree contain the same files
It checks the number of files and the hash of each file.
NOTE: tox puts .coverage files in the temp folder (?)
:param other_tree: dir tree to compare with
:return: true if the dir trees contain the same files
"""
lh_files = [x for x in sorted(self.get_file_list())
if not x.startswith('.coverage')]
rh_files = [x for x in sorted(other_tree.get_file_list())
if not x.startswith('.coverage')]
if lh_files != rh_files:
return False
for fname in lh_files:
if os.path.isfile(fname):
if self.get_file_hash(fname) != \
other_tree.get_file_hash(fname):
return False
return True
class TestFreezerScenario(BaseFreezerCliTest):
def setUp(self):
super(TestFreezerScenario, self).setUp()
self.source_tree = Temp_Tree()
self.source_tree.add_random_data()
self.dest_tree = Temp_Tree()
self.cli.freezer_scheduler(action='start', flags='-c test_node')
def tearDown(self):
super(TestFreezerScenario, self).tearDown()
self.source_tree.cleanup()
self.dest_tree.cleanup()
self.cli.freezer_scheduler(action='stop', flags='-c test_node')
def test_simple_backup(self):
backup_job = {
"job_actions": [
{
"freezer_action": {
"action": "backup",
"mode": "fs",
"backup_name": "backup1",
"path_to_backup": self.source_tree.path,
"container": "freezer_test",
},
"max_retries": 3,
"max_retries_interval": 60
}
],
"description": "a test backup"
}
restore_job = {
"job_actions": [
{
"freezer_action": {
"action": "restore",
"restore_abs_path": self.dest_tree.path,
"backup_name": "backup1",
"container": "freezer_test",
},
"max_retries": 3,
"max_retries_interval": 60
}
],
"description": "a test restore"
}
backup_job_id = self.create_job(backup_job)
self.wait_for_job_status(backup_job_id, 'completed')
self.assertJobColumnEqual(backup_job_id, JOB_TABLE_RESULT_COLUMN, 'success')
restore_job_id = self.create_job(restore_job)
self.wait_for_job_status(restore_job_id, 'completed')
self.assertJobColumnEqual(restore_job_id, JOB_TABLE_RESULT_COLUMN, 'success')
self.assertTrue(self.source_tree.is_equal(self.dest_tree))