Remove bundled intree freezer_api tempest plugin

* https://review.openstack.org/#/c/526905/ moves the intree bundled
  freezer_api tempest plugin to its new home freezer-tempest-plugin.

Depends-On: I66cc2507b0bbd9dda9d6279f9b8d74c546d1b0a6
Change-Id: I9703758c7bdb9250686dabb376f82174abf74b63
This commit is contained in:
Chandan Kumar 2017-12-10 15:01:29 +05:30
parent 5628ca1953
commit f1812b89b1
24 changed files with 13 additions and 1458 deletions

View File

@ -21,6 +21,7 @@
- openstack/freezer-api
- openstack/freezer-web-ui
- openstack/python-freezerclient
- openstack/freezer-tempest-plugin
- job:
name: freezer-api-centos-7
@ -35,6 +36,7 @@
- openstack/freezer-api
- openstack/freezer-web-ui
- openstack/python-freezerclient
- openstack/freezer-tempest-plugin
- job:
name: freezer-api-opensuse-423
@ -49,3 +51,4 @@
- openstack/freezer-api
- openstack/freezer-web-ui
- openstack/python-freezerclient
- openstack/freezer-tempest-plugin

View File

@ -18,7 +18,7 @@
# Install freezer devstack integration
#export DEVSTACK_LOCAL_CONFIG="enable_plugin freezer-api https://git.openstack.org/openstack/freezer-api"
export DEVSTACK_GATE_TEMPEST_REGEX="freezer_api_tempest_plugin"
export DEVSTACK_GATE_TEMPEST_REGEX="freezer_tempest_plugin.tests.freezer_api"
export PROJECTS="openstack/python-freezerclient $PROJECTS"

View File

@ -1,105 +0,0 @@
==================================
Tempest Integration of Freezer API
==================================
This directory contains Tempest tests to cover the freezer-api project.
Instructions for Running/Developing Tempest Tests with Freezer API Project
#. Make sure there is Devstack or other environment for running Keystone and Elasticsearch.
#. Clone the Tempest Repo::
run 'git clone https://github.com/openstack/tempest.git'
#. Create a virtual environment for Tempest. In these instructions, the Tempest virtual environment is ``~/virtualenvs/tempest-freezer-api``.
#. Activate the Tempest virtual environment::
run 'source ~/virtualenvs/tempest-freezer-api/bin/activate'
#. Make sure you have the latest pip installed in the virtual environment::
run 'pip install --upgrade pip'
#. Install Tempest requirements.txt and test-requirements.txt in the Tempest virtual environment::
run 'pip install -r requirements.txt -r test-requirements.txt'
#. Install Tempest project into the virtual environment in develop mode::
run python setup.py develop
#. Create logging.conf in Tempest Repo home dir/etc
Make a copy of logging.conf.sample as logging.conf
In logging configuration
You will see this error on Mac OS X
socket.error: [Errno 2] No such file or directory
To fix this, edit logging.conf
Change /dev/log/ to '/var/run/syslog in logging.conf
see: https://github.com/baremetal/python-backoff/issues/1 for details
#. Create tempest.conf in Tempest Repo home dir/etc::
run 'oslo-config-generator --config-file etc/config-generator.tempest.conf --output-file etc/tempest.conf'
Add the following sections to tempest.conf and modify uri and uri_v3 to point to the host where Keystone is running::
[identity]
username = freezer
password = secretservice
tenant_name = service
domain_name = default
admin_username = admin
admin_password = secretadmin
admin_domain_name = default
admin_tenant_name = admin
alt_username = admin
alt_password = secretadmin
alt_tenant_name = admin
use_ssl = False
auth_version = v3
uri = http://10.10.10.6:5000/v2.0/
uri_v3 = http://10.10.10.6:35357/v3/
[auth]
allow_tenant_isolation = true
tempest_roles = admin
#. Clone freezer-api Repo::
run 'git clone https://github.com/openstack/freezer-api.git'
#. Set the virtual environment for the freezer-api to the Tempest virtual environment::
run 'source ~/virtualenvs/tempest-freezer-api/bin/activate'
#. pip install freezer-api requirements.txt and test-requirements.txt in the Tempest virtual environment::
run 'pip install -r requirements.txt -r test-requirements.txt'
#. Install nose in the Tempest virtual environment::
run 'pip install nose'
#. Install the freezer-api project into the Tempest virtual environment in develop mode::
run python setup.py develop
#. Setup the freezer-api.conf and freezer-paste.ini files according to the main freezer-api README.rst
#. Set the freezer-api project interpreter (pycharm) to the Tempest virtual environment.
#. Create a test config (pycharm) using the Tempest virtual environment as python interpreter.
#. Run the tests in the api directory in the freezer_api_tempest_plugin directory.

View File

@ -1,25 +0,0 @@
# (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.
from tempest import clients
from freezer_api.tests.freezer_api_tempest_plugin.services import\
freezer_api_client
class Manager(clients.Manager):
def __init__(self, credentials=None):
super(Manager, self).__init__(credentials)
self.freezer_api_client = freezer_api_client.FreezerApiClient(
self.auth_provider)

View File

@ -1,45 +0,0 @@
# Copyright 2015
# All Rights Reserved.
#
# 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.
from oslo_config import cfg
service_available_group = cfg.OptGroup(name="service_available",
title="Available OpenStack Services")
ServiceAvailableGroup = [
cfg.BoolOpt("freezer-api",
default=True,
help="Whether or not Freezer API is expected to be available"),
]
freezer_api_group = cfg.OptGroup(name="backup",
title="Freezer API Service Options")
FreezerApiGroup = [
cfg.StrOpt("region",
default="",
help="The freezer api region name to use. If empty, the value "
"of identity.region is used instead. If no such region "
"is found in the service catalog, the first found one is "
"used."),
cfg.StrOpt("catalog_type",
default="backup",
help="Catalog type of the freezer-api service."),
cfg.StrOpt('endpoint_type',
default='publicURL',
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
help="The endpoint type to use for the freezer-api service.")
]

View File

@ -1,41 +0,0 @@
# Copyright 2015
# All Rights Reserved.
#
# 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
from tempest import config as tempest_config
from tempest.test_discover import plugins
from freezer_api.tests.freezer_api_tempest_plugin import config
class FreezerApiTempestPlugin(plugins.TempestPlugin):
def load_tests(self):
base_path = os.path.split(os.path.dirname(
os.path.abspath(__file__)))[0]
test_dir = "freezer_api_tempest_plugin/tests"
full_test_dir = os.path.join(base_path, test_dir)
return full_test_dir, base_path
def register_opts(self, conf):
tempest_config.register_opt_group(
conf, config.service_available_group,
config.ServiceAvailableGroup)
tempest_config.register_opt_group(conf, config.freezer_api_group,
config.FreezerApiGroup)
def get_opt_lists(self):
return [(config.freezer_api_group.name,
config.FreezerApiGroup)]

View File

@ -1,184 +0,0 @@
# (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 urllib
from oslo_serialization import jsonutils as json
from tempest import config
from tempest.lib.common import rest_client
CONF = config.CONF
class FreezerApiClient(rest_client.RestClient):
def __init__(self, auth_provider):
super(FreezerApiClient, self).__init__(
auth_provider,
CONF.backup.catalog_type,
CONF.backup.region or CONF.identity.region,
endpoint_type=CONF.backup.endpoint_type
)
def get_version(self):
resp, response_body = self.get('/')
return resp, response_body
def get_version_v1(self):
resp, response_body = self.get('/v1')
return resp, response_body
def get_version_v2(self):
resp, response_body = self.get('/v2')
return resp, response_body
def get_backups(self, backup_id=None, **params):
if backup_id is None:
uri = '/v1/backups'
if params:
uri += '?%s' % urllib.urlencode(params)
else:
uri = '/v1/backups/' + backup_id
resp, response_body = self.get(uri)
return resp, json.loads(response_body)
def post_backups(self, metadata, backup_id=None):
uri = '/v1/backups'
if backup_id is not None:
uri += '/' + backup_id
request_body = json.dumps(metadata)
resp, response_body = self.post(uri, request_body)
return resp, json.loads(response_body)
def delete_backups(self, backup_id):
uri = '/v1/backups/' + backup_id
resp, response_body = self.delete(uri)
return resp, response_body
def get_clients(self, client_id=None, **params):
if client_id is None:
uri = '/v1/clients'
if params:
uri += '?%s' % urllib.urlencode(params)
else:
uri = 'v1/clients/' + client_id
resp, response_body = self.get(uri)
return resp, response_body
def post_clients(self, client):
request_body = json.dumps(client)
resp, response_body = self.post('/v1/clients', request_body)
return resp, json.loads(response_body)
def delete_clients(self, client_id):
uri = '/v1/clients/' + client_id
resp, response_body = self.delete(uri)
return resp, response_body
def get_jobs(self, job_id=None, **params):
if job_id is None:
uri = '/v1/jobs'
if params:
uri += '?%s' % urllib.urlencode(params)
else:
uri = '/v1/jobs/' + job_id
resp, response_body = self.get(uri)
return resp, response_body
def post_jobs(self, job):
request_body = json.dumps(job)
resp, response_body = self.post('/v1/jobs', request_body)
return resp, json.loads(response_body)
def delete_jobs(self, job_id):
uri = '/v1/jobs/' + job_id
resp, response_body = self.delete(uri)
return resp, response_body
def get_actions(self, action_id=None, **params):
if action_id is None:
uri = '/v1/actions'
if params:
uri += '?%s' % urllib.urlencode(params)
else:
uri = '/v1/actions/' + action_id
resp, response_body = self.get(uri)
return resp, response_body
def post_actions(self, action, action_id=None):
request_body = json.dumps(action)
if action_id is None:
uri = '/v1/actions'
else:
uri = '/v1/actions/' + action_id
resp, response_body = self.post(uri, request_body)
return resp, json.loads(response_body)
def patch_actions(self, action, action_id):
request_body = json.dumps(action)
uri = '/v1/actions/' + action_id
resp, response_body = self.patch(uri, request_body)
return resp, json.loads(response_body)
def delete_actions(self, id):
uri = '/v1/actions/' + id
resp, response_body = self.delete(uri)
return resp, response_body
def get_sessions(self, session_id=None, **params):
if session_id is None:
uri = '/v1/sessions'
if params:
uri += '?%s' % urllib.urlencode(params)
else:
uri = 'v1/sessions/' + session_id
resp, response_body = self.get(uri)
return resp, response_body
def post_sessions(self, session):
request_body = json.dumps(session)
resp, response_body = self.post('/v1/sessions', request_body)
return resp, json.loads(response_body)
def delete_sessions(self, session_id):
uri = '/v1/sessions/' + session_id
resp, response_body = self.delete(uri)
return resp, response_body

View File

@ -1,33 +0,0 @@
# (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.
from tempest import config
from tempest import test
from freezer_api.tests.freezer_api_tempest_plugin import clients
CONF = config.CONF
class BaseFreezerApiTest(test.BaseTestCase):
"""Base test case class for all Freezer API tests."""
credentials = ['primary']
client_manager = clients.Manager
@classmethod
def setup_clients(cls):
super(BaseFreezerApiTest, cls).setup_clients()
cls.freezer_api_client = cls.os_primary.freezer_api_client

View File

@ -1,219 +0,0 @@
# (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 json
from tempest.lib import decorators
from tempest.lib import exceptions
from freezer_api.tests.freezer_api_tempest_plugin.tests.api import base
class TestFreezerApiActions(base.BaseFreezerApiTest):
@classmethod
def resource_setup(cls):
super(TestFreezerApiActions, cls).resource_setup()
@classmethod
def resource_cleanup(cls):
super(TestFreezerApiActions, cls).resource_cleanup()
@decorators.attr(type="gate")
def test_api_actions(self):
resp, response_body = self.freezer_api_client.get_actions()
self.assertEqual(200, resp.status)
resp_body_json = json.loads(response_body)
self.assertIn('actions', resp_body_json)
actions = resp_body_json['actions']
self.assertEqual(actions, [])
@decorators.attr(type="gate")
def test_api_actions_get_limit(self):
# limits > 0 should return successfully
for valid_limit in [2, 1]:
resp, body = self.freezer_api_client.get_actions(limit=valid_limit)
self.assertEqual(200, resp.status)
# limits <= 0 should raise a bad request error
for bad_limit in [0, -1, -2]:
self.assertRaises(exceptions.BadRequest,
self.freezer_api_client.get_actions,
limit=bad_limit)
@decorators.attr(type="gate")
def test_api_actions_get_offset(self):
# offsets >= 0 should return 200
for valid_offset in [1, 0]:
resp, body = self.freezer_api_client.get_actions(
offset=valid_offset)
self.assertEqual(200, resp.status)
# offsets < 0 should return 400
for bad_offset in [-1, -2]:
self.assertRaises(exceptions.BadRequest,
self.freezer_api_client.get_actions,
offset=bad_offset)
@decorators.attr(type="gate")
def test_api_actions_post(self):
action = {
'freezer_action':
{
'actions': 'backup',
'mode': 'fs',
'src_file': '/dev/null',
'backup_name': 'test freezer api actions',
'container': 'test_freezer_api_actions_container',
'max_backup_level': 1,
'always_backup_level': 0,
'no_incremental': True,
'encrypt_pass_file': '/dev/null',
'log_file': '/dev/null',
'hostname': False,
'max_cpu_priority': False
}
}
# Create the action with POST
resp, response_body = self.freezer_api_client.post_actions(action)
self.assertEqual(201, resp.status)
self.assertIn('action_id', response_body)
action_id = response_body['action_id']
# Check that the action has the correct values
resp, response_body = self.freezer_api_client.get_actions(action_id)
self.assertEqual(200, resp.status)
resp_body_json = json.loads(response_body)
freezer_action_json = resp_body_json['freezer_action']
self.assertIn('backup_name', freezer_action_json)
backup_name = freezer_action_json['backup_name']
self.assertEqual('test freezer api actions', backup_name)
self.assertIn('container', freezer_action_json)
container = freezer_action_json['container']
self.assertEqual('test_freezer_api_actions_container', container)
self.assertIn('no_incremental', freezer_action_json)
no_incremental = freezer_action_json['no_incremental']
self.assertEqual(True, no_incremental)
self.assertIn('max_backup_level', freezer_action_json)
max_backup_level = freezer_action_json['max_backup_level']
self.assertEqual(1, max_backup_level)
self.assertIn('hostname', freezer_action_json)
hostname = freezer_action_json['hostname']
self.assertEqual(False, hostname)
self.assertIn('_version', response_body)
_version = resp_body_json['_version']
self.assertEqual(1, _version)
self.assertIn('actions', freezer_action_json)
actions = freezer_action_json['actions']
self.assertEqual('backup', actions)
self.assertIn('src_file', freezer_action_json)
src_file = freezer_action_json['src_file']
self.assertEqual('/dev/null', src_file)
self.assertIn('always_backup_level', freezer_action_json)
always_backup_level = freezer_action_json['always_backup_level']
self.assertEqual(0, always_backup_level)
self.assertIn('mode', freezer_action_json)
mode = freezer_action_json['mode']
self.assertEqual('fs', mode)
self.assertIn('encrypt_pass_file', freezer_action_json)
encrypt_pass_file = freezer_action_json['encrypt_pass_file']
self.assertEqual('/dev/null', encrypt_pass_file)
self.assertIn('max_cpu_priority', freezer_action_json)
max_cpu_priority = freezer_action_json['max_cpu_priority']
self.assertEqual(False, max_cpu_priority)
self.assertIn('user_id', response_body)
self.assertIn('log_file', freezer_action_json)
log_file = freezer_action_json['log_file']
self.assertEqual('/dev/null', log_file)
self.assertIn('action_id', response_body)
action_id_in_resp_body = resp_body_json['action_id']
self.assertEqual(action_id, action_id_in_resp_body)
# Update the action backup_name with POST
action['freezer_action']['backup_name'] = \
'test freezer api actions update with post'
resp, response_body = self.freezer_api_client.post_actions(
action, action_id)
self.assertEqual(201, resp.status)
self.assertIn('version', response_body)
version = response_body['version']
self.assertEqual(2, version)
self.assertIn('action_id', response_body)
action_id_in_resp_body = response_body['action_id']
self.assertEqual(action_id, action_id_in_resp_body)
resp, response_body = self.freezer_api_client.get_actions(action_id)
self.assertEqual(200, resp.status)
resp_body_json = json.loads(response_body)
freezer_action_json = resp_body_json['freezer_action']
self.assertIn('backup_name', freezer_action_json)
backup_name = freezer_action_json['backup_name']
self.assertEqual('test freezer api actions update with post',
backup_name)
# Update the action backup_name with PATCH
action['freezer_action']['backup_name'] = \
'test freezer api actions update with patch'
resp, response_body = self.freezer_api_client.patch_actions(
action, action_id)
self.assertEqual(200, resp.status)
self.assertIn('version', response_body)
version = response_body['version']
self.assertEqual(3, version)
self.assertIn('action_id', response_body)
action_id_in_resp_body = response_body['action_id']
self.assertEqual(action_id, action_id_in_resp_body)
resp, response_body = self.freezer_api_client.get_actions(action_id)
self.assertEqual(200, resp.status)
resp_body_json = json.loads(response_body)
freezer_action_json = resp_body_json['freezer_action']
self.assertIn('backup_name', freezer_action_json)
backup_name = freezer_action_json['backup_name']
self.assertEqual('test freezer api actions update with patch',
backup_name)
# Delete the action
resp, response_body = self.freezer_api_client.delete_actions(
action_id)
self.assertEqual(204, resp.status)

View File

@ -1,300 +0,0 @@
# (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 json
import tempest
from tempest.lib import decorators
from freezer_api.tests.freezer_api_tempest_plugin.tests.api import base
class TestFreezerApiBackups(base.BaseFreezerApiTest):
credentials = ['primary', 'alt']
@decorators.attr(type="gate")
def test_api_backups_list(self):
for i in range(1, 4):
self._create_temporary_backup(
self._build_metadata("test_freezer_backups_" + str(i)))
resp, response_body = self.freezer_api_client.get_backups()
self.assertEqual(200, resp.status)
self.assertIn('backups', response_body)
backups = response_body['backups']
self.assertEqual(3, len(backups))
backup_names = [b['backup_metadata']['backup_name'] for b in backups]
for i in range(1, 4):
self.assertIn("test_freezer_backups_" + str(i), backup_names)
@decorators.attr(type="gate")
def test_api_backups_list_other_users_backups(self):
# Test if it is not possible to list backups
# from a different user
self._create_temporary_backup(
self._build_metadata("test_freezer_backups"))
# Switching to alt_user here
resp, response_body = self.os_alt.freezer_api_client.get_backups()
self.assertEqual(200, resp.status)
self.assertEmpty(response_body['backups'])
@decorators.attr(type="gate")
def test_api_backups_list_empty(self):
resp, response_body = self.freezer_api_client.get_backups()
self.assertEqual(200, resp.status)
self.assertEmpty(response_body['backups'])
@decorators.attr(type="gate")
def test_api_backups_list_limit(self):
for i in range(1, 9):
self._create_temporary_backup(
self._build_metadata("test_freezer_backups_" + str(i)))
resp, response_body = self.freezer_api_client.get_backups(limit=5)
self.assertEqual(200, resp.status)
self.assertIn('backups', response_body)
backups = response_body['backups']
self.assertEqual(5, len(backups))
# limits <= 0 should return an error (bad request)
for bad_limit in [0, -1, -2]:
self.assertRaises(tempest.lib.exceptions.BadRequest,
self.freezer_api_client.get_actions,
limit=bad_limit)
@decorators.attr(type="gate")
def test_api_backups_list_offset(self):
for i in range(1, 9):
self._create_temporary_backup(
self._build_metadata("test_freezer_backups_" + str(i)))
# valid offsets should return the correct number of entries
resp, response_body = self.freezer_api_client.get_backups(offset=0)
self.assertEqual(200, resp.status)
self.assertEqual(8, len(response_body['backups']))
resp, response_body = self.freezer_api_client.get_backups(offset=5)
self.assertEqual(200, resp.status)
self.assertEqual(3, len(response_body['backups']))
resp, response_body = self.freezer_api_client.get_backups(offset=8)
self.assertEqual(200, resp.status)
self.assertEqual(0, len(response_body['backups']))
# an offset greater than the number of entries should successfully
# return no entries
resp, response_body = self.freezer_api_client.get_backups(offset=10)
self.assertEqual(200, resp.status)
self.assertEqual(0, len(response_body['backups']))
# negative offsets should raise an error
self.assertRaises(tempest.lib.exceptions.BadRequest,
self.freezer_api_client.get_backups, offset=-1)
self.assertRaises(tempest.lib.exceptions.BadRequest,
self.freezer_api_client.get_backups, offset=-2)
@decorators.attr(type="gate")
def test_api_backups_list_limit_offset(self):
""" Test pagination by grabbing the backups in two steps and
comparing to the list of all backups.
"""
for i in range(1, 9):
self._create_temporary_backup(
self._build_metadata("test_freezer_backups_" + str(i)))
resp, response_body = self.freezer_api_client.get_backups(limit=5)
self.assertEqual(200, resp.status)
self.assertIn('backups', response_body)
first_5_backups = response_body['backups']
self.assertEqual(5, len(first_5_backups))
resp, response_body = self.freezer_api_client.get_backups(limit=3,
offset=5)
second_3_backups = response_body['backups']
self.assertEqual(3, len(second_3_backups))
resp, response_body = self.freezer_api_client.get_backups()
all_backups = response_body['backups']
self.assertEqual(len(all_backups),
len(first_5_backups + second_3_backups))
self.assertEqual(all_backups, first_5_backups + second_3_backups)
@decorators.attr(type="gate")
def test_api_backups_post(self):
metadata = self._build_metadata("test_freezer_backups")
backup_id = self._create_temporary_backup(metadata)
resp, response_body = self._workaround_get_backup(backup_id)
expected = self._build_expected_data(backup_id, metadata)
# backup_id is generated automatically, we can't know it
del(response_body['backup_id'])
self.assertEqual(200, resp.status)
self.assertEqual(expected, response_body)
@decorators.attr(type="gate")
def test_api_backups_post_without_content_type(self):
""" Test the backup endpoint without content-type=application/json.
It's expected to work regardless of whether content-type is set or not.
"""
metadata = self._build_metadata("test_freezer_backups")
uri = '/v1/backups'
request_body = json.dumps(metadata)
# Passing in an empty dict for headers to avoid automatically
# generating headers
resp, response_body = self.freezer_api_client.post(uri, request_body,
headers={})
self.assertEqual(resp.status, 201)
@decorators.attr(type="gate")
def test_api_backups_post_incomplete(self):
metadata = self._build_metadata("test_freezer_backups")
del (metadata['container'])
self.assertRaises(tempest.lib.exceptions.BadRequest,
self.freezer_api_client.post_backups, metadata)
@decorators.attr(type="gate")
def test_api_backups_post_minimal(self):
metadata = {
"curr_backup_level": 0,
"container": "test_freezer_api_backups_container",
"hostname": "localhost",
"backup_name": "test_freezer_backups",
"time_stamp": 1459349846,
}
backup_id = self._create_temporary_backup(metadata)
resp, response_body = self._workaround_get_backup(backup_id)
expected = self._build_expected_data(backup_id, metadata)
# backup_id is generated automatically, we can't know it
del(response_body['backup_id'])
self.assertEqual(200, resp.status)
self.assertEqual(expected, response_body)
@decorators.attr(type="gate")
def test_api_backups_delete(self):
metadata = self._build_metadata("test_freezer_backups")
backup_id = self._create_temporary_backup(metadata)
self.freezer_api_client.delete_backups(backup_id)
resp, response_body = self.freezer_api_client.get_backups()
self.assertEqual(0, len(response_body['backups']))
@decorators.attr(type="gate")
def test_api_backups_delete_other_users_backups(self):
metadata = self._build_metadata("test_freezer_backups")
backup_id = self._create_temporary_backup(metadata)
# Switching user
resp, response_body = self.os_alt.freezer_api_client.delete_backups(
backup_id)
self.assertEqual('204', resp['status'])
self.assertEmpty(response_body)
# Switching back to original user
resp, response_body = self.freezer_api_client.get_backups()
self.assertEqual(1, len(response_body['backups']))
def _build_expected_data(self, backup_id, metadata):
return {
'user_name': self.os_primary.credentials.username,
'user_id': self.os_primary.credentials.user_id,
'backup_metadata': metadata
}
def _build_metadata(self, backup_name):
return {
"action": "backup",
"always_level": "",
"backup_media": "fs",
"client_id": "freezer",
"client_version": "1.2.18",
"container_segments": "",
"curr_backup_level": 0,
"dry_run": "",
"log_file": "",
"job_id": "76cf6739ca2e4dc58b2215632c2a0b49",
"os_auth_version": "",
"path_to_backup": "/dev/null",
"proxy": "",
"ssh_host": "",
"ssh_key": "",
"ssh_port": 22,
"ssh_username": "",
"storage": "swift",
"container": "test_freezer_api_backups_container",
"hostname": "localhost",
"backup_name": backup_name,
"time_stamp": 1459349846,
"level": 1,
"max_level": 14,
"mode": "fs",
"fs_real_path": "/dev/null",
"vol_snap_path": "",
"total_broken_links": 1,
"total_fs_files": 100,
"total_directories": 100,
"backup_size_uncompressed": 10000,
"backup_size_compressed": 1000,
"compression": "gzip",
"encrypted": False,
"client_os": "linux2",
"broken_links": [],
"excluded_files": [],
"cli": "freezer",
"version": "1.0"
}
def _create_temporary_backup(self, metadata):
resp, response_body = self.freezer_api_client.post_backups(metadata)
self.assertEqual('201', resp['status'])
self.assertIn('backup_id', response_body)
backup_id = response_body['backup_id']
self.addCleanup(self.freezer_api_client.delete_backups, backup_id)
return backup_id
def _workaround_get_backup(self, backup_id):
# TODO(JonasPf): Use the following line, once this bug is fixed:
# https://bugs.launchpad.net/freezer/+bug/1564649
# resp, response_body = self.freezer_api_client.get_backups(backup_id)
resp, response_body = self.freezer_api_client.get_backups()
result = next((b for b in response_body['backups'] if
b['backup_id'] == backup_id))
return resp, result

View File

@ -1,96 +0,0 @@
# (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 json
import time
from tempest.lib import decorators
from tempest.lib import exceptions
from freezer_api.tests.freezer_api_tempest_plugin.tests.api import base
class TestFreezerApiClients(base.BaseFreezerApiTest):
@classmethod
def resource_setup(cls):
super(TestFreezerApiClients, cls).resource_setup()
@classmethod
def resource_cleanup(cls):
super(TestFreezerApiClients, cls).resource_cleanup()
@decorators.attr(type="gate")
def test_api_clients(self):
resp, response_body = self.freezer_api_client.get_clients()
self.assertEqual(200, resp.status)
response_body_json = json.loads(response_body)
self.assertIn('clients', response_body_json)
clients = response_body_json['clients']
self.assertEmpty(clients)
@decorators.attr(type="gate")
def test_api_clients_get_limit(self):
# limits > 0 should return successfully
for valid_limit in [2, 1]:
resp, body = self.freezer_api_client.get_clients(limit=valid_limit)
self.assertEqual(200, resp.status)
# limits <= 0 should raise a bad request error
for bad_limit in [0, -1, -2]:
self.assertRaises(exceptions.BadRequest,
self.freezer_api_client.get_clients,
limit=bad_limit)
@decorators.attr(type="gate")
def test_api_clients_get_offset(self):
# offsets >= 0 should return 200
for valid_offset in [1, 0]:
resp, body = self.freezer_api_client.get_clients(
offset=valid_offset)
self.assertEqual(200, resp.status)
# offsets < 0 should return 400
for bad_offset in [-1, -2]:
self.assertRaises(exceptions.BadRequest,
self.freezer_api_client.get_clients,
offset=bad_offset)
@decorators.attr(type="gate")
def test_api_clients_post(self):
client = {'client_id': 'test-client-id',
'hostname': 'test-host-name',
'description': 'a test client',
'uuid': 'test-client-uuid'}
# Create the client with POST
resp, response_body = self.freezer_api_client.post_clients(client)
self.assertEqual(201, resp.status)
self.assertIn('client_id', response_body)
client_id = response_body['client_id']
# Check that the client has the correct values
# Give the DB some time to catch up
time.sleep(5)
resp, response_body = self.freezer_api_client.get_clients(client_id)
self.assertEqual(200, resp.status)
# Delete the client
resp, response_body = self.freezer_api_client.delete_clients(
client_id)
self.assertEqual(204, resp.status)

View File

@ -1,154 +0,0 @@
# (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 json
from tempest.lib import decorators
from tempest.lib import exceptions
from freezer_api.tests.freezer_api_tempest_plugin.tests.api import base
fake_job = {
"job_actions":
[
{
"freezer_action":
{
"action": "backup",
"mode": "fs",
"src_file": "/home/tylerdurden/project_mayhem",
"backup_name": "project_mayhem_backup",
"container": "my_backup_container",
},
"exit_status": "success",
"max_retries": 1,
"max_retries_interval": 1,
"mandatory": True
}
],
"job_schedule":
{
"time_created": 1234,
"time_started": 1234,
"time_ended": 0,
"status": "stop",
"schedule_date": "2015-06-02T16:20:00",
"schedule_month": "1-6, 9-12",
"schedule_day": "mon, wed, fri",
"schedule_hour": "03",
"schedule_minute": "25",
},
"job_id": "blabla",
"client_id": "01b0f00a-4ce2-11e6-beb8-9e71128cae77_myhost.mydomain.mytld",
"user_id": "blabla",
"description": "scheduled one shot"
}
class TestFreezerApiJobs(base.BaseFreezerApiTest):
@classmethod
def resource_setup(cls):
super(TestFreezerApiJobs, cls).resource_setup()
@classmethod
def resource_cleanup(cls):
super(TestFreezerApiJobs, cls).resource_cleanup()
@decorators.attr(type="gate")
def test_api_jobs(self):
resp, response_body = self.freezer_api_client.get_jobs()
self.assertEqual(200, resp.status)
response_body_json = json.loads(response_body)
self.assertIn('jobs', response_body_json)
jobs = response_body_json['jobs']
self.assertEmpty(jobs)
@decorators.attr(type="gate")
def test_api_jobs_get_limit(self):
# limits > 0 should return successfully
for valid_limit in [2, 1]:
resp, body = self.freezer_api_client.get_jobs(limit=valid_limit)
self.assertEqual(200, resp.status)
# limits <= 0 should raise a bad request error
for bad_limit in [0, -1, -2]:
self.assertRaises(exceptions.BadRequest,
self.freezer_api_client.get_jobs,
limit=bad_limit)
@decorators.attr(type="gate")
def test_api_jobs_get_offset(self):
# offsets >= 0 should return 200
for valid_offset in [1, 0]:
resp, body = self.freezer_api_client.get_jobs(offset=valid_offset)
self.assertEqual(200, resp.status)
# offsets < 0 should return 400
for bad_offset in [-1, -2]:
self.assertRaises(exceptions.BadRequest,
self.freezer_api_client.get_jobs,
offset=bad_offset)
@decorators.attr(type="gate")
def test_api_jobs_post(self):
# Create the job with POST
resp, response_body = self.freezer_api_client.post_jobs(fake_job)
self.assertEqual(201, resp.status)
self.assertIn('job_id', response_body)
job_id = response_body['job_id']
# Check that the job has the correct values
resp, response_body = self.freezer_api_client.get_jobs(job_id)
self.assertEqual(200, resp.status)
# Delete the job
resp, response_body = self.freezer_api_client.delete_jobs(
job_id)
self.assertEqual(204, resp.status)
@decorators.attr(type="gate")
def test_api_jobs_with_invalid_client_project_id_fail(self):
"""Ensure that a job submitted with a bad client_id project id fails"""
fake_bad_job = fake_job
fake_bad_job['client_id'] = 'bad%project$id_host.domain.tld'
# Create the job with POST
self.assertRaises(exceptions.BadRequest,
lambda: self.freezer_api_client.post_jobs(
fake_bad_job))
@decorators.attr(type="gate")
def test_api_jobs_with_invalid_client_host_fail(self):
"""Ensure that a job submitted with a bad client_id hostname fails"""
fake_bad_job = fake_job
fake_bad_job['client_id'] = ("01b0f00a-4ce2-11e6-beb8-9e71128cae77"
"_bad_hostname.bad/domain.b")
# Create the job with POST
self.assertRaises(exceptions.BadRequest,
lambda: self.freezer_api_client.post_jobs(
fake_bad_job))
def test_api_jobs_with_only_fqdn_succeeds(self):
"""Ensure that a job submitted with only an FQDN succeeds"""
fqdn_only_job = fake_job
fqdn_only_job['client_id'] = 'padawan-ccp-c1-m1-mgmt'
# Attempt to post the job, should succeed
resp, response_body = self.freezer_api_client.post_jobs(fqdn_only_job)
self.assertEqual(201, resp.status)

View File

@ -1,131 +0,0 @@
# (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 json
from tempest.lib import decorators
from tempest.lib import exceptions
from freezer_api.tests.freezer_api_tempest_plugin.tests.api import base
class TestFreezerApiSessions(base.BaseFreezerApiTest):
@classmethod
def resource_setup(cls):
super(TestFreezerApiSessions, cls).resource_setup()
@classmethod
def resource_cleanup(cls):
super(TestFreezerApiSessions, cls).resource_cleanup()
@decorators.attr(type="gate")
def test_api_sessions(self):
resp, response_body = self.freezer_api_client.get_sessions()
self.assertEqual(200, resp.status)
response_body_json = json.loads(response_body)
self.assertIn('sessions', response_body_json)
sessions = response_body_json['sessions']
self.assertEmpty(sessions)
@decorators.attr(type="gate")
def test_api_sessions_get_limit(self):
# limits > 0 should return successfully
for valid_limit in [2, 1]:
resp, body = self.freezer_api_client.get_sessions(
limit=valid_limit)
self.assertEqual(200, resp.status)
# limits <= 0 should raise a bad request error
for bad_limit in [0, -1, -2]:
self.assertRaises(exceptions.BadRequest,
self.freezer_api_client.get_sessions,
limit=bad_limit)
@decorators.attr(type="gate")
def test_api_sessions_get_offset(self):
# offsets >= 0 should return 200
for valid_offset in [1, 0]:
resp, body = self.freezer_api_client.get_sessions(
offset=valid_offset)
self.assertEqual(200, resp.status)
# offsets < 0 should return 400
for bad_offset in [-1, -2]:
self.assertRaises(exceptions.BadRequest,
self.freezer_api_client.get_sessions,
offset=bad_offset)
@decorators.attr(type="gate")
def test_api_sessions_post(self):
session = {
"session_id": "test-session",
"session_tag": 1,
"description": "a test session",
"hold_off": 5,
"schedule": {
"time_created": 1234,
"time_started": 1234,
"time_ended": 0,
"status": "stop",
"schedule_date": "2015-06-02T16:20:00",
"schedule_month": "1-6, 9-12",
"schedule_day": "mon, wed, fri",
"schedule_hour": "03",
"schedule_minute": "25",
},
"jobs": [
{
'job_id_1': {
"client_id": "client-id-1",
"status": "stop",
"result": "success",
"time_started": 1234,
"time_ended": 1234
},
'job_id_2': {
"client_id": "client-id-1",
"status": "stop",
"result": "success",
"time_started": 1234,
"time_ended": 1234,
}
}
],
"time_start": 1234,
"time_end": 1234,
"time_started": 1234,
"time_ended": 1234,
"status": "completed",
"result": "success",
"user_id": "user-id-1"
}
# Create the session with POST
resp, response_body = self.freezer_api_client.post_sessions(session)
self.assertEqual(201, resp.status)
self.assertIn('session_id', response_body)
session_id = response_body['session_id']
# Check that the session has the correct values
resp, response_body = self.freezer_api_client.get_sessions(session_id)
self.assertEqual(200, resp.status)
# Delete the session
resp, response_body = self.freezer_api_client.delete_sessions(
session_id)
self.assertEqual(204, resp.status)

View File

@ -1,110 +0,0 @@
# (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 json
from tempest.lib import decorators
from freezer_api.tests.freezer_api_tempest_plugin.tests.api import base
class TestFreezerApiVersion(base.BaseFreezerApiTest):
@classmethod
def resource_setup(cls):
super(TestFreezerApiVersion, cls).resource_setup()
@classmethod
def resource_cleanup(cls):
super(TestFreezerApiVersion, cls).resource_cleanup()
@decorators.attr(type="gate")
def test_api_version(self):
resp, response_body = self.freezer_api_client.get_version()
self.assertEqual(300, resp.status)
resp_body_json = json.loads(response_body)
self.assertIn('versions', resp_body_json)
current_version = resp_body_json['versions'][1]
self.assertEqual(len(current_version), 4)
self.assertIn('id', current_version)
self.assertEqual(current_version['id'], 'v1')
self.assertIn('links', current_version)
links = current_version['links'][0]
self.assertIn('href', links)
href = links['href']
self.assertIn('/v1/', href)
self.assertIn('rel', links)
rel = links['rel']
self.assertEqual('self', rel)
self.assertIn('status', current_version)
status = current_version['status']
self.assertEqual('CURRENT', status)
self.assertIn('updated', current_version)
@decorators.attr(type="gate")
def test_api_version_v1(self):
resp, response_body = self.freezer_api_client.get_version_v1()
self.assertEqual(200, resp.status)
response_body_jason = json.loads(response_body)
self.assertIn('resources', response_body_jason)
resource = response_body_jason['resources']
self.assertIn('rel/backups', resource)
rel_backups = resource['rel/backups']
self.assertIn('href-template', rel_backups)
href_template = rel_backups['href-template']
self.assertEqual('/v1/backups/{backup_id}', href_template)
self.assertIn('href-vars', rel_backups)
href_vars = rel_backups['href-vars']
self.assertIn('backup_id', href_vars)
backup_id = href_vars['backup_id']
self.assertEqual('param/backup_id', backup_id)
self.assertIn('hints', rel_backups)
hints = rel_backups['hints']
self.assertIn('allow', hints)
allow = hints['allow']
self.assertEqual('GET', allow[0])
self.assertIn('formats', hints)
formats = hints['formats']
self.assertIn('application/json', formats)
@decorators.attr(type="gate")
def test_api_version_v2(self):
resp, response_body = self.freezer_api_client.get_version_v2()
self.assertEqual(200, resp.status)
response_body_jason = json.loads(response_body)
self.assertIn('resources', response_body_jason)
resource = response_body_jason['resources']
self.assertIn('rel/backups', resource)
rel_backups = resource['rel/backups']
self.assertIn('href-template', rel_backups)
href_template = rel_backups['href-template']
self.assertEqual('/v2/{project_id}/backups/{backup_id}', href_template)
self.assertIn('href-vars', rel_backups)
href_vars = rel_backups['href-vars']
self.assertIn('backup_id', href_vars)
self.assertIn('project_id', href_vars)
backup_id = href_vars['backup_id']
self.assertEqual('param/backup_id', backup_id)
project_id = href_vars['project_id']
self.assertEqual('param/project_id', project_id)
self.assertIn('hints', rel_backups)
hints = rel_backups['hints']
self.assertIn('allow', hints)
allow = hints['allow']
self.assertEqual('GET', allow[0])
self.assertIn('formats', hints)
formats = hints['formats']
self.assertIn('application/json', formats)

View File

@ -35,6 +35,7 @@
enable_plugin freezer-api https://git.openstack.org/openstack/freezer-api
# enable freezer-web-ui and python-freezerclient
enable_plugin freezer-web-ui https://git.openstack.org/openstack/freezer-web-ui
TEMPEST_PLUGINS='/opt/stack/new/freezer-tempest-plugin'
EOF
executable: /bin/bash
@ -50,11 +51,10 @@
export PROJECTS="openstack/freezer-web-ui $PROJECTS"
export PROJECTS="openstack/freezer $PROJECTS"
export PROJECTS="openstack/python-freezerclient $PROJECTS"
export PROJECTS="openstack/freezer-tempest-plugin $PROJECTS"
# tempest config
export DEVSTACK_GATE_TEMPEST=1
export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
TEMPEST_REGEX="freezer_api_tempest_plugin"
export DEVSTACK_GATE_TEMPEST_REGEX="freezer_api_tempest_plugin"
export DEVSTACK_GATE_TEMPEST_REGEX="freezer_tempest_plugin.tests.freezer_api"
# which repo is being tested
export DEVSTACK_PROJECT_FROM_GIT=freezer-api

View File

@ -35,6 +35,7 @@
enable_plugin freezer-api https://git.openstack.org/openstack/freezer-api
# enable freezer-web-ui and python-freezerclient
enable_plugin freezer-web-ui https://git.openstack.org/openstack/freezer-web-ui
TEMPEST_PLUGINS='/opt/stack/new/freezer-tempest-plugin'
EOF
executable: /bin/bash
@ -50,11 +51,10 @@
export PROJECTS="openstack/freezer-web-ui $PROJECTS"
export PROJECTS="openstack/freezer $PROJECTS"
export PROJECTS="openstack/python-freezerclient $PROJECTS"
export PROJECTS="openstack/freezer-tempest-plugin $PROJECTS"
# tempest config
export DEVSTACK_GATE_TEMPEST=1
export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
TEMPEST_REGEX="freezer_api_tempest_plugin"
export DEVSTACK_GATE_TEMPEST_REGEX="freezer_api_tempest_plugin"
export DEVSTACK_GATE_TEMPEST_REGEX="freezer_tempest_plugin.tests.freezer_api"
# which repo is being tested
export DEVSTACK_PROJECT_FROM_GIT=freezer-api

View File

@ -35,6 +35,7 @@
enable_plugin freezer-api https://git.openstack.org/openstack/freezer-api
# enable freezer-web-ui and python-freezerclient
enable_plugin freezer-web-ui https://git.openstack.org/openstack/freezer-web-ui
TEMPEST_PLUGINS='/opt/stack/new/freezer-tempest-plugin'
EOF
executable: /bin/bash
@ -50,11 +51,10 @@
export PROJECTS="openstack/freezer-web-ui $PROJECTS"
export PROJECTS="openstack/freezer $PROJECTS"
export PROJECTS="openstack/python-freezerclient $PROJECTS"
export PROJECTS="openstack/freezer-tempest-plugin $PROJECTS"
# tempest config
export DEVSTACK_GATE_TEMPEST=1
export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
TEMPEST_REGEX="freezer_api_tempest_plugin"
export DEVSTACK_GATE_TEMPEST_REGEX="freezer_api_tempest_plugin"
export DEVSTACK_GATE_TEMPEST_REGEX="freezer_tempest_plugin.tests.freezer_api"
# which repo is being tested
export DEVSTACK_PROJECT_FROM_GIT=freezer-api

View File

@ -52,8 +52,6 @@ oslo.policy.policies =
console_scripts =
freezer-api = freezer_api.cmd.api:main
freezer-manage = freezer_api.cmd.manage:main
tempest.test_plugins =
freezer_api_tempest_tests = freezer_api.tests.freezer_api_tempest_plugin.plugin:FreezerApiTempestPlugin
paste.app_factory =
service_v1 = freezer_api.service:freezer_app_factory
wsgi_scripts =

View File

@ -15,6 +15,3 @@ testtools>=2.2.0 # MIT
os-api-ref>=1.4.0 # Apache-2.0
reno>=2.5.0 # Apache-2.0
openstackdocstheme>=1.17.0 # Apache-2.0
# Tempest Plugin
tempest>=17.1.0 # Apache-2.0