Remove intree magnum tempest plugin

* It removes magnum tempest plugin reference in favour of using
  newly magnum-tempest-plugin.
* We removed tempest tests resides under functional.api.v1.test-*
  files as they are tempest tests and keeping the rest as they are
  used by functional tests.

Depends-On: Ibdddd26da9cfb0d08c2977660320b2c052d7261b
Change-Id: Ida2fa1ef5880ebead787e3b27ac7ebf5aa498f62
This commit is contained in:
Chandan Kumar 2017-12-08 13:38:31 +05:30
parent 2fafa7fdab
commit 53fa3accea
15 changed files with 28 additions and 1258 deletions

View File

@ -13,6 +13,7 @@
- openstack/ironic-lib
- openstack/ironic-python-agent
- openstack/magnum
- openstack/magnum-tempest-plugin
- openstack/pyghmi
- openstack/python-ironicclient
- openstack/python-magnumclient

View File

@ -197,22 +197,23 @@ if [[ "api" == "$coe" ]]; then
# strigazi: don't run test_create_list_sign_delete_clusters because
# it is very unstable in the CI
_magnum_tests="magnum.tests.functional.api.v1.test_bay"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_baymodel"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_cluster_template"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_cluster_template_admin"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_magnum_service"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_cluster.ClusterTest.test_create_cluster_for_nonexisting_cluster_template"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_cluster.ClusterTest.test_create_cluster_with_node_count_0"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_cluster.ClusterTest.test_create_cluster_with_nonexisting_flavor"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_cluster.ClusterTest.test_create_cluster_with_zero_masters"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_cluster.ClusterTest.test_delete_cluster_for_nonexisting_cluster"
_magnum_tests="$_magnum_tests magnum.tests.functional.api.v1.test_cluster.ClusterTest.test_update_cluster_for_nonexisting_cluster"
_magnum_tests="magnum_tempest_plugin.tests.api.v1.test_bay"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_baymodel"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_cluster_template"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_cluster_template_admin"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_magnum_service"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_cluster.ClusterTest.test_create_cluster_for_nonexisting_cluster_template"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_cluster.ClusterTest.test_create_cluster_with_node_count_0"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_cluster.ClusterTest.test_create_cluster_with_nonexisting_flavor"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_cluster.ClusterTest.test_create_cluster_with_zero_masters"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_cluster.ClusterTest.test_delete_cluster_for_nonexisting_cluster"
_magnum_tests="$_magnum_tests magnum_tempest_plugin.tests.api.v1.test_cluster.ClusterTest.test_update_cluster_for_nonexisting_cluster"
fi
target="${coe}${special}"
pushd $BASE/new/magnum-tempest-plugin
sudo -E -H -u $USER tox -e functional-"$target" $_magnum_tests -- --concurrency=1
popd
EXIT_CODE=$?
# Delete the keypair used in the functional test.

View File

@ -1,216 +0,0 @@
# 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 fixtures
from oslo_log import log as logging
from oslo_utils import uuidutils
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions
import testtools
from magnum.tests.functional.api import base
from magnum.tests.functional.common import config
from magnum.tests.functional.common import datagen
class BayTest(base.BaseTempestTest):
"""Tests for bay CRUD."""
LOG = logging.getLogger(__name__)
def __init__(self, *args, **kwargs):
super(BayTest, self).__init__(*args, **kwargs)
self.bays = []
self.creds = None
self.keypair = None
self.baymodel = None
self.baymodel_client = None
self.keypairs_client = None
self.bay_client = None
self.cert_client = None
def setUp(self):
try:
super(BayTest, self).setUp()
(self.creds, self.keypair) = self.get_credentials_with_keypair(
type_of_creds='default')
(self.baymodel_client,
self.keypairs_client) = self.get_clients_with_existing_creds(
creds=self.creds,
type_of_creds='default',
request_type='baymodel')
(self.bay_client, _) = self.get_clients_with_existing_creds(
creds=self.creds,
type_of_creds='default',
request_type='bay')
(self.cert_client, _) = self.get_clients_with_existing_creds(
creds=self.creds,
type_of_creds='default',
request_type='cert')
model = datagen.valid_swarm_mode_baymodel()
_, self.baymodel = self._create_baymodel(model)
# NOTE (dimtruck) by default tempest sets timeout to 20 mins.
# We need more time.
test_timeout = 1800
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
except Exception:
self.tearDown()
raise
def tearDown(self):
try:
bay_list = self.bays[:]
for bay_id in bay_list:
self._delete_bay(bay_id)
self.bays.remove(bay_id)
if self.baymodel:
self._delete_baymodel(self.baymodel.uuid)
finally:
super(BayTest, self).tearDown()
def _create_baymodel(self, baymodel_model):
self.LOG.debug('We will create a baymodel for %s', baymodel_model)
resp, model = self.baymodel_client.post_baymodel(baymodel_model)
return resp, model
def _delete_baymodel(self, baymodel_id):
self.LOG.debug('We will delete a baymodel for %s', baymodel_id)
resp, model = self.baymodel_client.delete_baymodel(baymodel_id)
return resp, model
def _create_bay(self, bay_model, is_async=False):
self.LOG.debug('We will create bay for %s', bay_model)
headers = {'Content-Type': 'application/json',
'Accept': 'application/json'}
if is_async:
headers["OpenStack-API-Version"] = "container-infra 1.2"
resp, model = self.bay_client.post_bay(bay_model, headers=headers)
self.LOG.debug('Response: %s', resp)
if is_async:
self.assertEqual(202, resp.status)
else:
self.assertEqual(201, resp.status)
self.assertIsNotNone(model.uuid)
self.assertTrue(uuidutils.is_uuid_like(model.uuid))
self.bays.append(model.uuid)
self.bay_uuid = model.uuid
if config.Config.copy_logs:
self.addCleanup(self.copy_logs_handler(
lambda: list(
[self._get_bay_by_id(self.bay_uuid)[1].master_addresses,
self._get_bay_by_id(self.bay_uuid)[1].node_addresses]),
self.baymodel.coe,
self.keypair))
self.bay_client.wait_for_created_bay(model.uuid, delete_on_error=False)
return resp, model
def _delete_bay(self, bay_id):
self.LOG.debug('We will delete a bay for %s', bay_id)
resp, model = self.bay_client.delete_bay(bay_id)
self.assertEqual(204, resp.status)
self.bay_client.wait_for_bay_to_delete(bay_id)
self.assertRaises(
exceptions.NotFound,
self.cert_client.get_cert, bay_id)
return resp, model
def _get_bay_by_id(self, bay_id):
resp, model = self.bay_client.get_bay(bay_id)
return resp, model
@testtools.testcase.attr('negative')
def test_create_bay_for_nonexisting_baymodel(self):
gen_model = datagen.valid_bay_data(baymodel_id='this-does-not-exist')
self.assertRaises(
exceptions.BadRequest,
self.bay_client.post_bay, gen_model)
@testtools.testcase.attr('negative')
def test_create_bay_with_node_count_0(self):
gen_model = datagen.valid_bay_data(
baymodel_id=self.baymodel.uuid, node_count=0)
self.assertRaises(
exceptions.BadRequest,
self.bay_client.post_bay, gen_model)
@testtools.testcase.attr('negative')
def test_create_bay_with_zero_masters(self):
gen_model = datagen.valid_bay_data(baymodel_id=self.baymodel.uuid,
master_count=0)
self.assertRaises(
exceptions.BadRequest,
self.bay_client.post_bay, gen_model)
@testtools.testcase.attr('negative')
def test_create_bay_with_nonexisting_flavor(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, baymodel = self._create_baymodel(gen_model)
self.assertEqual(201, resp.status)
self.assertIsNotNone(baymodel.uuid)
gen_model = datagen.valid_bay_data(baymodel_id=baymodel.uuid)
gen_model.flavor_id = 'aaa'
self.assertRaises(
exceptions.BadRequest,
self.bay_client.post_bay, gen_model)
resp, _ = self._delete_baymodel(baymodel.uuid)
self.assertEqual(204, resp.status)
@testtools.testcase.attr('negative')
def test_create_bay_with_nonexisting_keypair(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, baymodel = self._create_baymodel(gen_model)
self.assertEqual(201, resp.status)
self.assertIsNotNone(baymodel.uuid)
gen_model = datagen.valid_bay_data(baymodel_id=baymodel.uuid)
gen_model.keypair_id = 'aaa'
self.assertRaises(
exceptions.BadRequest,
self.bay_client.post_bay, gen_model)
resp, _ = self._delete_baymodel(baymodel.uuid)
self.assertEqual(204, resp.status)
@testtools.testcase.attr('negative')
def test_create_bay_with_nonexisting_external_network(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, baymodel = self._create_baymodel(gen_model)
self.assertEqual(201, resp.status)
self.assertIsNotNone(baymodel.uuid)
gen_model = datagen.valid_bay_data(baymodel_id=baymodel.uuid)
gen_model.external_network_id = 'aaa'
self.assertRaises(
exceptions.BadRequest,
self.bay_client.post_bay, gen_model)
resp, _ = self._delete_baymodel(baymodel.uuid)
self.assertEqual(204, resp.status)
@testtools.testcase.attr('negative')
def test_update_bay_for_nonexisting_bay(self):
patch_model = datagen.bay_name_patch_data()
self.assertRaises(
exceptions.NotFound,
self.bay_client.patch_bay, 'fooo', patch_model)
@testtools.testcase.attr('negative')
def test_delete_bay_for_nonexisting_bay(self):
self.assertRaises(
exceptions.NotFound,
self.bay_client.delete_bay, data_utils.rand_uuid())

View File

@ -1,207 +0,0 @@
# 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.lib.common.utils import data_utils
from tempest.lib import exceptions
import testtools
from magnum.tests.functional.api import base
from magnum.tests.functional.common import datagen
class BayModelTest(base.BaseTempestTest):
"""Tests for baymodel CRUD."""
def __init__(self, *args, **kwargs):
super(BayModelTest, self).__init__(*args, **kwargs)
self.baymodels = []
self.baymodel_client = None
self.keypairs_client = None
def setUp(self):
try:
super(BayModelTest, self).setUp()
(self.baymodel_client,
self.keypairs_client) = self.get_clients_with_new_creds(
type_of_creds='default',
request_type='baymodel')
except Exception:
self.tearDown()
raise
def tearDown(self):
for baymodel_id in self.baymodels:
self._delete_baymodel(baymodel_id)
self.baymodels.remove(baymodel_id)
super(BayModelTest, self).tearDown()
def _create_baymodel(self, baymodel_model):
resp, model = self.baymodel_client.post_baymodel(baymodel_model)
self.assertEqual(201, resp.status)
self.baymodels.append(model.uuid)
return resp, model
def _delete_baymodel(self, baymodel_id):
resp, model = self.baymodel_client.delete_baymodel(baymodel_id)
self.assertEqual(204, resp.status)
return resp, model
@testtools.testcase.attr('positive')
def test_list_baymodels(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
_, temp_model = self._create_baymodel(gen_model)
resp, model = self.baymodel_client.list_baymodels()
self.assertEqual(200, resp.status)
self.assertGreater(len(model.baymodels), 0)
self.assertIn(
temp_model.uuid, list([x['uuid'] for x in model.baymodels]))
@testtools.testcase.attr('positive')
def test_create_baymodel(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, model = self._create_baymodel(gen_model)
@testtools.testcase.attr('positive')
def test_create_get_public_baymodel(self):
gen_model = datagen.valid_swarm_mode_baymodel(is_public=True)
self.assertRaises(
exceptions.Forbidden,
self.baymodel_client.post_baymodel, gen_model)
@testtools.testcase.attr('positive')
def test_update_baymodel_public_by_uuid(self):
path = "/public"
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, old_model = self._create_baymodel(gen_model)
patch_model = datagen.baymodel_replace_patch_data(path, value=True)
self.assertRaises(
exceptions.Forbidden,
self.baymodel_client.patch_baymodel, old_model.uuid, patch_model)
@testtools.testcase.attr('positive')
def test_update_baymodel_by_uuid(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, old_model = self._create_baymodel(gen_model)
path = "/name"
patch_model = datagen.baymodel_replace_patch_data(path)
resp, new_model = self.baymodel_client.patch_baymodel(
old_model.uuid, patch_model)
self.assertEqual(200, resp.status)
resp, model = self.baymodel_client.get_baymodel(new_model.uuid)
self.assertEqual(200, resp.status)
self.assertEqual(old_model.uuid, new_model.uuid)
self.assertEqual(model.name, new_model.name)
@testtools.testcase.attr('positive')
def test_delete_baymodel_by_uuid(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, model = self._create_baymodel(gen_model)
resp, _ = self.baymodel_client.delete_baymodel(model.uuid)
self.assertEqual(204, resp.status)
self.baymodels.remove(model.uuid)
@testtools.testcase.attr('positive')
def test_delete_baymodel_by_name(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, model = self._create_baymodel(gen_model)
resp, _ = self.baymodel_client.delete_baymodel(model.name)
self.assertEqual(204, resp.status)
self.baymodels.remove(model.uuid)
@testtools.testcase.attr('negative')
def test_get_baymodel_by_uuid_404(self):
self.assertRaises(
exceptions.NotFound,
self.baymodel_client.get_baymodel, data_utils.rand_uuid())
@testtools.testcase.attr('negative')
def test_update_baymodel_404(self):
path = "/name"
patch_model = datagen.baymodel_replace_patch_data(path)
self.assertRaises(
exceptions.NotFound,
self.baymodel_client.patch_baymodel,
data_utils.rand_uuid(), patch_model)
@testtools.testcase.attr('negative')
def test_delete_baymodel_404(self):
self.assertRaises(
exceptions.NotFound,
self.baymodel_client.delete_baymodel, data_utils.rand_uuid())
@testtools.testcase.attr('negative')
def test_get_baymodel_by_name_404(self):
self.assertRaises(
exceptions.NotFound,
self.baymodel_client.get_baymodel, 'fooo')
@testtools.testcase.attr('negative')
def test_update_baymodel_name_not_found(self):
path = "/name"
patch_model = datagen.baymodel_replace_patch_data(path)
self.assertRaises(
exceptions.NotFound,
self.baymodel_client.patch_baymodel, 'fooo', patch_model)
@testtools.testcase.attr('negative')
def test_delete_baymodel_by_name_404(self):
self.assertRaises(
exceptions.NotFound,
self.baymodel_client.get_baymodel, 'fooo')
@testtools.testcase.attr('negative')
def test_create_baymodel_missing_image(self):
gen_model = datagen.baymodel_data_with_missing_image()
self.assertRaises(
exceptions.BadRequest,
self.baymodel_client.post_baymodel, gen_model)
@testtools.testcase.attr('negative')
def test_create_baymodel_missing_flavor(self):
gen_model = datagen.baymodel_data_with_missing_flavor()
self.assertRaises(
exceptions.BadRequest,
self.baymodel_client.post_baymodel, gen_model)
@testtools.testcase.attr('negative')
def test_update_baymodel_invalid_patch(self):
# get json object
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, old_model = self._create_baymodel(gen_model)
self.assertRaises(
exceptions.BadRequest,
self.baymodel_client.patch_baymodel, data_utils.rand_uuid(),
gen_model)
@testtools.testcase.attr('negative')
def test_create_baymodel_invalid_network_driver(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
gen_model.network_driver = 'invalid_network_driver'
self.assertRaises(
exceptions.BadRequest,
self.baymodel_client.post_baymodel, gen_model)
@testtools.testcase.attr('negative')
def test_create_baymodel_invalid_volume_driver(self):
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
gen_model.volume_driver = 'invalid_volume_driver'
self.assertRaises(
exceptions.BadRequest,
self.baymodel_client.post_baymodel, gen_model)

View File

@ -1,80 +0,0 @@
# 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 testtools
from magnum.tests.functional.api import base
from magnum.tests.functional.common import datagen
class BayModelAdminTest(base.BaseTempestTest):
"""Tests for baymodel admin operations."""
def __init__(self, *args, **kwargs):
super(BayModelAdminTest, self).__init__(*args, **kwargs)
self.baymodels = []
self.baymodel_client = None
self.keypairs_client = None
def setUp(self):
try:
super(BayModelAdminTest, self).setUp()
(self.baymodel_client,
self.keypairs_client) = self.get_clients_with_new_creds(
type_of_creds='admin',
request_type='baymodel')
except Exception:
self.tearDown()
raise
def tearDown(self):
for baymodel_id in self.baymodels:
self._delete_baymodel(baymodel_id)
self.baymodels.remove(baymodel_id)
super(BayModelAdminTest, self).tearDown()
def _create_baymodel(self, baymodel_model):
resp, model = self.baymodel_client.post_baymodel(baymodel_model)
self.assertEqual(201, resp.status)
self.baymodels.append(model.uuid)
return resp, model
def _delete_baymodel(self, baymodel_id):
resp, model = self.baymodel_client.delete_baymodel(baymodel_id)
self.assertEqual(204, resp.status)
return resp, model
@testtools.testcase.attr('positive')
def test_create_get_public_baymodel(self):
gen_model = datagen.valid_swarm_mode_baymodel(is_public=True)
resp, model = self._create_baymodel(gen_model)
resp, model = self.baymodel_client.get_baymodel(model.uuid)
self.assertEqual(200, resp.status)
self.assertTrue(model.public)
@testtools.testcase.attr('positive')
def test_update_baymodel_public_by_uuid(self):
path = "/public"
gen_model = datagen.baymodel_data_with_valid_keypair_image_flavor()
resp, old_model = self._create_baymodel(gen_model)
patch_model = datagen.baymodel_replace_patch_data(path, value=True)
resp, new_model = self.baymodel_client.patch_baymodel(
old_model.uuid, patch_model)
self.assertEqual(200, resp.status)
resp, model = self.baymodel_client.get_baymodel(new_model.uuid)
self.assertEqual(200, resp.status)
self.assertTrue(model.public)

View File

@ -1,262 +0,0 @@
# 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 fixtures
from oslo_log import log as logging
from oslo_utils import uuidutils
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions
import testtools
from magnum.tests.functional.api import base
from magnum.tests.functional.common import config
from magnum.tests.functional.common import datagen
HEADERS = {'OpenStack-API-Version': 'container-infra latest',
'Accept': 'application/json',
'Content-Type': 'application/json'}
class ClusterTest(base.BaseTempestTest):
"""Tests for cluster CRUD."""
LOG = logging.getLogger(__name__)
def __init__(self, *args, **kwargs):
super(ClusterTest, self).__init__(*args, **kwargs)
self.clusters = []
self.creds = None
self.keypair = None
self.cluster_template = None
self.cluster_template_client = None
self.keypairs_client = None
self.cluster_client = None
self.cert_client = None
def setUp(self):
try:
super(ClusterTest, self).setUp()
(self.creds, self.keypair) = self.get_credentials_with_keypair(
type_of_creds='default')
(self.cluster_template_client,
self.keypairs_client) = self.get_clients_with_existing_creds(
creds=self.creds,
type_of_creds='default',
request_type='cluster_template')
(self.cluster_client, _) = self.get_clients_with_existing_creds(
creds=self.creds,
type_of_creds='default',
request_type='cluster')
(self.cert_client, _) = self.get_clients_with_existing_creds(
creds=self.creds,
type_of_creds='default',
request_type='cert')
model = datagen.valid_swarm_mode_cluster_template()
_, self.cluster_template = self._create_cluster_template(model)
# NOTE (dimtruck) by default tempest sets timeout to 20 mins.
# We need more time.
test_timeout = 1800
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
except Exception:
self.tearDown()
raise
def tearDown(self):
try:
cluster_list = self.clusters[:]
for cluster_id in cluster_list:
self._delete_cluster(cluster_id)
self.clusters.remove(cluster_id)
if self.cluster_template:
self._delete_cluster_template(self.cluster_template.uuid)
finally:
super(ClusterTest, self).tearDown()
def _create_cluster_template(self, cm_model):
self.LOG.debug('We will create a clustertemplate for %s', cm_model)
resp, model = self.cluster_template_client.post_cluster_template(
cm_model)
return resp, model
def _delete_cluster_template(self, cm_id):
self.LOG.debug('We will delete a clustertemplate for %s', cm_id)
resp, model = self.cluster_template_client.delete_cluster_template(
cm_id)
return resp, model
def _create_cluster(self, cluster_model):
self.LOG.debug('We will create cluster for %s', cluster_model)
resp, model = self.cluster_client.post_cluster(cluster_model)
self.LOG.debug('Response: %s', resp)
self.assertEqual(202, resp.status)
self.assertIsNotNone(model.uuid)
self.assertTrue(uuidutils.is_uuid_like(model.uuid))
self.clusters.append(model.uuid)
self.cluster_uuid = model.uuid
if config.Config.copy_logs:
self.addCleanup(self.copy_logs_handler(
lambda: list(
[self._get_cluster_by_id(model.uuid)[1].master_addresses,
self._get_cluster_by_id(model.uuid)[1].node_addresses]),
self.cluster_template.coe,
self.keypair))
self.cluster_client.wait_for_created_cluster(model.uuid,
delete_on_error=False)
return resp, model
def _delete_cluster(self, cluster_id):
self.LOG.debug('We will delete a cluster for %s', cluster_id)
resp, model = self.cluster_client.delete_cluster(cluster_id)
self.assertEqual(204, resp.status)
self.cluster_client.wait_for_cluster_to_delete(cluster_id)
self.assertRaises(exceptions.NotFound, self.cert_client.get_cert,
cluster_id, headers=HEADERS)
return resp, model
def _get_cluster_by_id(self, cluster_id):
resp, model = self.cluster_client.get_cluster(cluster_id)
return resp, model
# (dimtruck) Combining all these tests in one because
# they time out on the gate (2 hours not enough)
@testtools.testcase.attr('positive')
def test_create_list_sign_delete_clusters(self):
gen_model = datagen.valid_cluster_data(
cluster_template_id=self.cluster_template.uuid, node_count=1)
# test cluster create
_, cluster_model = self._create_cluster(gen_model)
self.assertNotIn('status', cluster_model)
# test cluster list
resp, cluster_list_model = self.cluster_client.list_clusters()
self.assertEqual(200, resp.status)
self.assertGreater(len(cluster_list_model.clusters), 0)
self.assertIn(
cluster_model.uuid, list([x['uuid']
for x in cluster_list_model.clusters]))
# test invalid cluster update
patch_model = datagen.cluster_name_patch_data()
self.assertRaises(
exceptions.BadRequest,
self.cluster_client.patch_cluster,
cluster_model.uuid, patch_model)
# test ca show
resp, cert_model = self.cert_client.get_cert(
cluster_model.uuid, headers=HEADERS)
self.LOG.debug("cert resp: %s", resp)
self.assertEqual(200, resp.status)
self.assertEqual(cert_model.cluster_uuid, cluster_model.uuid)
self.assertIsNotNone(cert_model.pem)
self.assertIn('-----BEGIN CERTIFICATE-----', cert_model.pem)
self.assertIn('-----END CERTIFICATE-----', cert_model.pem)
# test ca sign
csr_sample = """-----BEGIN CERTIFICATE REQUEST-----
MIIByjCCATMCAQAwgYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgSW5jMR8w
HQYDVQQLExZJbmZvcm1hdGlvbiBUZWNobm9sb2d5MRcwFQYDVQQDEw53d3cuZ29v
Z2xlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApZtYJCHJ4VpVXHfV
IlstQTlO4qC03hjX+ZkPyvdYd1Q4+qbAeTwXmCUKYHThVRd5aXSqlPzyIBwieMZr
WFlRQddZ1IzXAlVRDWwAo60KecqeAXnnUK+5fXoTI/UgWshre8tJ+x/TMHaQKR/J
cIWPhqaQhsJuzZbvAdGA80BLxdMCAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBAIhl
4PvFq+e7ipARgI5ZM+GZx6mpCz44DTo0JkwfRDf+BtrsaC0q68eTf2XhYOsq4fkH
Q0uA0aVog3f5iJxCa3Hp5gxbJQ6zV6kJ0TEsuaaOhEko9sdpCoPOnRBm2i/XRD2D
6iNh8f8z0ShGsFqjDgFHyF3o+lUyj+UC6H1QW7bn
-----END CERTIFICATE REQUEST-----
"""
cert_data_model = datagen.cert_data(cluster_model.uuid,
csr_data=csr_sample)
resp, cert_model = self.cert_client.post_cert(cert_data_model,
headers=HEADERS)
self.LOG.debug("cert resp: %s", resp)
self.assertEqual(201, resp.status)
self.assertEqual(cert_model.cluster_uuid, cluster_model.uuid)
self.assertIsNotNone(cert_model.pem)
self.assertIn('-----BEGIN CERTIFICATE-----', cert_model.pem)
self.assertIn('-----END CERTIFICATE-----', cert_model.pem)
# test ca sign invalid
cert_data_model = datagen.cert_data(cluster_model.uuid,
csr_data="invalid_csr")
self.assertRaises(
exceptions.BadRequest,
self.cert_client.post_cert,
cert_data_model, headers=HEADERS)
# test cluster delete
self._delete_cluster(cluster_model.uuid)
self.clusters.remove(cluster_model.uuid)
@testtools.testcase.attr('negative')
def test_create_cluster_for_nonexisting_cluster_template(self):
cm_id = 'this-does-not-exist'
gen_model = datagen.valid_cluster_data(cluster_template_id=cm_id)
self.assertRaises(
exceptions.BadRequest,
self.cluster_client.post_cluster, gen_model)
@testtools.testcase.attr('negative')
def test_create_cluster_with_node_count_0(self):
gen_model = datagen.valid_cluster_data(
cluster_template_id=self.cluster_template.uuid, node_count=0)
self.assertRaises(
exceptions.BadRequest,
self.cluster_client.post_cluster, gen_model)
@testtools.testcase.attr('negative')
def test_create_cluster_with_zero_masters(self):
uuid = self.cluster_template.uuid
gen_model = datagen.valid_cluster_data(cluster_template_id=uuid,
master_count=0)
self.assertRaises(
exceptions.BadRequest,
self.cluster_client.post_cluster, gen_model)
@testtools.testcase.attr('negative')
def test_create_cluster_with_nonexisting_flavor(self):
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
resp, cluster_template = self._create_cluster_template(gen_model)
self.assertEqual(201, resp.status)
self.assertIsNotNone(cluster_template.uuid)
uuid = cluster_template.uuid
gen_model = datagen.valid_cluster_data(cluster_template_id=uuid)
gen_model.flavor_id = 'aaa'
self.assertRaises(exceptions.BadRequest,
self.cluster_client.post_cluster, gen_model)
resp, _ = self._delete_cluster_template(cluster_template.uuid)
self.assertEqual(204, resp.status)
@testtools.testcase.attr('negative')
def test_update_cluster_for_nonexisting_cluster(self):
patch_model = datagen.cluster_name_patch_data()
self.assertRaises(
exceptions.NotFound,
self.cluster_client.patch_cluster, 'fooo', patch_model)
@testtools.testcase.attr('negative')
def test_delete_cluster_for_nonexisting_cluster(self):
self.assertRaises(
exceptions.NotFound,
self.cluster_client.delete_cluster, data_utils.rand_uuid())

View File

@ -1,230 +0,0 @@
# 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.lib.common.utils import data_utils
from tempest.lib import exceptions
import testtools
from magnum.tests.functional.api import base
from magnum.tests.functional.common import datagen
class ClusterTemplateTest(base.BaseTempestTest):
"""Tests for clustertemplate CRUD."""
def __init__(self, *args, **kwargs):
super(ClusterTemplateTest, self).__init__(*args, **kwargs)
self.cluster_templates = []
self.cluster_template_client = None
self.keypairs_client = None
def setUp(self):
try:
super(ClusterTemplateTest, self).setUp()
(self.cluster_template_client,
self.keypairs_client) = self.get_clients_with_new_creds(
type_of_creds='default',
request_type='cluster_template')
except Exception:
self.tearDown()
raise
def tearDown(self):
for cluster_template_id in self.cluster_templates:
self._delete_cluster_template(cluster_template_id)
self.cluster_templates.remove(cluster_template_id)
super(ClusterTemplateTest, self).tearDown()
def _create_cluster_template(self, cmodel_model):
resp, model = \
self.cluster_template_client.post_cluster_template(cmodel_model)
self.assertEqual(201, resp.status)
self.cluster_templates.append(model.uuid)
return resp, model
def _delete_cluster_template(self, model_id):
resp, model = \
self.cluster_template_client.delete_cluster_template(model_id)
self.assertEqual(204, resp.status)
return resp, model
@testtools.testcase.attr('positive')
def test_list_cluster_templates(self):
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
_, temp_model = self._create_cluster_template(gen_model)
resp, model = self.cluster_template_client.list_cluster_templates()
self.assertEqual(200, resp.status)
self.assertGreater(len(model.clustertemplates), 0)
self.assertIn(
temp_model.uuid,
list([x['uuid'] for x in model.clustertemplates]))
@testtools.testcase.attr('positive')
def test_create_cluster_template(self):
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
resp, model = self._create_cluster_template(gen_model)
@testtools.testcase.attr('positive')
def test_create_get_public_cluster_template(self):
gen_model = datagen.valid_swarm_mode_cluster_template(is_public=True)
self.assertRaises(
exceptions.Forbidden,
self.cluster_template_client.post_cluster_template, gen_model)
@testtools.testcase.attr('positive')
def test_update_cluster_template_public_by_uuid(self):
path = "/public"
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
resp, old_model = self._create_cluster_template(gen_model)
patch_model = datagen.cluster_template_replace_patch_data(path,
value=True)
self.assertRaises(
exceptions.Forbidden,
self.cluster_template_client.patch_cluster_template,
old_model.uuid, patch_model)
@testtools.testcase.attr('positive')
def test_update_cluster_template_by_uuid(self):
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
resp, old_model = self._create_cluster_template(gen_model)
patch_model = datagen.cluster_template_name_patch_data()
resp, new_model = self.cluster_template_client.patch_cluster_template(
old_model.uuid, patch_model)
self.assertEqual(200, resp.status)
resp, model = \
self.cluster_template_client.get_cluster_template(new_model.uuid)
self.assertEqual(200, resp.status)
self.assertEqual(old_model.uuid, new_model.uuid)
self.assertEqual(model.name, new_model.name)
@testtools.testcase.attr('positive')
def test_delete_cluster_template_by_uuid(self):
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
resp, model = self._create_cluster_template(gen_model)
resp, _ = self.cluster_template_client.delete_cluster_template(
model.uuid)
self.assertEqual(204, resp.status)
self.cluster_templates.remove(model.uuid)
@testtools.testcase.attr('positive')
def test_delete_cluster_template_by_name(self):
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
resp, model = self._create_cluster_template(gen_model)
resp, _ = self.cluster_template_client.delete_cluster_template(
model.name)
self.assertEqual(204, resp.status)
self.cluster_templates.remove(model.uuid)
@testtools.testcase.attr('negative')
def test_get_cluster_template_by_uuid_404(self):
self.assertRaises(
exceptions.NotFound,
self.cluster_template_client.get_cluster_template,
data_utils.rand_uuid())
@testtools.testcase.attr('negative')
def test_update_cluster_template_404(self):
patch_model = datagen.cluster_template_name_patch_data()
self.assertRaises(
exceptions.NotFound,
self.cluster_template_client.patch_cluster_template,
data_utils.rand_uuid(), patch_model)
@testtools.testcase.attr('negative')
def test_delete_cluster_template_404(self):
self.assertRaises(
exceptions.NotFound,
self.cluster_template_client.delete_cluster_template,
data_utils.rand_uuid())
@testtools.testcase.attr('negative')
def test_get_cluster_template_by_name_404(self):
self.assertRaises(
exceptions.NotFound,
self.cluster_template_client.get_cluster_template, 'fooo')
@testtools.testcase.attr('negative')
def test_update_cluster_template_name_not_found(self):
patch_model = datagen.cluster_template_name_patch_data()
self.assertRaises(
exceptions.NotFound,
self.cluster_template_client.patch_cluster_template,
'fooo', patch_model)
@testtools.testcase.attr('negative')
def test_delete_cluster_template_by_name_404(self):
self.assertRaises(
exceptions.NotFound,
self.cluster_template_client.get_cluster_template, 'fooo')
@testtools.testcase.attr('negative')
def test_create_cluster_template_missing_image(self):
gen_model = datagen.cluster_template_data_with_missing_image()
self.assertRaises(
exceptions.BadRequest,
self.cluster_template_client.post_cluster_template, gen_model)
@testtools.testcase.attr('negative')
def test_create_cluster_template_missing_flavor(self):
gen_model = datagen.cluster_template_data_with_missing_flavor()
self.assertRaises(
exceptions.BadRequest,
self.cluster_template_client.post_cluster_template, gen_model)
@testtools.testcase.attr('positive')
def test_create_cluster_template_missing_keypair(self):
gen_model = \
datagen.cluster_template_data_with_missing_keypair()
resp, model = self._create_cluster_template(gen_model)
@testtools.testcase.attr('negative')
def test_update_cluster_template_invalid_patch(self):
# get json object
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
resp, old_model = self._create_cluster_template(gen_model)
self.assertRaises(
exceptions.BadRequest,
self.cluster_template_client.patch_cluster_template,
data_utils.rand_uuid(), gen_model)
@testtools.testcase.attr('negative')
def test_create_cluster_template_invalid_network_driver(self):
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
gen_model.network_driver = 'invalid_network_driver'
self.assertRaises(
exceptions.BadRequest,
self.cluster_template_client.post_cluster_template, gen_model)
@testtools.testcase.attr('negative')
def test_create_cluster_template_invalid_volume_driver(self):
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
gen_model.volume_driver = 'invalid_volume_driver'
self.assertRaises(
exceptions.BadRequest,
self.cluster_template_client.post_cluster_template, gen_model)

View File

@ -1,86 +0,0 @@
# 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 testtools
from magnum.tests.functional.api import base
from magnum.tests.functional.common import datagen
class ClusterTemplateAdminTest(base.BaseTempestTest):
"""Tests for clustertemplate admin operations."""
def __init__(self, *args, **kwargs):
super(ClusterTemplateAdminTest, self).__init__(*args, **kwargs)
self.cluster_templates = []
self.cluster_template_client = None
self.keypairs_client = None
def setUp(self):
try:
super(ClusterTemplateAdminTest, self).setUp()
(self.cluster_template_client,
self.keypairs_client) = self.get_clients_with_new_creds(
type_of_creds='admin',
request_type='cluster_template')
except Exception:
self.tearDown()
raise
def tearDown(self):
for cluster_template_id in self.cluster_templates:
self._delete_cluster_template(cluster_template_id)
self.cluster_templates.remove(cluster_template_id)
super(ClusterTemplateAdminTest, self).tearDown()
def _create_cluster_template(self, cmodel_model):
resp, model = \
self.cluster_template_client.post_cluster_template(cmodel_model)
self.assertEqual(201, resp.status)
self.cluster_templates.append(model.uuid)
return resp, model
def _delete_cluster_template(self, model_id):
resp, model = \
self.cluster_template_client.delete_cluster_template(model_id)
self.assertEqual(204, resp.status)
return resp, model
@testtools.testcase.attr('positive')
def test_create_get_public_cluster_template(self):
gen_model = datagen.valid_swarm_mode_cluster_template(is_public=True)
resp, model = self._create_cluster_template(gen_model)
resp, model = \
self.cluster_template_client.get_cluster_template(model.uuid)
self.assertEqual(200, resp.status)
self.assertTrue(model.public)
@testtools.testcase.attr('positive')
def test_update_cluster_template_public_by_uuid(self):
path = "/public"
gen_model = \
datagen.cluster_template_data_with_valid_keypair_image_flavor()
resp, old_model = self._create_cluster_template(gen_model)
patch_model = datagen.cluster_template_replace_patch_data(path,
value=True)
resp, new_model = self.cluster_template_client.patch_cluster_template(
old_model.uuid, patch_model)
self.assertEqual(200, resp.status)
resp, model = self.cluster_template_client.get_cluster_template(
new_model.uuid)
self.assertEqual(200, resp.status)
self.assertTrue(model.public)

View File

@ -1,53 +0,0 @@
# 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.lib import exceptions
import testtools
from magnum.tests.functional.api import base
class MagnumServiceTest(base.BaseTempestTest):
"""Tests for magnum-service ."""
def __init__(self, *args, **kwargs):
super(MagnumServiceTest, self).__init__(*args, **kwargs)
self.service_client = None
@testtools.testcase.attr('negative')
def test_magnum_service_list_needs_admin(self):
# Ensure that policy enforcement does not allow 'default' user
(self.service_client, _) = self.get_clients_with_new_creds(
type_of_creds='default',
request_type='service')
self.assertRaises(exceptions.Forbidden,
self.service_client.magnum_service_list)
@testtools.testcase.attr('positive')
def test_magnum_service_list(self):
# get json object
(self.service_client, _) = self.get_clients_with_new_creds(
type_of_creds='admin',
request_type='service',
class_cleanup=False)
resp, msvcs = self.service_client.magnum_service_list()
self.assertEqual(200, resp.status)
# Note(suro-patz): Following code assumes that we have only
# one service, magnum-conductor enabled, as of now.
self.assertEqual(1, len(msvcs.mservices))
mcond_svc = msvcs.mservices[0]
self.assertEqual(mcond_svc['id'], 1)
self.assertEqual('up', mcond_svc['state'])
self.assertEqual('magnum-conductor', mcond_svc['binary'])
self.assertGreater(mcond_svc['report_count'], 0)

View File

@ -1,67 +0,0 @@
# 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 __future__ import print_function
from oslo_config import cfg
from tempest import config # noqa
service_available_group = cfg.OptGroup(name="service_available",
title="Available OpenStack Services")
ServiceAvailableGroup = [
cfg.BoolOpt("magnum",
default=True,
help="Whether or not magnum is expected to be available"),
]
magnum_group = cfg.OptGroup(name="magnum", title="Magnum Options")
MagnumGroup = [
cfg.StrOpt("image_id",
default="fedora-atomic-latest",
help="Image id to be used for ClusterTemplate."),
cfg.StrOpt("nic_id",
default="public",
help="NIC id."),
cfg.StrOpt("keypair_id",
default="default",
help="Keypair id to use to log into nova instances."),
cfg.StrOpt("flavor_id",
default="s1.magnum",
help="Flavor id to use for ClusterTemplate."),
cfg.StrOpt("magnum_url",
help="Bypass URL for Magnum to skip service catalog lookup"),
cfg.StrOpt("master_flavor_id",
default="m1.magnum",
help="Master flavor id to use for ClusterTemplate."),
cfg.StrOpt("csr_location",
default="/opt/stack/new/magnum/default.csr",
deprecated_for_removal=True,
help="CSR location for certificates. This option is no "
"longer used for anything."),
cfg.StrOpt("dns_nameserver",
default="8.8.8.8",
help="DNS nameserver to use for ClusterTemplate."),
cfg.BoolOpt("copy_logs",
default=True,
help="Specify whether to copy nova server logs on failure."),
]

View File

@ -1,42 +0,0 @@
# 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
from tempest.test_discover import plugins
import magnum
from magnum.tests.functional.tempest_tests import config as magnum_config
class MagnumTempestPlugin(plugins.TempestPlugin):
def load_tests(self):
base_path = os.path.split(os.path.dirname(
os.path.abspath(magnum.__file__)))[0]
test_dir = "magnum/tests/functional/api/v1"
full_test_dir = os.path.join(base_path, test_dir)
return full_test_dir, base_path
def register_opts(self, conf):
config.register_opt_group(
conf, magnum_config.service_available_group,
magnum_config.ServiceAvailableGroup)
config.register_opt_group(conf, magnum_config.magnum_group,
magnum_config.MagnumGroup)
def get_opt_lists(self):
return [
(magnum_config.magnum_group.name, magnum_config.MagnumGroup),
('service_available', magnum_config.ServiceAvailableGroup)
]

View File

@ -1,5 +1,18 @@
- hosts: primary
tasks:
- shell:
cmd: |
set -e
set -x
cat << 'EOF' >>"/tmp/dg-local.conf"
[[local|localrc]]
# Enable Magnum Tempest plugin
TEMPEST_PLUGINS='/opt/stack/new/magnum-tempest-plugin'
EOF
executable: /bin/bash
chdir: '{{ ansible_user_dir }}/workspace'
environment: '{{ zuul | zuul_legacy_vars }}'
- shell:
cmd: |
set -e
@ -23,6 +36,7 @@
export PROJECTS="openstack/magnum $PROJECTS"
export PROJECTS="openstack/python-magnumclient $PROJECTS"
export PROJECTS="openstack/diskimage-builder $PROJECTS"
export PROJECTS="openstack/magnum-tempest-plugin $PROJECTS"
if [ "{{ multinode }}" -eq 1 ] ; then
export DEVSTACK_GATE_TOPOLOGY="multinode"

View File

@ -81,8 +81,6 @@ magnum.cert_manager.backend =
local = magnum.common.cert_manager.local_cert_manager
x509keypair = magnum.common.cert_manager.x509keypair_cert_manager
tempest.test_plugins =
magnum_tests = magnum.tests.functional.tempest_tests.plugin:MagnumTempestPlugin
[wheel]
universal = 1

View File

@ -20,7 +20,6 @@ os-testr>=1.0.0 # Apache-2.0
python-subunit>=1.0.0 # Apache-2.0/BSD
pytz>=2013.6 # MIT
sphinx>=1.6.2 # BSD
tempest>=17.1.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=2.2.0 # MIT