feat: GET certificate endpoint

REQUEST:

    GET /ssl_certificate/{domain_name}

RESPONSE:

    200 OK

        [
          {
            "flavor_id": "myflavor",
            "domain_name": "www.mydomain.com",
            "cert_type": "san",
            "cert_details": {
              "provider": {
                "cert_domain": null,
                "extra_info": {
                  "status": "create_in_progress",
                  "san cert": null,
                  "action": "Waiting for action"
                }
              }
            },
            "status": "create_in_progress"
          }
        ]

Implements: blueprint ssl-certificates
Implements: blueprint akamai-ssl-driver

Change-Id: I677fc10a627659b01f87a174998435733faec9e2
This commit is contained in:
Sriram Madapusi Vasudevan 2015-10-07 14:39:52 -04:00
parent 5a5e5fc979
commit 032af4a026
14 changed files with 444 additions and 65 deletions

View File

@ -31,7 +31,6 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
self.flavor_controller = self._driver.storage.flavors_controller
def create_ssl_certificate(self, project_id, cert_obj):
try:
flavor = self.flavor_controller.get(cert_obj.flavor_id)
# raise a lookup error if the flavor is not found
@ -68,3 +67,17 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
delete_ssl_certificate.delete_ssl_certificate,
**kwargs)
return kwargs
def get_certs_info_by_domain(self, domain_name, project_id):
try:
certs_info = self.storage_controller.get_certs_by_domain(
domain_name=domain_name,
project_id=project_id)
if not certs_info:
raise ValueError("certificate information"
"not found for {0} ".format(domain_name))
return certs_info
except ValueError as e:
raise e

View File

@ -32,11 +32,13 @@ class SSLCertificate(common.DictSerializableModel):
flavor_id,
domain_name,
cert_type,
project_id=None,
cert_details={}):
self._flavor_id = flavor_id
self._domain_name = domain_name
self._cert_type = cert_type
self._cert_details = cert_details
self._project_id = project_id
@property
def flavor_id(self):
@ -47,6 +49,15 @@ class SSLCertificate(common.DictSerializableModel):
def flavor_id(self, value):
self._flavor_id = value
@property
def project_id(self):
"""Get project id."""
return self._project_id
@project_id.setter
def project_id(self, value):
self._project_id = value
@property
def domain_name(self):
"""Get domain name"""
@ -89,7 +100,7 @@ class SSLCertificate(common.DictSerializableModel):
# provider per flavor (that is akamai), so the first one
# value of this dictionary is akamai cert_details
first_provider_cert_details = (
self.cert_details.values()[0].get("extra_info", None))
list(self.cert_details.values())[0].get("extra_info", None))
if first_provider_cert_details is None:
return "deployed"
else:
@ -109,10 +120,26 @@ class SSLCertificate(common.DictSerializableModel):
if self.cert_details is None or self.cert_details == {}:
return None
first_provider_cert_details = (
self.cert_details.values()[0].get("extra_info", None))
list(self.cert_details.values())[0].get("extra_info", None))
if first_provider_cert_details is None:
return None
else:
return first_provider_cert_details.get('san cert', None)
else:
return None
@classmethod
def init_from_dict(cls, input_dict):
flavor_id = input_dict.get('flavor_id', None)
domain_name = input_dict.get('domain_name', None)
cert_type = input_dict.get('cert_type', None)
cert_details = input_dict.get('cert_details', {})
project_id = input_dict.get('project_id', None)
ssl_cert = cls(flavor_id=flavor_id,
domain_name=domain_name,
cert_type=cert_type,
cert_details=cert_details,
project_id=project_id)
return ssl_cert

View File

@ -614,9 +614,8 @@ class ServiceController(base.ServiceBase):
'status': 'failed',
'san cert': None,
'action': 'Waiting for action... '
'Provision san cert failed for %s failed.'
' Reason: %s' %
(cert_obj.domain_name, str(e))
'Provision san cert failed for %s failed.' %
cert_obj.domain_name
})
else:
return self.responder.ssl_certificate_provisioned(None, {

View File

@ -22,7 +22,12 @@ except ImportError: # pragma: no cover
import collections # pragma: no cover
from cassandra import query
import six
if six.PY2:
from itertools import ifilterfalse as filterfalse
else:
from itertools import filterfalse
from poppy.model.helpers import cachingrule
from poppy.model.helpers import domain
from poppy.model.helpers import origin
@ -364,36 +369,14 @@ class ServicesController(base.ServicesController):
:param cert_type
:param comparing_project_id
:raises ValueError
:returns Boolean if the cert with same type exists with another user.
"""
LOG.info("Check if cert on '{0}' exists".format(domain_name))
args = {
'domain_name': domain_name.lower()
}
stmt = query.SimpleStatement(
CQL_VERIFY_CERT,
consistency_level=self._driver.consistency_level)
results = self.session.execute(stmt, args)
cert = self.get_certs_by_domain(domain_name=domain_name,
cert_type=comparing_cert_type,
flavor_id=comparing_flavor_id)
if results:
msg = None
for r in results:
if str(r.get('project_id')) != str(comparing_project_id):
msg = "Domain '{0}' has already been created cert by {1}"\
.format(domain_name, r.get('project_id'))
LOG.warn(msg)
raise ValueError(msg)
elif (str(r.get('flavor_id')) == str(comparing_flavor_id)
and
str(r.get('cert_type')) == str(comparing_cert_type)):
msg = "{0} have already created cert of type {1} on {2}"\
.format(str(comparing_project_id),
comparing_cert_type,
domain_name)
LOG.warn(msg)
raise ValueError(msg)
return False
if cert:
return True
else:
return False
@ -503,13 +486,9 @@ class ServicesController(base.ServicesController):
"project_id: {0} set to be {1}".format(project_id,
project_limit))
def get_cert_by_domain(self, domain_name, cert_type,
flavor_id,
project_id):
LOG.info(("Search for cert on '{0}', type: {1}, flavor_id: {2}, "
"project_id: {3}").format(domain_name, cert_type, flavor_id,
project_id))
def get_certs_by_domain(self, domain_name, project_id=None, flavor_id=None,
cert_type=None):
LOG.info("Check if cert on '{0}' exists".format(domain_name))
args = {
'domain_name': domain_name.lower()
}
@ -517,7 +496,7 @@ class ServicesController(base.ServicesController):
CQL_SEARCH_CERT_BY_DOMAIN,
consistency_level=self._driver.consistency_level)
results = self.session.execute(stmt, args)
certs = []
if results:
for r in results:
r_project_id = str(r.get('project_id'))
@ -529,18 +508,50 @@ class ServicesController(base.ServicesController):
# And the value of cert_details is a string dict
for key in cert_details:
r_cert_details[key] = json.loads(cert_details[key])
if r_project_id == str(project_id) and \
r_flavor_id == str(flavor_id) and \
r_cert_type == str(cert_type):
res = ssl_certificate.SSLCertificate(r_flavor_id,
domain_name,
LOG.info("Certificate for domain: {0} "
"with flavor_id: {1}, "
"cert_details : {2} and "
"cert_type: {3} present "
"on project_id: {4}".format(domain_name,
r_flavor_id,
r_cert_details,
r_cert_type,
r_cert_details)
return res
r_project_id))
ssl_cert = ssl_certificate.SSLCertificate(
domain_name=domain_name,
flavor_id=r_flavor_id,
cert_details=r_cert_details,
cert_type=r_cert_type,
project_id=r_project_id)
certs.append(ssl_cert)
non_none_attrs_gen = filterfalse(
lambda x: list(x.values())[0] is None, [{'project_id': project_id},
{'flavor_id': flavor_id},
{'cert_type': cert_type}])
non_none_attrs_list = list(non_none_attrs_gen)
non_none_attrs_dict = {}
if non_none_attrs_list:
for attr in non_none_attrs_list:
non_none_attrs_dict.update(attr)
def argfilter(certificate):
all_conditions = True
if non_none_attrs_dict:
for k, v in non_none_attrs_dict.items():
if getattr(certificate, k) != v:
all_conditions = False
return all_conditions
total_certs = [cert for cert in certs if argfilter(cert)]
if len(total_certs) == 1:
return total_certs[0]
else:
return None
else:
return None
return total_certs
def delete_cert(self, project_id, domain_name, cert_type):
"""delete_cert
@ -795,12 +806,12 @@ class ServicesController(base.ServicesController):
self.session.execute(stmt, delete_args)
def create_cert(self, project_id, cert_obj):
if not self.cert_already_exist(cert_obj.domain_name,
cert_obj.cert_type,
cert_obj.flavor_id,
project_id):
pass
if self.cert_already_exist(domain_name=cert_obj.domain_name,
comparing_cert_type=cert_obj.cert_type,
comparing_flavor_id=cert_obj.flavor_id,
comparing_project_id=project_id):
raise ValueError('Certificate already exists '
'for {0} '.format(cert_obj.domain_name))
args = {
'project_id': project_id,

View File

@ -33,6 +33,7 @@ class ServicesController(base.ServicesController):
self.projectid_service_limit = {}
self.default_max_service_limit = 20
self.service_count_per_project_id = {}
self.certs = {}
@property
def session(self):
@ -161,10 +162,26 @@ class ServicesController(base.ServicesController):
def update_cert_info(self, domain_name, cert_type, flavor_id,
cert_details):
pass
key = (flavor_id, domain_name, cert_type)
if key in self.certs:
self.certs[key].cert_details = cert_details
def create_cert(self, project_id, cert_obj):
pass
key = (cert_obj.flavor_id, cert_obj.domain_name, cert_obj.cert_type)
if key not in self.certs:
self.certs[key] = cert_obj
else:
raise ValueError
def get_certs_by_domain(self, domain_name, project_id=None):
certs = []
for cert in self.certs:
if domain_name in cert:
certs.append(self.certs[cert])
if project_id:
return [cert for cert in certs if cert.project_id == project_id]
else:
return certs
def delete_cert(self, project_id, domain_name, cert_type):
if "non_exist" in domain_name:

View File

@ -21,6 +21,8 @@ from pecan import hooks
from poppy.transport.pecan.controllers import base
from poppy.transport.pecan import hooks as poppy_hooks
from poppy.transport.pecan.models.request import ssl_certificate
from poppy.transport.pecan.models.response import ssl_certificate \
as ssl_cert_model
from poppy.transport.validators import helpers
from poppy.transport.validators.schemas import ssl_certificate\
as ssl_certificate_validation
@ -51,6 +53,7 @@ class SSLCertificateController(base.Controller, hooks.HookController):
try:
project_id = certificate_info_dict.get('project_id')
cert_obj = ssl_certificate.load_from_json(certificate_info_dict)
cert_obj.project_id = project_id
ssl_certificate_controller.create_ssl_certificate(project_id,
cert_obj)
except LookupError as e:
@ -83,3 +86,34 @@ class SSLCertificateController(base.Controller, hooks.HookController):
'Reason: %s' % str(e))
return pecan.Response(None, 202)
@pecan.expose('json')
@decorators.validate(
domain_name=rule.Rule(
helpers.is_valid_domain_by_name(),
helpers.abort_with_message)
)
def get_one(self, domain_name):
certificate_controller = \
self._driver.manager.ssl_certificate_controller
total_cert_info = []
try:
certs_info = certificate_controller.get_certs_info_by_domain(
domain_name=domain_name,
project_id=self.project_id)
except ValueError:
pecan.abort(404, detail='certificate '
'could not be found '
'for domain : %s' %
domain_name)
else:
# convert a cert model into a response cert model
try:
if iter(certs_info):
for cert in certs_info:
total_cert_info.append(ssl_cert_model.Model(cert))
return total_cert_info
except TypeError:
return ssl_cert_model.Model(certs_info)

View File

@ -29,3 +29,5 @@ class Model(collections.OrderedDict):
self["flavor_id"] = ssl_certificate.flavor_id
self['domain_name'] = util.help_escape(ssl_certificate.domain_name)
self['cert_type'] = ssl_certificate.cert_type
self['cert_details'] = ssl_certificate.cert_details
self['status'] = ssl_certificate.get_cert_status()

View File

@ -12,6 +12,8 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import ddt
from tests.api import base
@ -50,7 +52,7 @@ class TestCreateSSLCertificate(base.TestBase):
self.skipTest('Create ssl certificate needs to'
' be run when commanded')
self.cert_type = test_data.get('cert_type')
cert_type = test_data.get('cert_type')
rand_string = self.generate_random_string()
domain_name = rand_string + test_data.get('domain_name')
flavor_id = test_data.get('flavor_id') or self.flavor_id
@ -63,6 +65,16 @@ class TestCreateSSLCertificate(base.TestBase):
)
self.assertEqual(resp.status_code, 202)
resp = self.client.get_ssl_certificate(
domain_name=domain_name
)
self.assertEqual(resp.status_code, 200)
for cert in json.loads(resp.content):
self.assertEqual(cert['domain_name'], domain_name)
self.assertEqual(cert['flavor_id'], flavor_id)
self.assertEqual(cert['cert_type'], cert_type)
def tearDown(self):
self.client.delete_ssl_certificate(
cert_type=self.cert_type,

View File

@ -437,8 +437,8 @@ class PoppyClient(client.AutoMarshallingHTTPClient):
requestslib_kwargs=None):
"""Creates SSL Certificate
:return: Response Object containing response code 200 and body with
details of service
:return: Response Object containing response code 202
POST
ssl_certificate
"""
@ -460,10 +460,28 @@ class PoppyClient(client.AutoMarshallingHTTPClient):
"""Deletes SSL Certificate
:return: Response Object containing response code 202
GET
DELETE
ssl_certificate
"""
url = '{0}/ssl_certificate/{1}'.format(self.url, domain_name)
return self.request('DELETE', url,
requestslib_kwargs=requestslib_kwargs)
def get_ssl_certificate(self,
domain_name,
requestslib_kwargs=None,):
"""GET SSL Certificate
:return: Response Object containing response code 200 and body with
details of certificate request
GET
ssl_certificate
"""
url = '{0}/ssl_certificate/{1}'.format(self.url, domain_name)
return self.request('GET', url, requestslib_kwargs=requestslib_kwargs)

View File

@ -132,7 +132,7 @@ class CreateFlavor(base.AutoMarshallingModel):
class CreateSSLCertificate(base.AutoMarshallingModel):
"""Marshalling for Create Flavor requests."""
"""Marshalling for Create SSL Certificate requests."""
def __init__(self, cert_type=None, domain_name=None, flavor_id=None,
project_id=None):

View File

@ -69,6 +69,71 @@ class SSLCertificateControllerTest(base.FunctionalTest):
'X-Project-ID': self.project_id})
self.assertEqual(202, response.status_code)
def test_get_ssl_certificate_non_existing_domain(self):
# get non existing domain
domain = 'www.idontexist.com'
response = self.app.get('/v1.0/ssl_certificate/{0}'.format(domain),
headers={
'Content-Type': 'application/json',
'X-Project-ID': self.project_id},
expect_errors=True)
self.assertEqual(404, response.status_code)
def test_get_ssl_certificate_existing_domain(self):
domain = 'www.iexist.com'
ssl_certificate_json = {
"cert_type": "san",
"domain_name": domain,
"flavor_id": self.flavor_id,
"project_id": self.project_id
}
response = self.app.post('/v1.0/ssl_certificate',
params=json.dumps(ssl_certificate_json),
headers={
'Content-Type': 'application/json',
'X-Project-ID': self.project_id})
self.assertEqual(202, response.status_code)
# get existing domain with same project_id
response = self.app.get('/v1.0/ssl_certificate/{0}'.format(domain),
headers={
'Content-Type': 'application/json',
'X-Project-ID': self.project_id})
response_list = json.loads(response.body.decode("utf-8"))
self.assertEqual(200, response.status_code)
self.assertEqual(ssl_certificate_json["cert_type"],
response_list[0]["cert_type"])
self.assertEqual(ssl_certificate_json["domain_name"],
response_list[0]["domain_name"])
self.assertEqual(ssl_certificate_json["flavor_id"],
response_list[0]["flavor_id"])
def test_get_ssl_certificate_existing_domain_different_project_id(self):
domain = 'www.iexist.com'
ssl_certificate_json = {
"cert_type": "san",
"domain_name": domain,
"flavor_id": self.flavor_id,
"project_id": self.project_id
}
response = self.app.post('/v1.0/ssl_certificate',
params=json.dumps(ssl_certificate_json),
headers={
'Content-Type': 'application/json',
'X-Project-ID': self.project_id})
self.assertEqual(202, response.status_code)
# get existing domain with different project_id
response = self.app.get('/v1.0/ssl_certificate/{0}'.format(domain),
headers={
'Content-Type': 'application/json',
'X-Project-ID': str(uuid.uuid4())},
expect_errors=True)
self.assertEqual(404, response.status_code)
def test_create_with_invalid_json(self):
# create with errorenous data: invalid json data
response = self.app.post('/v1.0/ssl_certificate',

View File

@ -0,0 +1,78 @@
# Copyright (c) 2015 Rackspace, Inc.
#
# 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 ddt
from poppy.model import ssl_certificate
from tests.unit import base
@ddt.ddt
class TestSSLCertificate(base.TestCase):
def test_ssl_certificate(self):
project_id = '12345'
cert_details = {
'mock': {
'extra_info': 'nope'
}
}
flavor_id = 'myflavor'
domain_name = 'www.mydomain.com'
cert_type = 'san'
ssl_cert = ssl_certificate.SSLCertificate(project_id=project_id,
flavor_id=flavor_id,
domain_name=domain_name,
cert_type=cert_type,
cert_details=cert_details)
# test all properties
# project_id
self.assertEqual(ssl_cert.project_id, project_id)
ssl_cert.project_id = '123456'
# flavor_id
self.assertEqual(ssl_cert.flavor_id, flavor_id)
ssl_cert.flavor_id = 'yourflavor'
# domain_name
self.assertEqual(ssl_cert.domain_name, domain_name)
ssl_cert.domain_name = 'www.yourdomain.com'
# cert_type
self.assertEqual(ssl_cert.cert_type, cert_type)
ssl_cert.cert_type = 'custom'
self.assertRaises(ValueError, setattr, ssl_cert, 'cert_type',
'whatever')
# cert_details
self.assertEqual(ssl_cert.cert_details, cert_details)
cert_details_two = cert_details.copy()
cert_details_two['mock']['extra_info'] = 'maybe'
ssl_cert.cert_details = cert_details_two
# get cert status
cert_details['mock']['extra_info'] = {
'status': 'deployed'
}
ssl_cert.cert_details = cert_details
self.assertEqual(ssl_cert.get_cert_status(), 'deployed')
cert_details['mock']['extra_info'] = {
'status': 'whatever'
}
self.assertRaises(ValueError, ssl_cert.get_cert_status)

View File

@ -0,0 +1,73 @@
[
[
[
{
"project_id": 12345,
"flavor_id": "flavor1",
"cert_type": "san",
"domain_name": "www.mydomain.com",
"cert_details": {
"provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}"
}
},
{
"project_id": 12345,
"flavor_id": "flavor2",
"cert_type": "custom",
"domain_name": "www.mydomain.com",
"cert_details": {
"provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_custom\", \"action\": \"Ready\"}}"
}
}
],
[
{
"project_id": 12345,
"flavor_id": "flavor1",
"cert_type": "custom",
"domain_name": "www.example.com",
"cert_details": {
"provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_custom\", \"action\": \"Ready\"}}"
}
},
{
"project_id": 12345,
"flavor_id": "flavor1",
"cert_type": "san",
"domain_name": "www.example.com",
"cert_details": {
"provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}"
}
},
{
"project_id": 12346,
"flavor_id": "flavor2",
"cert_type": "san",
"domain_name": "www.mydomain2.com",
"cert_details": {
"provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}"
}
}
],
[
{
"project_id": 12345,
"flavor_id": "flavor1",
"cert_type": "san",
"domain_name": "www.mydomain.com",
"cert_details": {
"provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}"
}
},
{
"project_id": 12346,
"flavor_id": "flavor2",
"cert_type": "san",
"domain_name": "www.mydomain2.com",
"cert_details": {
"provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}"
}
}
]
]
]

View File

@ -26,6 +26,7 @@ import mock
from oslo_config import cfg
from poppy.model.helpers import provider_details
from poppy.model import ssl_certificate
from poppy.storage.cassandra import driver
from poppy.storage.cassandra import services
from poppy.transport.pecan.models.request import service as req_service
@ -179,6 +180,35 @@ class CassandraStorageServiceTests(base.TestCase):
self.assertTrue("CloudFront" in actual_response)
self.assertTrue("Fastly" in actual_response)
@ddt.file_data('data_get_cert_by_domain.json')
@mock.patch.object(services.ServicesController, 'session')
@mock.patch.object(cassandra.cluster.Session, 'execute')
def test_get_cert_by_domain(self, cert_details_json,
mock_session, mock_execute):
# mock the response from cassandra
mock_execute.execute.return_value = cert_details_json[0]
actual_response = self.sc.get_certs_by_domain(
domain_name="www.mydomain.com")
self.assertEqual(len(actual_response), 2)
self.assertTrue(all([isinstance(ssl_cert,
ssl_certificate.SSLCertificate)
for ssl_cert in actual_response]))
mock_execute.execute.return_value = cert_details_json[1]
actual_response = self.sc.get_certs_by_domain(
domain_name="www.example.com",
flavor_id="flavor1")
self.assertEqual(len(actual_response), 2)
self.assertTrue(all([isinstance(ssl_cert,
ssl_certificate.SSLCertificate)
for ssl_cert in actual_response]))
mock_execute.execute.return_value = cert_details_json[2]
actual_response = self.sc.get_certs_by_domain(
domain_name="www.mydomain.com",
flavor_id="flavor1",
cert_type="san")
self.assertTrue(isinstance(actual_response,
ssl_certificate.SSLCertificate))
@ddt.file_data('data_provider_details.json')
@mock.patch.object(services.ServicesController, 'session')
@mock.patch.object(cassandra.cluster.Session, 'execute')