diff --git a/magnum/tests/contrib/post_test_hook.sh b/magnum/tests/contrib/post_test_hook.sh index 1d07c6406a..80857cb382 100755 --- a/magnum/tests/contrib/post_test_hook.sh +++ b/magnum/tests/contrib/post_test_hook.sh @@ -59,6 +59,7 @@ keypair_id = default flavor_id = m1.magnum2 master_flavor_id = m1.magnum copy_logs = true +csr_location = $MAGNUM_DIR/default.csr EOF # Note(eliqiao): Let's keep this only for debugging on gate. @@ -70,6 +71,40 @@ EOF ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa nova keypair-add --pub-key ~/.ssh/id_rsa.pub default + # create a valid sample csr + export CSR_FILE=$MAGNUM_DIR/default.csr + cat < $CSR_FILE +-----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----- +EOF + + # create an ivalid sample csr + export INVALID_CSR_FILE=$MAGNUM_DIR/invalid.csr + cat < $INVALID_CSR_FILE +-----BEGIN CERTIFICATE REQUEST----- +FAKERFAKERyjCCATMCAQAwgYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh +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----- +EOF + } function add_flavor { @@ -149,6 +184,7 @@ if [[ "api" == "$coe" ]]; then iniset $BASE/new/tempest/etc/tempest.conf magnum keypair_id default iniset $BASE/new/tempest/etc/tempest.conf magnum flavor_id m1.magnum2 iniset $BASE/new/tempest/etc/tempest.conf magnum master_flavor_id m1.magnum + iniset $BASE/new/tempest/etc/tempest.conf magnum csr_location $CSR_FILE # show tempest config with magnum cat etc/tempest.conf diff --git a/magnum/tests/functional/api/v1/clients/bay_client.py b/magnum/tests/functional/api/v1/clients/bay_client.py index 2ed98fdeb7..cebbc512bf 100644 --- a/magnum/tests/functional/api/v1/clients/bay_client.py +++ b/magnum/tests/functional/api/v1/clients/bay_client.py @@ -111,16 +111,32 @@ class BayClient(client.MagnumClient): utils.wait_for_condition( lambda: self.does_bay_not_exist(bay_id), 10, 3600) - def wait_for_created_bay(self, bay_id): + def wait_for_created_bay(self, bay_id, delete_on_error=True): try: utils.wait_for_condition( lambda: self.does_bay_exist(bay_id), 10, 3600) except Exception: - # In error state. Clean up the bay id - self.delete_bay(bay_id) - self.wait_for_bay_to_delete(bay_id) + # In error state. Clean up the bay id if desired + if delete_on_error: + self.delete_bay(bay_id) + self.wait_for_bay_to_delete(bay_id) raise + def wait_for_final_state(self, bay_id): + utils.wait_for_condition( + lambda: self.is_bay_in_final_state(bay_id), 10, 3600) + + def is_bay_in_final_state(self, bay_id): + try: + resp, model = self.get_bay(bay_id) + if model.status in ['CREATED', 'CREATE_COMPLETE', + 'ERROR', 'CREATE_FAILED']: + return True + else: + return False + except exceptions.NotFound: + return False + def does_bay_exist(self, bay_id): try: resp, model = self.get_bay(bay_id) diff --git a/magnum/tests/functional/api/v1/clients/cert_client.py b/magnum/tests/functional/api/v1/clients/cert_client.py new file mode 100644 index 0000000000..e896ee9fbb --- /dev/null +++ b/magnum/tests/functional/api/v1/clients/cert_client.py @@ -0,0 +1,56 @@ +# 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 cert_model +from magnum.tests.functional.common import client + + +class CertClient(client.MagnumClient): + """Encapsulates REST calls and maps JSON to/from models""" + + url = "/certificates" + + @classmethod + def cert_uri(cls, bay_id): + """Construct bay uri + + :param bay_id: bay uuid or name + :returns: url string + """ + + return "{0}/{1}".format(cls.url, bay_id) + + def get_cert(self, bay_id, **kwargs): + """Makes GET /certificates/bay_id request and returns CertEntity + + Abstracts REST call to return a single cert based on uuid or name + + :param bay_id: bay uuid or name + :returns: response object and BayCollection object + """ + + resp, body = self.get(self.cert_uri(bay_id)) + return self.deserialize(resp, body, cert_model.CertEntity) + + def post_cert(self, model, **kwargs): + """Makes POST /certificates request and returns CertEntity + + Abstracts REST call to sign new certificate + + :param model: CertEntity + :returns: response object and CertEntity object + """ + + resp, body = self.post( + CertClient.url, + body=model.to_json(), **kwargs) + return self.deserialize(resp, body, cert_model.CertEntity) diff --git a/magnum/tests/functional/api/v1/models/cert_model.py b/magnum/tests/functional/api/v1/models/cert_model.py new file mode 100644 index 0000000000..4948a392ae --- /dev/null +++ b/magnum/tests/functional/api/v1/models/cert_model.py @@ -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 CertData(models.BaseModel): + """Data that encapsulates cert attributes""" + pass + + +class CertEntity(models.EntityModel): + """Entity Model that represents a single instance of CertData""" + ENTITY_NAME = 'certificate' + MODEL_TYPE = CertData diff --git a/magnum/tests/functional/api/v1/test_bay.py b/magnum/tests/functional/api/v1/test_bay.py index 0d754194fb..e309daf30d 100644 --- a/magnum/tests/functional/api/v1/test_bay.py +++ b/magnum/tests/functional/api/v1/test_bay.py @@ -35,6 +35,7 @@ class BayTest(base.BaseMagnumTest): self.baymodel_client = None self.keypairs_client = None self.bay_client = None + self.cert_client = None def setUp(self): try: @@ -49,6 +50,10 @@ class BayTest(base.BaseMagnumTest): creds=self.credentials, type_of_creds='default', request_type='bay') + (self.cert_client, _) = self.get_clients_with_existing_creds( + creds=self.credentials, + type_of_creds='default', + request_type='cert') model = datagen.valid_swarm_baymodel() _, self.baymodel = self._create_baymodel(model) @@ -108,12 +113,25 @@ class BayTest(base.BaseMagnumTest): 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) + + # test bay list resp, model = self.bay_client.list_bays() self.assertEqual(resp.status, 200) 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) @@ -153,3 +171,39 @@ class BayTest(base.BaseMagnumTest): 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.info("cert resp: %s" % resp) + self.LOG.info("cert model: %s" % model) + self.assertEqual(resp.status, 200) + 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.info("cert resp: %s" % resp) + self.LOG.info("cert model: %s" % model) + self.assertEqual(resp.status, 201) + 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_path") + self.assertRaises( + exceptions.ServerFault, + self.cert_client.post_cert, + model) diff --git a/magnum/tests/functional/api/v1/test_magnum_service.py b/magnum/tests/functional/api/v1/test_magnum_service.py index 97124967d3..32cf240172 100644 --- a/magnum/tests/functional/api/v1/test_magnum_service.py +++ b/magnum/tests/functional/api/v1/test_magnum_service.py @@ -39,7 +39,8 @@ class MagnumServiceTest(base.BaseMagnumTest): # get json object (self.service_client, _) = self.get_clients_with_new_creds( type_of_creds='admin', - request_type='service') + 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 diff --git a/magnum/tests/functional/common/base.py b/magnum/tests/functional/common/base.py index c08e1acbfb..b235939f7a 100644 --- a/magnum/tests/functional/common/base.py +++ b/magnum/tests/functional/common/base.py @@ -22,9 +22,11 @@ from magnum.tests.functional.common import manager class BaseMagnumTest(base.BaseTestCase): """Sets up configuration required for functional tests""" + ic_class_list = [] + ic_method_list = [] + def __init__(self, *args, **kwargs): super(BaseMagnumTest, self).__init__(*args, **kwargs) - self.ic = None @classmethod def setUpClass(cls): @@ -34,13 +36,27 @@ class BaseMagnumTest(base.BaseTestCase): @classmethod def tearDownClass(cls): super(BaseMagnumTest, cls).tearDownClass() + cls.clear_credentials(clear_class_creds=True) def tearDown(self): - if self.ic is not None: - self.ic.clear_creds() super(BaseMagnumTest, self).tearDown() + self.clear_credentials(clear_method_creds=True) - def get_credentials(self, name=None, type_of_creds="default"): + @classmethod + def clear_credentials(cls, + clear_class_creds=False, + clear_method_creds=False): + if clear_class_creds: + for ic in cls.ic_class_list: + ic.clear_creds() + if clear_method_creds: + for ic in cls.ic_method_list: + ic.clear_creds() + + @classmethod + def get_credentials(cls, name=None, + type_of_creds="default", + class_cleanup=False): if name is None: # Get name of test method name = inspect.stack()[1][3] @@ -48,22 +64,27 @@ class BaseMagnumTest(base.BaseTestCase): name = name[0:32] # Choose type of isolated creds - self.ic = common_creds.get_credentials_provider( + ic = common_creds.get_credentials_provider( name, identity_version=config.Config.auth_version ) + if class_cleanup: + cls.ic_class_list.append(ic) + else: + cls.ic_method_list.append(ic) + creds = None if "admin" == type_of_creds: - creds = self.ic.get_admin_creds() + creds = ic.get_admin_creds() elif "alt" == type_of_creds: - creds = self.ic.get_alt_creds() + creds = ic.get_alt_creds() elif "default" == type_of_creds: - creds = self.ic.get_primary_creds() + creds = ic.get_primary_creds() else: - creds = self.ic.self.get_credentials(type_of_creds) + creds = ic.self.get_credentials(type_of_creds) - _, keypairs_client = self.get_clients( + _, keypairs_client = cls.get_clients( creds, type_of_creds, 'keypair_setup') try: keypairs_client.show_keypair(config.Config.keypair_id) @@ -72,7 +93,8 @@ class BaseMagnumTest(base.BaseTestCase): return creds - def get_clients(self, creds, type_of_creds, request_type): + @classmethod + def get_clients(cls, creds, type_of_creds, request_type): if "admin" == type_of_creds: manager_inst = manager.AdminManager(credentials=creds, request_type=request_type) @@ -89,22 +111,27 @@ class BaseMagnumTest(base.BaseTestCase): # create client with isolated creds return (manager_inst.client, manager_inst.keypairs_client) - def get_clients_with_existing_creds(self, + @classmethod + def get_clients_with_existing_creds(cls, name=None, creds=None, type_of_creds="default", - request_type=None): + request_type=None, + class_cleanup=False): if creds is None: - return self.get_clients_with_isolated_creds(name, - type_of_creds, - request_type) + return cls.get_clients_with_new_creds(name, + type_of_creds, + request_type, + class_cleanup) else: - return self.get_clients(creds, type_of_creds, request_type) + return cls.get_clients(creds, type_of_creds, request_type) - def get_clients_with_new_creds(self, + @classmethod + def get_clients_with_new_creds(cls, name=None, type_of_creds="default", - request_type=None): + request_type=None, + class_cleanup=False): """Creates isolated creds. :param name: name, will be used for dynamic creds @@ -113,5 +140,5 @@ class BaseMagnumTest(base.BaseTestCase): :returns: MagnumClient -- client with isolated creds. :returns: KeypairClient -- allows for creating of keypairs """ - creds = self.get_credentials(name, type_of_creds) - return self.get_clients(creds, type_of_creds, request_type) + creds = cls.get_credentials(name, type_of_creds, class_cleanup) + return cls.get_clients(creds, type_of_creds, request_type) diff --git a/magnum/tests/functional/common/config.py b/magnum/tests/functional/common/config.py index e5d9fd9b7a..e78e14ba6d 100644 --- a/magnum/tests/functional/common/config.py +++ b/magnum/tests/functional/common/config.py @@ -97,6 +97,12 @@ class Config(object): raise Exception('config missing master_flavor_id key') cls.master_flavor_id = CONF.magnum.master_flavor_id + @classmethod + def set_csr_location(cls, config): + if 'csr_location' not in CONF.magnum: + raise Exception('config missing csr_location key') + cls.csr_location = CONF.magnum.csr_location + @classmethod def setUp(cls): cls.set_admin_creds(config) @@ -112,3 +118,4 @@ class Config(object): cls.set_flavor_id(config) cls.set_magnum_url(config) cls.set_master_flavor_id(config) + cls.set_csr_location(config) diff --git a/magnum/tests/functional/common/datagen.py b/magnum/tests/functional/common/datagen.py index 05f53c1797..7e605e7a56 100644 --- a/magnum/tests/functional/common/datagen.py +++ b/magnum/tests/functional/common/datagen.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import random import socket import string @@ -21,6 +22,7 @@ from magnum.tests.functional.api.v1.models import bay_model 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.common import config @@ -282,3 +284,18 @@ def bay_node_count_patch_data(node_count=2): "op": "replace" }] return baypatch_model.BayPatchCollection.from_dict(data) + + +def cert_data(bay_uuid, csr_data=None): + if csr_data is None: + csr_data = config.Config.csr_location + data = { + "bay_uuid": bay_uuid + } + if csr_data is not None and os.path.isfile(csr_data): + with open(csr_data, 'r') as f: + data['csr'] = f.read() + + model = cert_model.CertEntity.from_dict(data) + + return model diff --git a/magnum/tests/functional/common/manager.py b/magnum/tests/functional/common/manager.py index 71a44cadb8..33e5f28b67 100644 --- a/magnum/tests/functional/common/manager.py +++ b/magnum/tests/functional/common/manager.py @@ -15,6 +15,7 @@ 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 magnum_service_client from magnum.tests.functional.common import client from magnum.tests.functional.common import config @@ -33,6 +34,8 @@ class Manager(clients.Manager): self.client = baymodel_client.BayModelClient(self.auth_provider) elif request_type == 'bay': self.client = bay_client.BayClient(self.auth_provider) + elif request_type == 'cert': + self.client = cert_client.CertClient(self.auth_provider) elif request_type == 'service': self.client = magnum_service_client.MagnumServiceClient( self.auth_provider) diff --git a/magnum/tests/functional/tempest_tests/config.py b/magnum/tests/functional/tempest_tests/config.py index ecc012ceff..389e694bc8 100644 --- a/magnum/tests/functional/tempest_tests/config.py +++ b/magnum/tests/functional/tempest_tests/config.py @@ -51,4 +51,8 @@ MagnumGroup = [ cfg.StrOpt("master_flavor_id", default="m1.magnum", help="Master flavor id to use for baymodels."), + + cfg.StrOpt("csr_location", + default="/opt/stack/new/magnum/default.csr", + help="CSR location for certificates."), ]