Rename Bay to Cluster in api

This is the first of several patches to add new Cluster commands
that will replace the Bay terminalogy in Magnum. This patch adds
the new Cluster and ClusterTemplate commands in addition to the
Bay and Baymodel commands.  Additional patches will be created
for client, docs, and additional functional tests.

Change-Id: Ie686281a6f98a1a9931158d2a79eee6ac21ed9a1
Implements: blueprint rename-bay-to-cluster
This commit is contained in:
Jaycen Grant 2016-08-10 13:29:31 -07:00
parent be0f0b1cc7
commit bea867c8d1
13 changed files with 1298 additions and 94 deletions

View File

@ -0,0 +1,176 @@
# 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_log import log as logging
from tempest.lib import exceptions
from magnum.i18n import _LE
from magnum.i18n import _LI
from magnum.i18n import _LW
from magnum.tests.functional.api.v1.models import cluster_id_model
from magnum.tests.functional.api.v1.models import cluster_model
from magnum.tests.functional.common import client
from magnum.tests.functional.common import utils
class ClusterClient(client.MagnumClient):
"""Encapsulates REST calls and maps JSON to/from models"""
LOG = logging.getLogger(__name__)
@classmethod
def clusters_uri(cls, filters=None):
"""Construct clusters uri with optional filters
:param filters: Optional k:v dict that's converted to url query
:returns: url string
"""
url = "/clusters"
if filters:
url = cls.add_filters(url, filters)
return url
@classmethod
def cluster_uri(cls, cluster_id):
"""Construct cluster uri
:param cluster_id: cluster uuid or name
:returns: url string
"""
return "{0}/{1}".format(cls.clusters_uri(), cluster_id)
def list_clusters(self, filters=None, **kwargs):
"""Makes GET /clusters request and returns ClusterCollection
Abstracts REST call to return all clusters
:param filters: Optional k:v dict that's converted to url query
:returns: response object and ClusterCollection object
"""
resp, body = self.get(self.clusters_uri(filters), **kwargs)
return self.deserialize(resp, body, cluster_model.ClusterCollection)
def get_cluster(self, cluster_id, **kwargs):
"""Makes GET /cluster request and returns ClusterEntity
Abstracts REST call to return a single cluster based on uuid or name
:param cluster_id: cluster uuid or name
:returns: response object and ClusterCollection object
"""
resp, body = self.get(self.cluster_uri(cluster_id))
return self.deserialize(resp, body, cluster_model.ClusterEntity)
def post_cluster(self, model, **kwargs):
"""Makes POST /cluster request and returns ClusterIdEntity
Abstracts REST call to create new cluster
:param model: ClusterEntity
:returns: response object and ClusterIdEntity object
"""
resp, body = self.post(
self.clusters_uri(),
body=model.to_json(), **kwargs)
return self.deserialize(resp, body, cluster_id_model.ClusterIdEntity)
def patch_cluster(self, cluster_id, clusterpatch_listmodel, **kwargs):
"""Makes PATCH /cluster request and returns ClusterIdEntity
Abstracts REST call to update cluster attributes
:param cluster_id: UUID of cluster
:param clusterpatch_listmodel: ClusterPatchCollection
:returns: response object and ClusterIdEntity object
"""
resp, body = self.patch(
self.cluster_uri(cluster_id),
body=clusterpatch_listmodel.to_json(), **kwargs)
return self.deserialize(resp, body, cluster_id_model.ClusterIdEntity)
def delete_cluster(self, cluster_id, **kwargs):
"""Makes DELETE /cluster request and returns response object
Abstracts REST call to delete cluster based on uuid or name
:param cluster_id: UUID or name of cluster
:returns: response object
"""
return self.delete(self.cluster_uri(cluster_id), **kwargs)
def wait_for_cluster_to_delete(self, cluster_id):
utils.wait_for_condition(
lambda: self.does_cluster_not_exist(cluster_id), 10, 600)
def wait_for_created_cluster(self, cluster_id, delete_on_error=True):
try:
utils.wait_for_condition(
lambda: self.does_cluster_exist(cluster_id), 10, 1800)
except Exception:
# In error state. Clean up the cluster id if desired
self.LOG.error(_LE('Cluster %s entered an exception state.') %
cluster_id)
if delete_on_error:
self.LOG.error(_LE('We will attempt to delete clusters now.'))
self.delete_cluster(cluster_id)
self.wait_for_cluster_to_delete(cluster_id)
raise
def wait_for_final_state(self, cluster_id):
utils.wait_for_condition(
lambda: self.is_cluster_in_final_state(cluster_id), 10, 1800)
def is_cluster_in_final_state(self, cluster_id):
try:
resp, model = self.get_cluster(cluster_id)
if model.status in ['CREATED', 'CREATE_COMPLETE',
'ERROR', 'CREATE_FAILED']:
self.LOG.info(_LI('Cluster %s succeeded.') % cluster_id)
return True
else:
return False
except exceptions.NotFound:
self.LOG.warning(_LW('Cluster %s is not found.') % cluster_id)
return False
def does_cluster_exist(self, cluster_id):
try:
resp, model = self.get_cluster(cluster_id)
if model.status in ['CREATED', 'CREATE_COMPLETE']:
self.LOG.info(_LI('Cluster %s is created.') % cluster_id)
return True
elif model.status in ['ERROR', 'CREATE_FAILED']:
self.LOG.error(_LE('Cluster %s is in fail state.') %
cluster_id)
raise exceptions.ServerFault(
"Got into an error condition: %s for %s" %
(model.status, cluster_id))
else:
return False
except exceptions.NotFound:
self.LOG.warning(_LW('Cluster %s is not found.') % cluster_id)
return False
def does_cluster_not_exist(self, cluster_id):
try:
self.get_cluster(cluster_id)
except exceptions.NotFound:
self.LOG.warning(_LW('Cluster %s is not found.') % cluster_id)
return True
return False

View File

@ -0,0 +1,113 @@
# 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 magnum.tests.functional.api.v1.models import cluster_template_model
from magnum.tests.functional.common import client
class ClusterTemplateClient(client.MagnumClient):
"""Encapsulates REST calls and maps JSON to/from models"""
@classmethod
def cluster_templates_uri(cls, filters=None):
"""Construct clustertemplates uri with optional filters
:param filters: Optional k:v dict that's converted to url query
:returns: url string
"""
url = "/clustertemplates"
if filters:
url = cls.add_filters(url, filters)
return url
@classmethod
def cluster_template_uri(cls, cluster_template_id):
"""Construct cluster_template uri
:param cluster_template_id: cluster_template uuid or name
:returns: url string
"""
return "{0}/{1}".format(cls.cluster_templates_uri(),
cluster_template_id)
def list_cluster_templates(self, filters=None, **kwargs):
"""Makes GET /clustertemplates request
Abstracts REST call to return all clustertemplates
:param filters: Optional k:v dict that's converted to url query
:returns: response object and ClusterTemplateCollection object
"""
resp, body = self.get(self.cluster_templates_uri(filters), **kwargs)
collection = cluster_template_model.ClusterTemplateCollection
return self.deserialize(resp, body, collection)
def get_cluster_template(self, cluster_template_id, **kwargs):
"""Makes GET /clustertemplate request and returns ClusterTemplateEntity
Abstracts REST call to return a single clustertempalte based on uuid
or name
:param cluster_template_id: clustertempalte uuid or name
:returns: response object and ClusterTemplateCollection object
"""
resp, body = self.get(self.cluster_template_uri(cluster_template_id))
return self.deserialize(resp, body,
cluster_template_model.ClusterTemplateEntity)
def post_cluster_template(self, model, **kwargs):
"""Makes POST /clustertemplate request
Abstracts REST call to create new clustertemplate
:param model: ClusterTemplateEntity
:returns: response object and ClusterTemplateEntity object
"""
resp, body = self.post(
self.cluster_templates_uri(),
body=model.to_json(), **kwargs)
entity = cluster_template_model.ClusterTemplateEntity
return self.deserialize(resp, body, entity)
def patch_cluster_template(self, cluster_template_id,
cluster_templatepatch_listmodel, **kwargs):
"""Makes PATCH /clustertemplate and returns ClusterTemplateEntity
Abstracts REST call to update clustertemplate attributes
:param cluster_template_id: UUID of clustertemplate
:param cluster_templatepatch_listmodel: ClusterTemplatePatchCollection
:returns: response object and ClusterTemplateEntity object
"""
resp, body = self.patch(
self.cluster_template_uri(cluster_template_id),
body=cluster_templatepatch_listmodel.to_json(), **kwargs)
return self.deserialize(resp, body,
cluster_template_model.ClusterTemplateEntity)
def delete_cluster_template(self, cluster_template_id, **kwargs):
"""Makes DELETE /clustertemplate request and returns response object
Abstracts REST call to delete clustertemplate based on uuid or name
:param cluster_template_id: UUID or name of clustertemplate
:returns: response object
"""
return self.delete(self.cluster_template_uri(cluster_template_id),
**kwargs)

View File

@ -0,0 +1,24 @@
# 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 magnum.tests.functional.common import models
class ClusterIdData(models.BaseModel):
"""Data that encapsulates ClusterId attributes"""
pass
class ClusterIdEntity(models.EntityModel):
"""Entity Model that represents a single instance of CertData"""
ENTITY_NAME = 'clusterid'
MODEL_TYPE = ClusterIdData

View File

@ -0,0 +1,30 @@
# 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 magnum.tests.functional.common import models
class ClusterData(models.BaseModel):
"""Data that encapsulates cluster attributes"""
pass
class ClusterEntity(models.EntityModel):
"""Entity Model that represents a single instance of ClusterData"""
ENTITY_NAME = 'cluster'
MODEL_TYPE = ClusterData
class ClusterCollection(models.CollectionModel):
"""Collection Model that represents a list of ClusterData objects"""
COLLECTION_NAME = 'clusterlists'
MODEL_TYPE = ClusterData

View File

@ -0,0 +1,30 @@
# 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 magnum.tests.functional.common import models
class ClusterTemplateData(models.BaseModel):
"""Data that encapsulates clustertemplate attributes"""
pass
class ClusterTemplateEntity(models.EntityModel):
"""Entity Model that represents a single instance of ClusterTemplateData"""
ENTITY_NAME = 'clustertemplate'
MODEL_TYPE = ClusterTemplateData
class ClusterTemplateCollection(models.CollectionModel):
"""Collection that represents a list of ClusterTemplateData objects"""
COLLECTION_NAME = 'clustertemplatelists'
MODEL_TYPE = ClusterTemplateData

View File

@ -0,0 +1,77 @@
# 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 magnum.tests.functional.common import models
class ClusterTemplatePatchData(models.BaseModel):
"""Data that encapsulates clustertemplatepatch attributes"""
pass
class ClusterTemplatePatchEntity(models.EntityModel):
"""Model that represents a single instance of ClusterTemplatePatchData"""
ENTITY_NAME = 'clustertemplatepatch'
MODEL_TYPE = ClusterTemplatePatchData
class ClusterTemplatePatchCollection(models.CollectionModel):
"""Model that represents a list of ClusterTemplatePatchData objects"""
MODEL_TYPE = ClusterTemplatePatchData
COLLECTION_NAME = 'clustertemplatepatchlist'
def to_json(self):
"""Converts ClusterTemplatePatchCollection to json
Retrieves list from COLLECTION_NAME attribute and converts each object
to dict, appending it to a list. Then converts the entire list to
json
This is required due to COLLECTION_NAME holding a list of objects that
needed to be converted to dict individually
:returns: json object
"""
data = getattr(self, ClusterTemplatePatchCollection.COLLECTION_NAME)
collection = []
for d in data:
collection.append(d.to_dict())
return json.dumps(collection)
@classmethod
def from_dict(cls, data):
"""Converts dict to ClusterTemplatePatchData
Converts data dict to list of ClusterTemplatePatchData objects and
stores it in COLLECTION_NAME
Example of dict data:
[{
"path": "/name",
"value": "myname",
"op": "replace"
}]
:param data: dict of patch data
:returns: json object
"""
model = cls()
collection = []
for d in data:
collection.append(cls.MODEL_TYPE.from_dict(d))
setattr(model, cls.COLLECTION_NAME, collection)
return model

View File

@ -0,0 +1,76 @@
# 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 magnum.tests.functional.common import models
class ClusterPatchData(models.BaseModel):
"""Data that encapsulates clusterpatch attributes"""
pass
class ClusterPatchEntity(models.EntityModel):
"""Entity Model that represents a single instance of ClusterPatchData"""
ENTITY_NAME = 'clusterpatch'
MODEL_TYPE = ClusterPatchData
class ClusterPatchCollection(models.CollectionModel):
"""Collection Model that represents a list of ClusterPatchData objects"""
MODEL_TYPE = ClusterPatchData
COLLECTION_NAME = 'clusterpatchlist'
def to_json(self):
"""Converts ClusterPatchCollection to json
Retrieves list from COLLECTION_NAME attribute and converts each object
to dict, appending it to a list. Then converts the entire list to json
This is required due to COLLECTION_NAME holding a list of objects that
needed to be converted to dict individually
:returns: json object
"""
data = getattr(self, ClusterPatchCollection.COLLECTION_NAME)
collection = []
for d in data:
collection.append(d.to_dict())
return json.dumps(collection)
@classmethod
def from_dict(cls, data):
"""Converts dict to ClusterPatchData
Converts data dict to list of ClusterPatchData objects and stores it
in COLLECTION_NAME
Example of dict data:
[{
"path": "/name",
"value": "myname",
"op": "replace"
}]
:param data: dict of patch data
:returns: json object
"""
model = cls()
collection = []
for d in data:
collection.append(cls.MODEL_TYPE.from_dict(d))
setattr(model, cls.COLLECTION_NAME, collection)
return model

View File

@ -18,7 +18,6 @@ from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions
import testtools
from magnum.objects.fields import BayStatus
from magnum.tests.functional.api import base
from magnum.tests.functional.common import config
from magnum.tests.functional.common import datagen
@ -131,56 +130,6 @@ class BayTest(base.BaseTempestTest):
resp, model = self.bay_client.get_bay(bay_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_and_delete_bays(self):
gen_model = datagen.valid_bay_data(
baymodel_id=self.baymodel.uuid, node_count=1)
# test bay create
_, temp_model = self._create_bay(gen_model)
self.assertEqual(BayStatus.CREATE_IN_PROGRESS, temp_model.status)
self.assertIsNone(temp_model.status_reason)
# test bay list
resp, model = self.bay_client.list_bays()
self.assertEqual(200, resp.status)
self.assertGreater(len(model.bays), 0)
self.assertIn(
temp_model.uuid, list([x['uuid'] for x in model.bays]))
# test invalid bay update
patch_model = datagen.bay_name_patch_data()
self.assertRaises(
exceptions.BadRequest,
self.bay_client.patch_bay,
temp_model.uuid, patch_model)
# test bay delete
self._delete_bay(temp_model.uuid)
self.bays.remove(temp_model.uuid)
@testtools.testcase.attr('positive')
def test_create_delete_bays_async(self):
gen_model = datagen.valid_bay_data(
baymodel_id=self.baymodel.uuid, node_count=1)
# test bay create
_, temp_model = self._create_bay(gen_model, is_async=True)
self.assertNotIn('status', temp_model)
# test bay list
resp, model = self.bay_client.list_bays()
self.assertEqual(200, resp.status)
self.assertGreater(len(model.bays), 0)
self.assertIn(
temp_model.uuid, list([x['uuid'] for x in model.bays]))
# test bay delete
self._delete_bay(temp_model.uuid)
self.bays.remove(temp_model.uuid)
@testtools.testcase.attr('negative')
def test_create_bay_for_nonexisting_baymodel(self):
gen_model = datagen.valid_bay_data(baymodel_id='this-does-not-exist')
@ -265,37 +214,3 @@ class BayTest(base.BaseTempestTest):
self.assertRaises(
exceptions.NotFound,
self.bay_client.delete_bay, data_utils.rand_uuid())
@testtools.testcase.attr('positive')
def test_certificate_sign_and_show(self):
first_model = datagen.valid_bay_data(baymodel_id=self.baymodel.uuid,
name='test')
_, bay_model = self._create_bay(first_model)
# test ca show
resp, model = self.cert_client.get_cert(
bay_model.uuid)
self.LOG.debug("cert resp: %s" % resp)
self.assertEqual(200, resp.status)
self.assertEqual(model.bay_uuid, bay_model.uuid)
self.assertIsNotNone(model.pem)
self.assertIn('-----BEGIN CERTIFICATE-----', model.pem)
self.assertIn('-----END CERTIFICATE-----', model.pem)
# test ca sign
model = datagen.cert_data(bay_uuid=bay_model.uuid)
resp, model = self.cert_client.post_cert(model)
self.LOG.debug("cert resp: %s" % resp)
self.assertEqual(201, resp.status)
self.assertEqual(model.bay_uuid, bay_model.uuid)
self.assertIsNotNone(model.pem)
self.assertIn('-----BEGIN CERTIFICATE-----', model.pem)
self.assertIn('-----END CERTIFICATE-----', model.pem)
# test ca sign invalid
model = datagen.cert_data(bay_uuid=bay_model.uuid,
csr_data="invalid_csr")
self.assertRaises(
exceptions.BadRequest,
self.cert_client.post_cert,
model)

View File

@ -0,0 +1,240 @@
# 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 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_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.addOnException(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)
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)
self.LOG.debug("cert resp: %s" % resp)
self.assertEqual(200, resp.status)
self.assertEqual(cert_model.bay_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
cert_data_model = datagen.cert_data(cluster_model.uuid)
resp, cert_model = self.cert_client.post_cert(cert_data_model)
self.LOG.debug("cert resp: %s" % resp)
self.assertEqual(201, resp.status)
self.assertEqual(cert_model.bay_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)
# 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

@ -0,0 +1,239 @@
# 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_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)
@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('negative')
def test_create_cluster_template_missing_keypair(self):
gen_model = \
datagen.cluster_template_data_with_missing_keypair()
self.assertRaises(
exceptions.NotFound,
self.cluster_template_client.post_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

@ -79,9 +79,9 @@ class BaseMagnumTest(base.BaseTestCase):
])
except Exception:
cls.LOG.error(msg)
msg = (_LE("failed to copy from %{node_address}s "
"to %{base_path}s%{log_name}s-"
"%{node_address}s") %
msg = (_LE("failed to copy from %(node_address)s "
"to %(base_path)s%(log_name)s-"
"%(node_address)s") %
{'node_address': node_address,
'base_path': "/opt/stack/logs/bay-nodes/",
'log_name': log_name})

View File

@ -23,6 +23,10 @@ from magnum.tests.functional.api.v1.models import baymodel_model
from magnum.tests.functional.api.v1.models import baymodelpatch_model
from magnum.tests.functional.api.v1.models import baypatch_model
from magnum.tests.functional.api.v1.models import cert_model
from magnum.tests.functional.api.v1.models import cluster_model
from magnum.tests.functional.api.v1.models import cluster_template_model
from magnum.tests.functional.api.v1.models import cluster_templatepatch_model
from magnum.tests.functional.api.v1.models import clusterpatch_model
from magnum.tests.functional.common import config
@ -334,3 +338,277 @@ def cert_data(bay_uuid, csr_data=None):
model = cert_model.CertEntity.from_dict(data)
return model
def cluster_template_data(**kwargs):
"""Generates random cluster_template data
Keypair and image id cannot be random for the cluster_template to be valid
due to validations for the presence of keypair and image id prior to
cluster_template creation.
:param keypair_id: keypair name
:param image_id: image id or name
:returns: ClusterTemplateEntity with generated data
"""
data = {
"name": data_utils.rand_name('cluster'),
"coe": "swarm",
"tls_disabled": False,
"network_driver": None,
"volume_driver": None,
"docker_volume_size": 3,
"labels": {},
"public": False,
"fixed_network": "192.168.0.0/24",
"dns_nameserver": "8.8.8.8",
"flavor_id": data_utils.rand_name('cluster'),
"master_flavor_id": data_utils.rand_name('cluster'),
"external_network_id": config.Config.nic_id,
"keypair_id": data_utils.rand_name('cluster'),
"image_id": data_utils.rand_name('cluster')
}
data.update(kwargs)
model = cluster_template_model.ClusterTemplateEntity.from_dict(data)
return model
def cluster_template_replace_patch_data(path,
value=data_utils.rand_name('cluster')):
"""Generates random ClusterTemplate patch data
:param path: path to replace
:param value: value to replace in patch
:returns: ClusterTemplatePatchCollection with generated data
"""
data = [{
"path": path,
"value": value,
"op": "replace"
}]
collection = cluster_templatepatch_model.ClusterTemplatePatchCollection
return collection.from_dict(data)
def cluster_template_remove_patch_data(path):
"""Generates ClusterTempalte patch data by removing value
:param path: path to remove
:returns: BayModelPatchCollection with generated data
"""
data = [{
"path": path,
"op": "remove"
}]
collection = cluster_templatepatch_model.ClusterTemplatePatchCollection
return collection.from_dict(data)
def cluster_template_name_patch_data(name=data_utils.rand_name('cluster')):
"""Generates random cluster_template patch data
:param name: name to replace in patch
:returns: ClusterTemplatePatchCollection with generated data
"""
data = [{
"path": "/name",
"value": name,
"op": "replace"
}]
collection = cluster_templatepatch_model.ClusterTemplatePatchCollection
return collection.from_dict(data)
def cluster_template_flavor_patch_data(flavor=data_utils.rand_name('cluster')):
"""Generates random cluster_template patch data
:param flavor: flavor to replace in patch
:returns: ClusterTemplatePatchCollection with generated data
"""
data = [{
"path": "/flavor_id",
"value": flavor,
"op": "replace"
}]
collection = cluster_templatepatch_model.ClusterTemplatePatchCollection
return collection.from_dict(data)
def cluster_template_data_with_valid_keypair_image_flavor():
"""Generates random clustertemplate data with valid data
:returns: ClusterTemplateEntity with generated data
"""
master_flavor = config.Config.master_flavor_id
return cluster_template_data(keypair_id=config.Config.keypair_id,
image_id=config.Config.image_id,
flavor_id=config.Config.flavor_id,
master_flavor_id=master_flavor)
def cluster_template_data_with_missing_image():
"""Generates random cluster_template data with missing image
:returns: ClusterTemplateEntity with generated data
"""
return cluster_template_data(
keypair_id=config.Config.keypair_id,
flavor_id=config.Config.flavor_id,
master_flavor_id=config.Config.master_flavor_id)
def cluster_template_data_with_missing_flavor():
"""Generates random cluster_template data with missing flavor
:returns: ClusterTemplateEntity with generated data
"""
return cluster_template_data(keypair_id=config.Config.keypair_id,
image_id=config.Config.image_id)
def cluster_template_data_with_missing_keypair():
"""Generates random cluster_template data with missing keypair
:returns: ClusterTemplateEntity with generated data
"""
return cluster_template_data(
image_id=config.Config.image_id,
flavor_id=config.Config.flavor_id,
master_flavor_id=config.Config.master_flavor_id)
def cluster_template_valid_data_with_specific_coe(coe):
"""Generates random cluster_template data with valid keypair and image
:param coe: coe
:returns: ClusterTemplateEntity with generated data
"""
return cluster_template_data(keypair_id=config.Config.keypair_id,
image_id=config.Config.image_id, coe=coe)
def valid_swarm_cluster_template(is_public=False):
"""Generates a valid swarm cluster_template with valid data
:returns: ClusterTemplateEntity with generated data
"""
master_flavor_id = config.Config.master_flavor_id
return cluster_template_data(image_id=config.Config.image_id,
fixed_network="192.168.0.0/24",
flavor_id=config.Config.flavor_id,
public=is_public,
dns_nameserver=config.Config.dns_nameserver,
master_flavor_id=master_flavor_id,
keypair_id=config.Config.keypair_id,
coe="swarm", docker_volume_size=3,
cluster_distro=None,
external_network_id=config.Config.nic_id,
http_proxy=None, https_proxy=None,
no_proxy=None, network_driver=None,
volume_driver=None, labels={},
tls_disabled=False)
def cluster_data(name=data_utils.rand_name('cluster'),
cluster_template_id=data_utils.rand_uuid(),
node_count=random_int(1, 5), discovery_url=gen_random_ip(),
create_timeout=random_int(1, 30),
master_count=random_int(1, 5)):
"""Generates random cluster data
cluster_template_id cannot be random for the cluster to be valid due to
validations for the presence of clustertemplate prior to clustertemplate
creation.
:param name: cluster name (must be unique)
:param cluster_template_id: clustertemplate unique id (must already exist)
:param node_count: number of agents for cluster
:param discovery_url: url provided for node discovery
:param create_timeout: timeout in minutes for cluster create
:param master_count: number of master nodes for the cluster
:returns: ClusterEntity with generated data
"""
data = {
"name": name,
"cluster_template_id": cluster_template_id,
"node_count": node_count,
"discovery_url": None,
"create_timeout": create_timeout,
"master_count": master_count
}
model = cluster_model.ClusterEntity.from_dict(data)
return model
def valid_cluster_data(cluster_template_id,
name=data_utils.rand_name('cluster'),
node_count=1, master_count=1, create_timeout=None):
"""Generates random cluster data with valid
:param cluster_template_id: clustertemplate unique id that already exists
:param name: cluster name (must be unique)
:param node_count: number of agents for cluster
:returns: ClusterEntity with generated data
"""
return cluster_data(cluster_template_id=cluster_template_id, name=name,
master_count=master_count, node_count=node_count,
create_timeout=create_timeout)
def cluster_name_patch_data(name=data_utils.rand_name('cluster')):
"""Generates random clustertemplate patch data
:param name: name to replace in patch
:returns: ClusterPatchCollection with generated data
"""
data = [{
"path": "/name",
"value": name,
"op": "replace"
}]
return clusterpatch_model.ClusterPatchCollection.from_dict(data)
def cluster_api_addy_patch_data(address='0.0.0.0'):
"""Generates random cluster patch data
:param name: name to replace in patch
:returns: ClusterPatchCollection with generated data
"""
data = [{
"path": "/api_address",
"value": address,
"op": "replace"
}]
return clusterpatch_model.ClusterPatchCollection.from_dict(data)
def cluster_node_count_patch_data(node_count=2):
"""Generates random cluster patch data
:param name: name to replace in patch
:returns: ClusterPatchCollection with generated data
"""
data = [{
"path": "/node_count",
"value": node_count,
"op": "replace"
}]
return clusterpatch_model.ClusterPatchCollection.from_dict(data)

View File

@ -16,6 +16,8 @@ from tempest.common import credentials_factory as common_creds
from magnum.tests.functional.api.v1.clients import bay_client
from magnum.tests.functional.api.v1.clients import baymodel_client
from magnum.tests.functional.api.v1.clients import cert_client
from magnum.tests.functional.api.v1.clients import cluster_client
from magnum.tests.functional.api.v1.clients import cluster_template_client
from magnum.tests.functional.api.v1.clients import magnum_service_client
from magnum.tests.functional.common import client
from magnum.tests.functional.common import config
@ -29,17 +31,21 @@ class Manager(clients.Manager):
super(Manager, self).__init__(credentials, 'container-infra')
self.auth_provider.orig_base_url = self.auth_provider.base_url
self.auth_provider.base_url = self.bypassed_base_url
auth = self.auth_provider
if request_type == 'baymodel':
self.client = baymodel_client.BayModelClient(self.auth_provider)
self.client = baymodel_client.BayModelClient(auth)
elif request_type == 'bay':
self.client = bay_client.BayClient(self.auth_provider)
self.client = bay_client.BayClient(auth)
elif request_type == 'cert':
self.client = cert_client.CertClient(self.auth_provider)
self.client = cert_client.CertClient(auth)
elif request_type == 'cluster_template':
self.client = cluster_template_client.ClusterTemplateClient(auth)
elif request_type == 'cluster':
self.client = cluster_client.ClusterClient(auth)
elif request_type == 'service':
self.client = magnum_service_client.MagnumServiceClient(
self.auth_provider)
self.client = magnum_service_client.MagnumServiceClient(auth)
else:
self.client = client.MagnumClient(self.auth_provider)
self.client = client.MagnumClient(auth)
def bypassed_base_url(self, filters, auth_data=None):
if (config.Config.magnum_url and