Add support akamai DV SNI certificates

Change-Id: I9f1ef35ee5cb3e50ac689b333357d43ef43fb343
This commit is contained in:
Isaac Mungai 2016-08-01 15:44:14 -04:00
parent 0458de585d
commit 054dd47fd3
37 changed files with 1936 additions and 239 deletions

View File

@ -180,9 +180,11 @@ akamai_https_access_url_suffix = "MY_HTTPS_ACCESS_URL_SUFFIX"
akamai_http_config_number = 'MY_AKAMAI_HTTP_CONFIG_NUMBER' akamai_http_config_number = 'MY_AKAMAI_HTTP_CONFIG_NUMBER'
akamai_https_shared_config_number = 'MY_AKAMAI_HTTPS_CONFIG_SHARED_NUMBER' akamai_https_shared_config_number = 'MY_AKAMAI_HTTPS_CONFIG_SHARED_NUMBER'
akamai_https_san_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_SAN_NUMBER' akamai_https_san_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_SAN_NUMBER'
akamai_https_sni_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_SNI_NUMBER'
akamai_https_custom_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_CUSTOM_NUMBER' akamai_https_custom_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_CUSTOM_NUMBER'
san_cert_cnames = "MY_SAN_CERT_LIST" san_cert_cnames = "MY_SAN_CERT_LIST"
san_cert_hostname_limit = "MY_SAN_HOSTNAMES_LMIT" sni_cert_cnames = "MY_SNI_CERT_LIST"
san_cert_hostname_limit = "MY_SAN_HOSTNAMES_LIMIT"
contract_id = "MY_CONTRACT_ID" contract_id = "MY_CONTRACT_ID"
group_id = "MY_GROUP_ID" group_id = "MY_GROUP_ID"
property_id = "MY_PROPERTY_ID" property_id = "MY_PROPERTY_ID"

View File

@ -43,7 +43,7 @@ def delete_service():
delete_service_tasks.GatherProviderDetailsTask( delete_service_tasks.GatherProviderDetailsTask(
rebind=['responders', 'dns_responder']), rebind=['responders', 'dns_responder']),
linear_flow.Flow('Delete san certificates for service').add( linear_flow.Flow('Delete san certificates for service').add(
delete_service_tasks.DeleteCertificatesForServiceSanDomains() delete_service_tasks.DeleteCertificatesForService()
), ),
linear_flow.Flow('Delete service storage operation').add( linear_flow.Flow('Delete service storage operation').add(
common.UpdateProviderDetailIfNotEmptyTask( common.UpdateProviderDetailIfNotEmptyTask(

View File

@ -219,8 +219,8 @@ class UpdateProviderDetailIfNotEmptyTask(task.Task):
project_id, project_id,
service_id)) service_id))
LOG.info('Updating service detail task' LOG.info('Updating service detail task '
'complete for Changed Provider Details :' 'complete for Changed Provider Details : '
'{0}'.format(changed_provider_details_dict)) '{0}'.format(changed_provider_details_dict))
def revert(self, *args, **kwargs): def revert(self, *args, **kwargs):

View File

@ -40,7 +40,7 @@ class CreateProviderSSLCertificateTask(task.Task):
cert_obj = ssl_certificate.load_from_json(json.loads(cert_obj_json)) cert_obj = ssl_certificate.load_from_json(json.loads(cert_obj_json))
responders = [] responders = []
# try to create all service from each provider # try to create all certificates from each provider
for provider in providers_list: for provider in providers_list:
LOG.info('Starting to create ssl certificate: {0}' LOG.info('Starting to create ssl certificate: {0}'
'from {1}'.format(cert_obj.to_dict(), provider)) 'from {1}'.format(cert_obj.to_dict(), provider))

View File

@ -218,7 +218,8 @@ class DeleteStorageServiceTask(task.Task):
LOG.info('Cassandra session already shutdown') LOG.info('Cassandra session already shutdown')
class DeleteCertificatesForServiceSanDomains(task.Task): class DeleteCertificatesForService(task.Task):
"""Delete SAN and SNI certificates for a service."""
def execute(self, project_id, service_id): def execute(self, project_id, service_id):
service_controller, self.storage_controller = \ service_controller, self.storage_controller = \
@ -231,16 +232,21 @@ class DeleteCertificatesForServiceSanDomains(task.Task):
kwargs = { kwargs = {
'project_id': project_id, 'project_id': project_id,
'cert_type': 'san',
'context_dict': context_utils.get_current().to_dict() 'context_dict': context_utils.get_current().to_dict()
} }
for domain in service_obj.domains: for domain in service_obj.domains:
if domain.protocol == 'https' and domain.certificate == 'san': if (
domain.protocol == 'https' and
domain.certificate in ['san', 'sni']
):
kwargs["domain_name"] = domain.domain kwargs["domain_name"] = domain.domain
kwargs["cert_type"] = domain.certificate
LOG.info( LOG.info(
"Delete service submit task san_cert deletion {0}".format( "Delete service submit task {0} cert delete "
domain.domain "domain {1}.".format(
domain.certificate,
domain.domain,
) )
) )
service_controller.distributed_task_controller.submit_task( service_controller.distributed_task_controller.submit_task(

View File

@ -13,11 +13,14 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from taskflow import task from taskflow import task
from poppy.distributed_task.utils import memoized_controllers from poppy.distributed_task.utils import memoized_controllers
from poppy.transport.pecan.models.request import ssl_certificate
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -28,11 +31,24 @@ conf(project='poppy', prog='poppy', args=[])
class DeleteProviderSSLCertificateTask(task.Task): class DeleteProviderSSLCertificateTask(task.Task):
default_provides = "responders" default_provides = "responders"
def execute(self): def execute(self, providers_list_json, cert_obj_json):
# Note(tonytan4ever): For right now there is no service_controller = memoized_controllers.task_controllers('poppy')
# way to code the process of deleting a certificate object
# from Akamai cert_obj = ssl_certificate.load_from_json(json.loads(cert_obj_json))
providers_list = json.loads(providers_list_json)
responders = [] responders = []
# try to delete all certificates from each provider
for provider in providers_list:
LOG.info(
'Starting to delete ssl certificate: {0} from {1}.'.format(
cert_obj.to_dict(), provider))
responder = service_controller.provider_wrapper.delete_certificate(
service_controller._driver.providers[provider],
cert_obj,
)
responders.append(responder)
return responders return responders
@ -45,10 +61,23 @@ class SendNotificationTask(task.Task):
"Project ID: %s, Domain Name: %s, Cert type: %s" % "Project ID: %s, Domain Name: %s, Cert type: %s" %
(project_id, domain_name, cert_type)) (project_id, domain_name, cert_type))
notification_content = ""
for responder in responders:
for provider in responder:
notification_content += (
"Project ID: {0}, Provider: {1}, "
"Detail: {2}, Cert type: {3}".format(
project_id,
provider,
str(responder[provider]),
cert_type
)
)
for n_driver in service_controller._driver.notification: for n_driver in service_controller._driver.notification:
service_controller.notification_wrapper.send( service_controller.notification_wrapper.send(
n_driver, n_driver,
n_driver.obj.notification_subject, "Poppy Certificate Deleted",
notification_content) notification_content)
return return

View File

@ -74,12 +74,14 @@ class ProviderWrapper(object):
purge_url) purge_url)
def create_certificate(self, ext, cert_obj, enqueue, https_upgrade): def create_certificate(self, ext, cert_obj, enqueue, https_upgrade):
"""Create a certificate """Create a certificate.
:param ext :param ext
:param cert_obj :param cert_obj
:param enqueue :param enqueue
:returns: ext.obj.service_controller.create(service_obj) :param https_upgrade
:returns: ext.obj.certificate_controller.create_certificate(cert_obj,
enqueue, https_upgrade)
""" """
return ext.obj.certificate_controller.create_certificate( return ext.obj.certificate_controller.create_certificate(
@ -87,3 +89,13 @@ class ProviderWrapper(object):
enqueue, enqueue,
https_upgrade https_upgrade
) )
def delete_certificate(self, ext, cert_obj):
"""Delete a certificate.
:param ext
:param cert_obj
:returns: ext.obj.service_controller.delete_certificate(cert_obj)
"""
return ext.obj.certificate_controller.delete_certificate(cert_obj)

View File

@ -42,6 +42,8 @@ class BackgroundJobController(base.BackgroundJobController):
a_driver.AKAMAI_GROUP].akamai_https_access_url_suffix a_driver.AKAMAI_GROUP].akamai_https_access_url_suffix
self.akamai_san_cert_cname_list = self.driver.conf[ self.akamai_san_cert_cname_list = self.driver.conf[
a_driver.AKAMAI_GROUP].san_cert_cnames a_driver.AKAMAI_GROUP].san_cert_cnames
self.akamai_sni_cert_cname_list = self.driver.conf[
a_driver.AKAMAI_GROUP].sni_cert_cnames
self.notify_email_list = self.driver.conf[ self.notify_email_list = self.driver.conf[
n_driver.MAIL_NOTIFICATION_GROUP].recipients n_driver.MAIL_NOTIFICATION_GROUP].recipients
self.cert_storage = self._driver.storage.certificates_controller self.cert_storage = self._driver.storage.certificates_controller
@ -127,96 +129,109 @@ class BackgroundJobController(base.BackgroundJobController):
cert_dict = dict() cert_dict = dict()
try: try:
cert_dict = json.loads(cert) cert_dict = json.loads(cert)
# add validation that the domain still exists on a if cert_dict['cert_type'] == 'san':
# service and that it has a type of SAN # add validation that the domain still exists on a
cert_obj = ssl_certificate.SSLCertificate( # service and that it has a type of SAN
cert_dict['flavor_id'], cert_obj = ssl_certificate.SSLCertificate(
cert_dict['domain_name'], cert_dict['flavor_id'],
'san', cert_dict['domain_name'],
project_id=cert_dict['project_id'] cert_dict['cert_type'],
) project_id=cert_dict['project_id']
cert_for_domain = self.cert_storage.get_certs_by_domain(
cert_obj.domain_name,
project_id=cert_obj.project_id,
flavor_id=cert_obj.flavor_id,
cert_type=cert_obj.cert_type
)
if cert_for_domain == []:
ignore_list.append(cert_dict)
LOG.info(
"Ignored property update because "
"certificate for {0} does not exist.".format(
cert_obj.domain_name
)
) )
continue
service_obj = self.service_storage.\ cert_for_domain = self.cert_storage.\
get_service_details_by_domain_name( get_certs_by_domain(
cert_obj.domain_name,
cert_obj.project_id
)
found = False
for domain in service_obj.domains:
if (
domain.domain == cert_obj.domain_name and
domain.protocol == 'https' and
domain.certificate == 'san'
):
found = True
if found is False:
# skip the task for current cert obj is the
# domain doesn't exist on a service with the
# same protocol and certificate.
ignore_list.append(cert_dict)
LOG.info(
"Ignored update property for a "
"domain '{0}' that no longer exists on a service "
"with the same protocol 'https' and certificate "
"type 'san'".format(
cert_obj.domain_name, cert_obj.domain_name,
project_id=cert_obj.project_id,
flavor_id=cert_obj.flavor_id,
cert_type=cert_obj.cert_type
) )
) if cert_for_domain == []:
continue ignore_list.append(cert_dict)
domain_name = cert_dict["domain_name"] LOG.info(
san_cert = ( "Ignored property update because "
cert_dict["cert_details"] "certificate for {0} does not exist.".format(
["Akamai"]["extra_info"]["san cert"] cert_obj.domain_name
) )
LOG.info(
"{0}: {1} to {2}, on property: {3}".format(
kwargs.get("action", 'add'),
domain_name,
san_cert,
kwargs.get(
"property_spec",
'akamai_https_san_config_numbers'
) )
) continue
)
# Note(tonytan4ever): Put this check here so erroneous san service_obj = self.service_storage.\
# cert params will not pass. Support occasionally put in get_service_details_by_domain_name(
# the ending "edgekey.net" cert_obj.domain_name,
# (e.g: securexxx.san1.abc.com.edgekey.net), this check cert_obj.project_id
# will effectively error that out )
if san_cert not in self.akamai_san_cert_cname_list: if service_obj is None:
raise ValueError( ignore_list.append(cert_dict)
"Not A valid san cert cname: {0}, " LOG.info(
"valid san cert cnames are: {1}".format( "Ignored property update because "
"Service not found for domain {0}".format(
cert_obj.domain_name
)
)
continue
found = False
for domain in service_obj.domains:
if (
domain.domain == cert_obj.domain_name and
domain.protocol == 'https' and
domain.certificate == 'san'
):
found = True
if found is False:
# skip the task for current cert obj is the
# domain doesn't exist on a service with the
# same protocol and certificate.
ignore_list.append(cert_dict)
LOG.info(
"Ignored update property for domain "
"'{0}' that no longer exists on a service "
"with the same protocol 'https' and "
"certificate type '{1}'".format(
cert_obj.domain_name,
cert_obj.cert_type
)
)
continue
domain_name = cert_dict["domain_name"]
san_cert = (
cert_dict["cert_details"]
["Akamai"]["extra_info"]["san cert"]
)
LOG.info(
"{0}: {1} to {2}, on property: {3}".format(
kwargs.get("action", 'add'),
domain_name,
san_cert, san_cert,
self.akamai_san_cert_cname_list kwargs.get(
"property_spec",
'akamai_https_san_config_numbers'
)
) )
) )
cname_host_info_list.append({
"cnameFrom": domain_name, # Note(tonytan4ever): Put this check here so erroneous
"cnameTo": '.'.join( # san cert params will not pass. Support occasionally
[san_cert, self.akamai_san_cert_suffix] # put in the ending "edgekey.net"
), # (e.g: securexxx.san1.abc.com.edgekey.net), this check
"cnameType": "EDGE_HOSTNAME" # will effectively error that out
}) if san_cert not in self.akamai_san_cert_cname_list:
run_list.append(cert_dict) raise ValueError(
"Not A valid san cert cname: {0}, "
"valid san cert cnames are: {1}".format(
san_cert,
self.akamai_san_cert_cname_list
)
)
cname_host_info_list.append({
"cnameFrom": domain_name,
"cnameTo": '.'.join(
[san_cert, self.akamai_san_cert_suffix]
),
"cnameType": "EDGE_HOSTNAME"
})
run_list.append(cert_dict)
except Exception as e: except Exception as e:
cert_dict['error_message'] = str(e) cert_dict['error_message'] = str(e)
ignore_list.append(cert_dict) ignore_list.append(cert_dict)
@ -239,6 +254,156 @@ class BackgroundJobController(base.BackgroundJobController):
"notify_email_list": self.notify_email_list "notify_email_list": self.notify_email_list
} }
# check to see if there are changes to be made before submitting
# the task, avoids creating a new property version when there are
# no changes to be made.
if len(cname_host_info_list) > 0:
self.distributed_task_controller.submit_task(
update_property_flow.update_property_flow,
**t_kwargs)
else:
LOG.info(
"No tasks submitted to update_property_flow"
"update_info_list was empty: {0}".format(
update_info_list
)
)
return run_list, ignore_list
elif job_type == 'akamai_update_papi_property_for_mod_sni':
# this task leaves the san mapping queue intact,
# once items are successfully processed they are marked as
# ready for the next job type execution
if 'akamai' in self._driver.providers:
akamai_driver = self._driver.providers['akamai'].obj
queue_data += akamai_driver.san_mapping_queue.traverse_queue()
cname_host_info_list = []
for cert in queue_data:
cert_dict = dict()
try:
cert_dict = json.loads(cert)
if cert_dict['cert_type'] == 'sni':
# validate that the domain still exists on a
# service and that it has a type of SAN
cert_obj = ssl_certificate.SSLCertificate(
cert_dict['flavor_id'],
cert_dict['domain_name'],
cert_dict['cert_type'],
project_id=cert_dict['project_id']
)
cert_for_domain = self.cert_storage.\
get_certs_by_domain(
cert_obj.domain_name,
project_id=cert_obj.project_id,
flavor_id=cert_obj.flavor_id,
cert_type=cert_obj.cert_type
)
if cert_for_domain == []:
ignore_list.append(cert_dict)
LOG.info(
"Ignored property update because "
"certificate for {0} does not exist.".format(
cert_obj.domain_name
)
)
continue
service_obj = self.service_storage.\
get_service_details_by_domain_name(
cert_obj.domain_name,
cert_obj.project_id
)
if service_obj is None:
ignore_list.append(cert_dict)
LOG.info(
"Ignored property update because "
"Service not found for domain {0}".format(
cert_obj.domain_name
)
)
continue
found = False
for domain in service_obj.domains:
if (
domain.domain == cert_obj.domain_name and
domain.protocol == 'https' and
domain.certificate == cert_obj.cert_type
):
found = True
if found is False:
# skip the task for current cert obj is the
# domain doesn't exist on a service with the
# same protocol and certificate.
ignore_list.append(cert_dict)
LOG.info(
"Ignored update property for domain "
"'{0}' that no longer exists on a service "
"with the same protocol 'https' and "
"certificate type '{1}'".format(
cert_obj.domain_name,
cert_obj.cert_type
)
)
continue
domain_name = cert_dict["domain_name"]
sni_cert = (
cert_dict["cert_details"]
["Akamai"]["extra_info"]["sni_cert"]
)
LOG.info(
"{0}: {1} to {2}, on property: {3}".format(
kwargs.get("action", 'add'),
domain_name,
sni_cert,
kwargs.get(
"property_spec",
'akamai_https_sni_config_numbers'
)
)
)
if sni_cert not in self.akamai_sni_cert_cname_list:
raise ValueError(
"Not a valid sni cert cname: {0}, "
"valid sni cert cnames are: {1}".format(
sni_cert,
self.akamai_sni_cert_cname_list
)
)
cname_host_info_list.append({
"cnameFrom": domain_name,
"cnameTo": '.'.join(
[sni_cert, self.akamai_san_cert_suffix]
),
"cnameType": "EDGE_HOSTNAME"
})
run_list.append(cert_dict)
except Exception as e:
cert_dict['error_message'] = str(e)
ignore_list.append(cert_dict)
LOG.exception(e)
update_info_list = json.dumps([
(
kwargs.get("action", 'add'),
cname_host_info_list
)
])
t_kwargs = {
"property_spec": kwargs.get(
"property_spec",
'akamai_https_sni_config_numbers'
),
"update_type": kwargs.get("update_type", 'hostnames'),
"update_info_list": update_info_list,
"notify_email_list": self.notify_email_list
}
# check to see if there are changes to be made before submitting # check to see if there are changes to be made before submitting
# the task, avoids creating a new property version when there are # the task, avoids creating a new property version when there are
# no changes to be made. # no changes to be made.

View File

@ -86,10 +86,22 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
return kwargs return kwargs
def delete_ssl_certificate(self, project_id, domain_name, cert_type): def delete_ssl_certificate(self, project_id, domain_name, cert_type):
cert_obj = self.storage.get_certs_by_domain(
domain_name, cert_type=cert_type)
try:
flavor = self.flavor_controller.get(cert_obj.flavor_id)
# raise a lookup error if the flavor is not found
except LookupError as e:
raise e
providers = [p.provider_id for p in flavor.providers]
kwargs = { kwargs = {
'project_id': project_id, 'project_id': project_id,
'domain_name': domain_name, 'domain_name': domain_name,
'cert_type': cert_type, 'cert_type': cert_type,
'cert_obj_json': json.dumps(cert_obj.to_dict()),
'providers_list_json': json.dumps(providers),
'context_dict': context_utils.get_current().to_dict() 'context_dict': context_utils.get_current().to_dict()
} }
self.distributed_task_controller.submit_task( self.distributed_task_controller.submit_task(
@ -124,6 +136,7 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
{"domain_name": r['domain_name'], {"domain_name": r['domain_name'],
"project_id": r['project_id'], "project_id": r['project_id'],
"flavor_id": r['flavor_id'], "flavor_id": r['flavor_id'],
"cert_type": r['cert_type'],
"validate_service": r.get('validate_service', True)} "validate_service": r.get('validate_service', True)}
for r in res for r in res
] ]
@ -238,7 +251,7 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
cert_obj = ssl_certificate.SSLCertificate( cert_obj = ssl_certificate.SSLCertificate(
cert_obj_dict['flavor_id'], cert_obj_dict['flavor_id'],
cert_obj_dict['domain_name'], cert_obj_dict['domain_name'],
'san', cert_obj_dict['cert_type'],
project_id=cert_obj_dict['project_id'] project_id=cert_obj_dict['project_id']
) )
@ -254,6 +267,8 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
# If this cert has been deployed through manual # If this cert has been deployed through manual
# process we ignore the rerun process for this entry # process we ignore the rerun process for this entry
if cert_for_domain.get_cert_status() == 'deployed': if cert_for_domain.get_cert_status() == 'deployed':
run_list.remove(cert_obj_dict)
ignore_list.append(cert_obj_dict)
continue continue
# rerun the san process # rerun the san process
try: try:
@ -266,7 +281,7 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
kwargs = { kwargs = {
'project_id': cert_obj.project_id, 'project_id': cert_obj.project_id,
'domain_name': cert_obj.domain_name, 'domain_name': cert_obj.domain_name,
'cert_type': 'san', 'cert_type': cert_obj.cert_type,
'providers_list_json': json.dumps(providers), 'providers_list_json': json.dumps(providers),
'cert_obj_json': json.dumps(cert_obj.to_dict()), 'cert_obj_json': json.dumps(cert_obj.to_dict()),
'enqueue': False, 'enqueue': False,

View File

@ -13,17 +13,20 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from poppy.model import common
AVAILABLE_PROTOCOLS = [ AVAILABLE_PROTOCOLS = [
u'http', u'http',
u'https'] u'https'
]
CERTIFICATE_OPTIONS = [ CERTIFICATE_OPTIONS = [
None, None,
u'shared', u'shared',
u'san', u'san',
u'custom'] u'sni',
u'custom'
from poppy.model import common ]
class Domain(common.DictSerializableModel): class Domain(common.DictSerializableModel):
@ -32,7 +35,7 @@ class Domain(common.DictSerializableModel):
certificate=None): certificate=None):
self._domain = domain.lower().strip() self._domain = domain.lower().strip()
if (protocol in AVAILABLE_PROTOCOLS): if protocol in AVAILABLE_PROTOCOLS:
self._protocol = protocol self._protocol = protocol
else: else:
raise ValueError( raise ValueError(
@ -47,7 +50,7 @@ class Domain(common.DictSerializableModel):
if self._protocol == 'https': if self._protocol == 'https':
if self._certificate not in CERTIFICATE_OPTIONS: if self._certificate not in CERTIFICATE_OPTIONS:
raise ValueError( raise ValueError(
u'Certificate option: {0} is not valid.' u'Certificate option: {0} is not valid. '
'Valid certificate options are: {1}'.format( 'Valid certificate options are: {1}'.format(
certificate, certificate,
CERTIFICATE_OPTIONS) CERTIFICATE_OPTIONS)
@ -82,7 +85,7 @@ class Domain(common.DictSerializableModel):
' non-https domain') ' non-https domain')
if value not in CERTIFICATE_OPTIONS: if value not in CERTIFICATE_OPTIONS:
raise ValueError( raise ValueError(
u'Certificate option: {0} is not valid.' u'Certificate option: {0} is not valid. '
'Valid certificate options are: {1}'.format( 'Valid certificate options are: {1}'.format(
value, value,
CERTIFICATE_OPTIONS) CERTIFICATE_OPTIONS)

View File

@ -16,7 +16,7 @@
from poppy.model import common from poppy.model import common
VALID_CERT_TYPES = [u'san', u'custom', u'dedicated'] VALID_CERT_TYPES = [u'san', u'sni', u'custom', u'dedicated']
VALID_STATUS_IN_CERT_DETAIL = [ VALID_STATUS_IN_CERT_DETAIL = [
u'deployed', u'deployed',
u'create_in_progress', u'create_in_progress',
@ -120,8 +120,8 @@ class SSLCertificate(common.DictSerializableModel):
) )
return result return result
def get_san_edge_name(self): def get_edge_host_name(self):
if self.cert_type == 'san': if self.cert_type in ['san', 'sni']:
if self.cert_details is None or self.cert_details == {}: if self.cert_details is None or self.cert_details == {}:
return None return None
first_provider_cert_details = ( first_provider_cert_details = (
@ -129,7 +129,10 @@ class SSLCertificate(common.DictSerializableModel):
if first_provider_cert_details is None: if first_provider_cert_details is None:
return None return None
else: else:
return first_provider_cert_details.get('san cert', None) if self.cert_type == 'san':
return first_provider_cert_details.get('san cert', None)
else:
return first_provider_cert_details.get('sni_cert', None)
else: else:
return None return None

View File

@ -28,11 +28,11 @@ class ServicesControllerBase(controller.NotificationControllerBase):
def __init__(self, driver): def __init__(self, driver):
super(ServicesControllerBase, self).__init__(driver) super(ServicesControllerBase, self).__init__(driver)
def send(self): def send(self, subject, mail_content):
"""delete. """delete.
:param provider_details :param subject
:param mail_content
:raises NotImplementedError :raises NotImplementedError
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -36,11 +36,10 @@ class ServicesController(base.ServicesBase):
self.recipients = self.driver.recipients self.recipients = self.driver.recipients
def send(self, subject, mail_content): def send(self, subject, mail_content):
"""send notification to a (list) of recipients. """Send notification to a (list) of recipients.
:param subject :param subject
:param mail_content :param mail_content
:raises NotImplementedError
""" """
res = self._send_mail_notification_via_mailgun(subject, mail_content) res = self._send_mail_notification_via_mailgun(subject, mail_content)
if res: if res:

View File

@ -57,59 +57,113 @@ class CheckCertStatusTask(task.Task):
if cert_obj_json != "": if cert_obj_json != "":
cert_obj = ssl_certificate.load_from_json( cert_obj = ssl_certificate.load_from_json(
json.loads(cert_obj_json)) json.loads(cert_obj_json))
latest_sps_id = cert_obj.cert_details['Akamai']['extra_info'].get( if cert_obj.cert_type == 'san':
'akamai_spsId') latest_sps_id = cert_obj.\
current_status = cert_obj.cert_details['Akamai']['extra_info'].get( cert_details['Akamai']['extra_info'].get(
'status') 'akamai_spsId')
current_status = cert_obj.\
cert_details['Akamai']['extra_info'].get(
'status')
if latest_sps_id is None: if latest_sps_id is None:
return current_status return current_status
resp = self.akamai_driver.akamai_sps_api_client.get( resp = self.akamai_driver.akamai_sps_api_client.get(
self.akamai_driver.akamai_sps_api_base_url.format( self.akamai_driver.akamai_sps_api_base_url.format(
spsId=latest_sps_id spsId=latest_sps_id
)
)
if resp.status_code != 200:
raise RuntimeError('SPS API Request Failed'
'Exception: %s' % resp.text)
sps_request_info = json.loads(resp.text)['requestList'][0]
status = sps_request_info['status']
workFlowProgress = sps_request_info.get(
'workflowProgress')
# This SAN Cert is on pending status
if status == 'SPS Request Complete':
LOG.info("SPS completed for %s..." %
cert_obj.get_san_edge_name())
return "deployed"
elif status == 'edge host already created or pending':
if workFlowProgress is not None and \
'error' in workFlowProgress.lower():
LOG.info("SPS Pending with Error:" %
workFlowProgress)
return "failed"
else:
return "deployed"
elif status == 'CPS cancelled':
return "cancelled"
else:
LOG.info(
"SPS Not completed for domain {0}, san_cert {1}. "
"Found status {2}. "
"Returning certificate object to Queue.".format(
cert_obj.domain_name,
cert_obj.get_san_edge_name(),
status
) )
) )
# convert cert_obj_json from unicode -> string
# before enqueue if resp.status_code != 200:
self.akamai_driver.san_mapping_queue.enqueue_san_mapping( raise RuntimeError('SPS API Request Failed'
json.dumps(cert_obj.to_dict())) 'Exception: %s' % resp.text)
return ""
sps_request_info = json.loads(resp.text)['requestList'][0]
status = sps_request_info['status']
workFlowProgress = sps_request_info.get(
'workflowProgress')
# This SAN Cert is on pending status
if status == 'SPS Request Complete':
LOG.info("SPS completed for %s..." %
cert_obj.get_edge_host_name())
return "deployed"
elif status == 'edge host already created or pending':
if workFlowProgress is not None and \
'error' in workFlowProgress.lower():
LOG.info("SPS Pending with Error:" %
workFlowProgress)
return "failed"
else:
return "deployed"
elif status == 'CPS cancelled':
return "cancelled"
else:
LOG.info(
"SPS Not completed for domain {0}, san_cert {1}. "
"Found status {2}. "
"Returning certificate object to Queue.".format(
cert_obj.domain_name,
cert_obj.get_edge_host_name(),
status
)
)
# convert cert_obj_json from unicode -> string
# before enqueue
self.akamai_driver.san_mapping_queue.enqueue_san_mapping(
json.dumps(cert_obj.to_dict()))
return ""
elif cert_obj.cert_type == 'sni':
change_url = cert_obj.cert_details['Akamai']['extra_info'].get(
'change_url')
current_status = cert_obj.\
cert_details['Akamai']['extra_info'].get(
'status')
if change_url is None:
return current_status
enrollment_id = self.akamai_driver.cert_info_storage.\
get_cert_enrollment_id(cert_obj.get_edge_host_name())
headers = {
'Accept': (
'application/vnd.akamai.cps.enrollment.v1+json')
}
resp = self.akamai_driver.akamai_cps_api_client.get(
self.akamai_driver.akamai_cps_api_base_url.format(
enrollmentId=enrollment_id
),
headers=headers
)
if resp.status_code not in [200, 202]:
LOG.error(
"Unable to retrieve enrollment while attempting"
"to update cert status. Status {0} Body {1}".format(
resp.status_code,
resp.text
)
)
return current_status
resp_json = json.loads(resp.text)
pending_changes = resp_json["pendingChanges"]
dns_names = (
resp_json["networkConfiguration"]["sni"]["dnsNames"]
)
if change_url not in pending_changes:
if cert_obj.domain_name in dns_names:
return "deployed"
else:
return "failed"
else:
# the change url is still present under pending changes,
# return the item to the queue. another attempt to
# check and update the cert status should happen
self.akamai_driver.san_mapping_queue.enqueue_san_mapping(
json.dumps(cert_obj.to_dict()))
return current_status
class UpdateCertStatusTask(task.Task): class UpdateCertStatusTask(task.Task):

View File

@ -43,3 +43,15 @@ class BaseAkamaiSanInfoStorage(object):
@abc.abstractmethod @abc.abstractmethod
def list_all_san_cert_names(self): def list_all_san_cert_names(self):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod
def get_san_cert_hostname_limit(self):
raise NotImplementedError
@abc.abstractmethod
def set_san_cert_hostname_limit(self, new_hostname_limit):
raise NotImplementedError
@abc.abstractmethod
def get_cert_enrollment_id(self, san_cert_name):
raise NotImplementedError

View File

@ -315,6 +315,19 @@ class CassandraSanInfoStorage(base.BaseAkamaiSanInfoStorage):
spsId = the_san_cert_info.get('spsId') spsId = the_san_cert_info.get('spsId')
return spsId return spsId
def get_cert_enrollment_id(self, sni_cert_name):
sni_cert_info = self._get_akamai_sni_certs_info().get(
sni_cert_name
)
if sni_cert_info is None:
raise ValueError(
'No enrollment info found for {0}.'.format(sni_cert_name)
)
enrollment_id = sni_cert_info.get('enrollmentId')
return enrollment_id
def get_enabled_status(self, san_cert_name): def get_enabled_status(self, san_cert_name):
the_san_cert_info = self._get_akamai_san_certs_info().get( the_san_cert_info = self._get_akamai_san_certs_info().get(
san_cert_name san_cert_name

View File

@ -98,8 +98,27 @@ class ZookeeperSanInfoStorage(base.BaseAkamaiSanInfoStorage):
spsId, _ = self.zookeeper_client.get(my_sps_id_path) spsId, _ = self.zookeeper_client.get(my_sps_id_path)
return spsId return spsId
def get_cert_enrollment_id(self, san_cert_name):
enrollment_id_path = self._zk_path(san_cert_name, 'enrollmentId')
self.zookeeper_client.ensure_path(enrollment_id_path)
enrollment_id, _ = self.zookeeper_client.get(enrollment_id_path)
return enrollment_id
def _save_cert_property_value(self, san_cert_name, def _save_cert_property_value(self, san_cert_name,
property_name, value): property_name, value):
property_name_path = self._zk_path(san_cert_name, property_name) property_name_path = self._zk_path(san_cert_name, property_name)
self.zookeeper_client.ensure_path(property_name_path) self.zookeeper_client.ensure_path(property_name_path)
self.zookeeper_client.set(property_name_path, str(value)) self.zookeeper_client.set(property_name_path, str(value))
def set_san_cert_hostname_limit(self, new_hostname_limit):
san_cert_hostname_limit_path = self._zk_path('san_cert_hostname_limit')
self.zookeeper_client.ensure_path(san_cert_hostname_limit_path)
hostname_limit, _ = self.zookeeper_client.set(
san_cert_hostname_limit_path, str(new_hostname_limit))
def get_san_cert_hostname_limit(self):
san_cert_hostname_limit_path = self._zk_path('san_cert_hostname_limit')
self.zookeeper_client.ensure_path(san_cert_hostname_limit_path)
hostname_limit, _ = self.zookeeper_client.get(
san_cert_hostname_limit_path)
return hostname_limit

View File

@ -18,6 +18,7 @@ import datetime
import json import json
from oslo_log import log from oslo_log import log
from six.moves import urllib
from poppy.provider.akamai import utils from poppy.provider.akamai import utils
from poppy.provider import base from poppy.provider import base
@ -35,6 +36,10 @@ class CertificateController(base.CertificateBase):
def san_cert_cnames(self): def san_cert_cnames(self):
return self.driver.san_cert_cnames return self.driver.san_cert_cnames
@property
def sni_cert_cnames(self):
return self.driver.sni_cert_cnames
@property @property
def cert_info_storage(self): def cert_info_storage(self):
return self.driver.cert_info_storage return self.driver.cert_info_storage
@ -47,11 +52,16 @@ class CertificateController(base.CertificateBase):
def sps_api_client(self): def sps_api_client(self):
return self.driver.akamai_sps_api_client return self.driver.akamai_sps_api_client
@property
def cps_api_client(self):
return self.driver.akamai_cps_api_client
def __init__(self, driver): def __init__(self, driver):
super(CertificateController, self).__init__(driver) super(CertificateController, self).__init__(driver)
self.driver = driver self.driver = driver
self.sps_api_base_url = self.driver.akamai_sps_api_base_url self.sps_api_base_url = self.driver.akamai_sps_api_base_url
self.cps_api_base_url = self.driver.akamai_cps_api_base_url
def create_certificate(self, cert_obj, enqueue=True, https_upgrade=False): def create_certificate(self, cert_obj, enqueue=True, https_upgrade=False):
if cert_obj.cert_type == 'san': if cert_obj.cert_type == 'san':
@ -280,6 +290,10 @@ class CertificateController(base.CertificateBase):
'san cert failed for {0} failed.'.format( 'san cert failed for {0} failed.'.format(
cert_obj.domain_name) cert_obj.domain_name)
}) })
elif cert_obj.cert_type == 'sni':
# create a DV SAN SNI certificate using Akamai CPS API
return self.create_sni_certificate(
cert_obj, enqueue, https_upgrade)
else: else:
return self.responder.ssl_certificate_provisioned(None, { return self.responder.ssl_certificate_provisioned(None, {
'status': 'failed', 'status': 'failed',
@ -308,3 +322,289 @@ class CertificateController(base.CertificateBase):
break break
return found, found_cert return found, found_cert
def _check_domain_already_exists_on_sni_certs(self, domain_name):
"""Check all configured sni certs for domain."""
found = False
found_cert = None
for sni_cert_name in self.sni_cert_cnames:
sans = utils.get_sans_by_host_alternate(sni_cert_name)
if domain_name in sans:
found = True
found_cert = sni_cert_name
break
return found, found_cert
def create_sni_certificate(self, cert_obj, enqueue, https_upgrade):
try:
found, found_cert = (
self._check_domain_already_exists_on_sni_certs(
cert_obj.domain_name
)
)
if found is True:
return self.responder.ssl_certificate_provisioned(None, {
'status': 'failed',
'sni_cert': None,
'created_at': str(datetime.datetime.now()),
'action': (
'Domain {0} already exists '
'on sni cert {1}.'.format(
cert_obj.domain_name, found_cert
)
)
})
if enqueue:
self.mod_san_queue.enqueue_mod_san_request(
json.dumps(cert_obj.to_dict()))
extras = {
'status': 'create_in_progress',
'sni_cert': None,
# Add logging so it is easier for testing
'created_at': str(datetime.datetime.now()),
'action': (
'SNI cert request for {0} has been '
'enqueued.'.format(cert_obj.domain_name)
)
}
if https_upgrade is True:
extras['https upgrade notes'] = (
"This domain was upgraded from HTTP to HTTPS SNI."
"Take note of the domain name. Where applicable, "
"delete the old HTTP policy after the upgrade is "
"complete or the old policy is no longer in use."
)
return self.responder.ssl_certificate_provisioned(
None,
extras
)
cert_hostname_limit = (
self.cert_info_storage.get_san_cert_hostname_limit()
)
for cert_name in self.sni_cert_cnames:
cert_hostname_limit = (
cert_hostname_limit or
self.driver.san_cert_hostname_limit
)
host_names_count = utils.get_ssl_number_of_hosts_alternate(
cert_name
)
if host_names_count >= cert_hostname_limit:
continue
try:
enrollment_id = (
self.cert_info_storage.get_cert_enrollment_id(
cert_name))
# GET the enrollment by ID
headers = {
'Accept': ('application/vnd.akamai.cps.enrollment.v1+'
'json')
}
resp = self.cps_api_client.get(
self.cps_api_base_url.format(
enrollmentId=enrollment_id),
headers=headers
)
if resp.status_code not in [200, 202]:
raise RuntimeError(
'CPS Request failed. Unable to GET enrollment '
'with id {0} Exception: {1}'.format(
enrollment_id, resp.text))
resp_json = json.loads(resp.text)
# check enrollment does not have any pending changes
if len(resp_json['pendingChanges']) > 0:
LOG.info("{0} has pending changes, skipping...".format(
cert_name))
continue
# adding sans should get them cloned into sni host names
resp_json['csr']['sans'] = resp_json['csr']['sans'].append(
cert_obj.domain_name
)
# PUT the enrollment including the modifications
headers = {
'Content-Type': (
'application/vnd.akamai.cps.enrollment.v1+json'),
'Accept': (
'application/vnd.akamai.cps.enrollment-status.v1+'
'json')
}
resp = self.cps_api_client.put(
self.cps_api_base_url.format(
enrollmentId=enrollment_id),
data=json.dumps(resp_json),
headers=headers
)
if resp.status_code not in [200, 202]:
raise RuntimeError(
'CPS Request failed. Unable to modify enrollment '
'with id {0} Exception: {1}'.format(
enrollment_id, resp.text))
# resp code 200 means PUT didn't create a change
# resp code 202 means PUT created a change
if resp.status_code == 202:
# save the change id for future reference
change_url = json.loads(resp.text)['changes'][0]
cert_copy = copy.deepcopy(cert_obj.to_dict())
(
cert_copy['cert_details']
[self.driver.provider_name]
) = {
'extra_info': {
'change_url': change_url,
'sni_cert': cert_name
}
}
self.san_mapping_queue.enqueue_san_mapping(
json.dumps(cert_copy)
)
return self.responder.ssl_certificate_provisioned(
cert_name, {
'status': 'create_in_progress',
'sni_cert': cert_name,
'change_url': change_url,
'created_at': str(datetime.datetime.now()),
'action': 'Waiting for customer domain '
'validation for {0}'.format(
cert_obj.domain_name)
})
except Exception as exc:
LOG.exception(
"Unable to provision certificate {0}, "
"Error: {1}".format(cert_obj.domain_name, exc))
return self.responder.ssl_certificate_provisioned(None, {
'status': 'failed',
'sni_cert': None,
'created_at': str(datetime.datetime.now()),
'action': 'Waiting for action... CPS API provision '
'DV SNI cert failed for {0} failed.'.format(
cert_obj.domain_name)
})
else:
self.mod_san_queue.enqueue_mod_san_request(
json.dumps(cert_obj.to_dict()))
return self.responder.ssl_certificate_provisioned(None, {
'status': 'create_in_progress',
'sni_cert': None,
# Add logging so it is easier for testing
'created_at': str(datetime.datetime.now()),
'action': 'No available sni cert for {0} right now,'
' or no sni cert info available. Support:'
'Please write down the domain and keep an'
' eye on next available freed-up SNI certs.'
' More provisioning might be needed'.format(
cert_obj.domain_name)
})
except Exception as e:
LOG.exception(
"Error {0} during SNI certificate creation for {1} "
"sending the request sent back to the queue.".format(
e, cert_obj.domain_name
)
)
try:
self.mod_san_queue.enqueue_mod_san_request(
json.dumps(cert_obj.to_dict()))
return self.responder.ssl_certificate_provisioned(None, {
'status': 'create_in_progress',
'sni_cert': None,
# Add logging so it is easier for testing
'created_at': str(datetime.datetime.now()),
'action': (
'SNI cert request for {0} has been '
'enqueued.'.format(cert_obj.domain_name)
)
})
except Exception as exc:
LOG.exception("Unable to enqueue {0}, Error: {1}".format(
cert_obj.domain_name,
exc
))
return self.responder.ssl_certificate_provisioned(None, {
'status': 'failed',
'sni_cert': None,
'created_at': str(datetime.datetime.now()),
'action': 'Waiting for action... Provision '
'sni cert failed for {0} failed.'.format(
cert_obj.domain_name)
})
def delete_certificate(self, cert_obj):
if cert_obj.cert_type == 'sni':
# get change id
first_provider_cert_details = (
list(cert_obj.cert_details.values())[0].get("extra_info", None)
)
change_url = first_provider_cert_details.get('change_url')
if first_provider_cert_details is None or change_url is None:
return self.responder.ssl_certificate_deleted(
cert_obj.domain_name,
{
'status': 'failed',
'reason': (
'Cert is missing details required for delete '
'operation {0}.'.format(
first_provider_cert_details)
)
}
)
headers = {
'Accept': 'application/vnd.akamai.cps.change-id.v1+json'
}
akamai_change_url = urllib.parse.urljoin(
str(self.driver.akamai_conf.policy_api_base_url),
change_url
)
# delete call to cps api to cancel the change
resp = self.cps_api_client.delete(
akamai_change_url,
headers=headers
)
if resp.status_code != 200:
LOG.error(
"Certificate delete for {0} failed. "
"Status code {1}. Response {2}.".format(
cert_obj.domain_name,
resp.status_code,
resp.text,
))
return self.responder.ssl_certificate_deleted(
cert_obj.domain_name,
{
'status': 'failed',
'reason': 'Delete request for {0} failed.'.format(
cert_obj.domain_name)
}
)
else:
LOG.info(
"Successfully cancelled {0}, {1}".format(
cert_obj.domain_name,
resp.text)
)
return self.responder.ssl_certificate_deleted(
cert_obj.domain_name,
{
'status': 'deleted',
'deleted_at': str(datetime.datetime.now()),
'reason': 'Delete request for {0} succeeded.'.format(
cert_obj.domain_name)
}
)
else:
return self.responder.ssl_certificate_provisioned(None, {
'status': 'ignored',
'reason': "Delete cert type {0} not supported.".format(
cert_obj.cert_type
)
})

View File

@ -84,6 +84,11 @@ AKAMAI_OPTIONS = [
help='A list of Akamai configuration number for ' help='A list of Akamai configuration number for '
'SAN cert https policies' 'SAN cert https policies'
), ),
cfg.ListOpt(
'akamai_https_sni_config_numbers',
help='A list of Akamai configuration number for '
'SNI cert https policies'
),
cfg.ListOpt( cfg.ListOpt(
'akamai_https_custom_config_numbers', 'akamai_https_custom_config_numbers',
help='A list of Akamai configuration number for ' help='A list of Akamai configuration number for '
@ -94,7 +99,7 @@ AKAMAI_OPTIONS = [
help='A list of sni certs cname host names'), help='A list of sni certs cname host names'),
# SANCERT related configs # SANCERT related configs
cfg.ListOpt('san_cert_cnames', cfg.ListOpt('san_cert_cnames',
help='A list of san certs cnamehost names'), help='A list of san certs cname host names'),
cfg.IntOpt('san_cert_hostname_limit', default=80, cfg.IntOpt('san_cert_hostname_limit', default=80,
help='default limit on how many hostnames can' help='default limit on how many hostnames can'
' be held by a SAN cert'), ' be held by a SAN cert'),
@ -122,6 +127,7 @@ VALID_PROPERTY_SPEC = [
"akamai_http_config_number", "akamai_http_config_number",
"akamai_https_shared_config_number", "akamai_https_shared_config_number",
"akamai_https_san_config_numbers", "akamai_https_san_config_numbers",
"akamai_https_sni_config_numbers",
"akamai_https_custom_config_numbers"] "akamai_https_custom_config_numbers"]
@ -153,6 +159,8 @@ class CDNProvider(base.Driver):
self.akamai_conf.akamai_https_shared_config_number) self.akamai_conf.akamai_https_shared_config_number)
self.https_san_conf_number = ( self.https_san_conf_number = (
self.akamai_conf.akamai_https_san_config_numbers[-1]) self.akamai_conf.akamai_https_san_config_numbers[-1])
self.https_sni_conf_number = (
self.akamai_conf.akamai_https_sni_config_numbers[-1])
self.https_custom_conf_number = ( self.https_custom_conf_number = (
self.akamai_conf.akamai_https_custom_config_numbers[-1]) self.akamai_conf.akamai_https_custom_config_numbers[-1])
@ -184,6 +192,11 @@ class CDNProvider(base.Driver):
) )
]) ])
self.akamai_cps_api_base_url = ''.join([
str(self.akamai_conf.policy_api_base_url),
'cps/v2/enrollments/{enrollmentId}'
])
self.akamai_papi_api_base_url = ''.join([ self.akamai_papi_api_base_url = ''.join([
str(self.akamai_conf.policy_api_base_url), str(self.akamai_conf.policy_api_base_url),
'papi/v0/{middle_part}/' 'papi/v0/{middle_part}/'
@ -198,6 +211,7 @@ class CDNProvider(base.Driver):
self.akamai_sps_api_client = self.akamai_policy_api_client self.akamai_sps_api_client = self.akamai_policy_api_client
self.akamai_papi_api_client = self.akamai_policy_api_client self.akamai_papi_api_client = self.akamai_policy_api_client
self.akamai_cps_api_client = self.akamai_policy_api_client
self.akamai_sub_customer_api_client = self.akamai_policy_api_client self.akamai_sub_customer_api_client = self.akamai_policy_api_client
self.mod_san_queue = ( self.mod_san_queue = (
zookeeper_queue.ZookeeperModSanQueue(self._conf)) zookeeper_queue.ZookeeperModSanQueue(self._conf))

View File

@ -174,7 +174,7 @@ class ServiceController(base.ServiceBase):
(dp, classified_domain.domain)) (dp, classified_domain.domain))
# pick a san cert for this domain # pick a san cert for this domain
edge_host_name = None edge_host_name = None
if classified_domain.certificate == 'san': if classified_domain.certificate in ['san', 'sni']:
cert_info = getattr(classified_domain, 'cert_info', None) cert_info = getattr(classified_domain, 'cert_info', None)
if cert_info is None: if cert_info is None:
domains_certificate_status[ domains_certificate_status[
@ -182,7 +182,7 @@ class ServiceController(base.ServiceBase):
continue continue
else: else:
edge_host_name = ( edge_host_name = (
classified_domain.cert_info.get_san_edge_name()) classified_domain.cert_info.get_edge_host_name())
domains_certificate_status[classified_domain.domain] \ domains_certificate_status[classified_domain.domain] \
= (classified_domain.cert_info.get_cert_status()) = (classified_domain.cert_info.get_cert_status())
if edge_host_name is None: if edge_host_name is None:
@ -411,7 +411,7 @@ class ServiceController(base.ServiceBase):
'complete' % (dp, classified_domain.domain)) 'complete' % (dp, classified_domain.domain))
edge_host_name = None edge_host_name = None
old_operator_url = None old_operator_url = None
if classified_domain.certificate == 'san': if classified_domain.certificate in ['san', 'sni']:
cert_info = getattr(classified_domain, 'cert_info', cert_info = getattr(classified_domain, 'cert_info',
None) None)
if cert_info is None: if cert_info is None:
@ -422,7 +422,7 @@ class ServiceController(base.ServiceBase):
else: else:
edge_host_name = ( edge_host_name = (
classified_domain.cert_info. classified_domain.cert_info.
get_san_edge_name()) get_edge_host_name())
domain_access_url = service_obj.provider_details[ domain_access_url = service_obj.provider_details[
self.driver.provider_name self.driver.provider_name
].get_domain_access_url(classified_domain.domain) ].get_domain_access_url(classified_domain.domain)
@ -1075,6 +1075,8 @@ class ServiceController(base.ServiceBase):
configuration_number = self.driver.https_shared_conf_number configuration_number = self.driver.https_shared_conf_number
elif domain_obj.certificate == 'san': elif domain_obj.certificate == 'san':
configuration_number = self.driver.https_san_conf_number configuration_number = self.driver.https_san_conf_number
elif domain_obj.certificate == 'sni':
configuration_number = self.driver.https_sni_conf_number
elif domain_obj.certificate == 'custom': elif domain_obj.certificate == 'custom':
configuration_number = self.driver.https_custom_conf_number configuration_number = self.driver.https_custom_conf_number
else: else:
@ -1093,7 +1095,8 @@ class ServiceController(base.ServiceBase):
self.driver.akamai_https_access_url_suffix]) self.driver.akamai_https_access_url_suffix])
elif domain_obj.certificate == 'san': elif domain_obj.certificate == 'san':
if edge_host_name is None: if edge_host_name is None:
raise ValueError("No EdgeHost name provided for SAN Cert") raise ValueError(
"No EdgeHost name provided for SAN Cert")
# ugly fix for existing san cert domains, but we will # ugly fix for existing san cert domains, but we will
# have to take it for now # have to take it for now
elif edge_host_name.endswith( elif edge_host_name.endswith(
@ -1103,6 +1106,10 @@ class ServiceController(base.ServiceBase):
provider_access_url = '.'.join( provider_access_url = '.'.join(
[edge_host_name, [edge_host_name,
self.driver.akamai_https_access_url_suffix]) self.driver.akamai_https_access_url_suffix])
elif domain_obj.certificate == 'sni':
if edge_host_name is None:
raise ValueError("No EdgeHost name provided for SNI Cert")
provider_access_url = edge_host_name
elif domain_obj.certificate == 'custom': elif domain_obj.certificate == 'custom':
provider_access_url = '.'.join( provider_access_url = '.'.join(
[dp, self.driver.akamai_https_access_url_suffix]) [dp, self.driver.akamai_https_access_url_suffix])

View File

@ -13,13 +13,17 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import socket
import ssl import ssl
import sys import sys
from kazoo import client from kazoo import client
from OpenSSL import crypto from OpenSSL import crypto
from oslo_log import log
import six import six
LOG = log.getLogger(__name__)
# Python 3 does not have ssl.PROTOCOL_SSLv2, but has PROTOCOL_TLSv1_1, # Python 3 does not have ssl.PROTOCOL_SSLv2, but has PROTOCOL_TLSv1_1,
# PROTOCOL_TLSv1_2, and for some reason Jenkins will not pil up these # PROTOCOL_TLSv1_2, and for some reason Jenkins will not pil up these
# new versions # new versions
@ -38,20 +42,20 @@ ssl_versions = [
] ]
try: try:
# Warning from python: "documentation SSL version 3 is insecure. # Warning from python documentation "SSL version 3 is insecure.
# Its use is highly discouraged." # Its use is highly discouraged."
# https://docs.python.org/2/library/ssl.html#ssl.PROTOCOL_SSLv3
ssl_versions.append(ssl.PROTOCOL_SSLv3) ssl_versions.append(ssl.PROTOCOL_SSLv3)
except AttributeError: except AttributeError: # pragma: no cover
pass pass # pragma: no cover
ssl_versions.extend(extra_versions) ssl_versions.extend(extra_versions)
def get_ssl_number_of_hosts(remote_host): def get_ssl_number_of_hosts(remote_host):
'''Get number of Alternative names for a (SAN) Cert """Get number of Alternative names for a (SAN) Cert."""
'''
LOG.info("Checking number of hosts for {0}".format(remote_host))
for ssl_version in ssl_versions: for ssl_version in ssl_versions:
try: try:
cert = ssl.get_server_certificate((remote_host, 443), cert = ssl.get_server_certificate((remote_host, 443),
@ -76,13 +80,15 @@ def get_ssl_number_of_hosts(remote_host):
result = len(sans) result = len(sans)
break break
else: else:
raise ValueError('Get remote host certificate info failed...') raise ValueError(
'Get remote host certificate {0} info failed.'.format(remote_host))
return result return result
def get_sans_by_host(remote_host): def get_sans_by_host(remote_host):
"""Get Subject Alternative Names for a (SAN) Cert.""" """Get Subject Alternative Names for a (SAN) Cert."""
LOG.info("Retrieving sans for {0}".format(remote_host))
for ssl_version in ssl_versions: for ssl_version in ssl_versions:
try: try:
cert = ssl.get_server_certificate( cert = ssl.get_server_certificate(
@ -109,10 +115,45 @@ def get_sans_by_host(remote_host):
result = sans result = sans
break break
else: else:
raise ValueError('Get remote host certificate info failed...') raise ValueError(
'Get remote host certificate {0} info failed.'.format(remote_host))
return result return result
def _get_cert_alternate(remote_host):
context = ssl.create_default_context()
conn = context.wrap_socket(socket.socket(socket.AF_INET),
server_hostname=remote_host)
conn.connect((remote_host, 443))
cert = conn.getpeercert()
conn.close()
return cert
def get_ssl_number_of_hosts_alternate(remote_host):
LOG.info("Checking number of hosts for {0}".format(remote_host))
cert = _get_cert_alternate(remote_host)
return len([
san for record_type, san in cert['subjectAltName']
if record_type == 'DNS'
])
def get_sans_by_host_alternate(remote_host):
LOG.info("Retrieving sans for {0}".format(remote_host))
cert = _get_cert_alternate(remote_host)
return [
san for record_type, san in cert['subjectAltName']
if record_type == 'DNS'
]
def connect_to_zookeeper_storage_backend(conf): def connect_to_zookeeper_storage_backend(conf):
"""Connect to a zookeeper cluster""" """Connect to a zookeeper cluster"""
storage_backend_hosts = ','.join(['%s:%s' % ( storage_backend_hosts = ','.join(['%s:%s' % (

View File

@ -139,3 +139,17 @@ class Responder(object):
'extra_info': extra_info 'extra_info': extra_info
} }
} }
def ssl_certificate_deleted(self, cert_domain, extra_info=None):
"""SSL Certificate Deleted.
:param cert_domain
:param extra_info
:returns provider msg{cert_domain, extra_info}
"""
return {
self.provider: {
'cert_domain': cert_domain,
'extra_info': extra_info
}
}

View File

@ -74,4 +74,7 @@ class CertificatesController(base.CertificatesController):
) )
return [cert for cert in certs if cert.project_id == project_id] return [cert for cert in certs if cert.project_id == project_id]
else: else:
return certs if len(certs) == 1:
return certs[0]
else:
return certs

View File

@ -45,7 +45,10 @@ class BackgroundJobSchema(schema_base.SchemaBase):
'job_type': { 'job_type': {
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'enum': ['akamai_update_papi_property_for_mod_san'] 'enum': [
'akamai_update_papi_property_for_mod_san',
'akamai_update_papi_property_for_mod_sni'
]
}, },
'update_type': { 'update_type': {
'type': 'string', 'type': 'string',
@ -57,7 +60,10 @@ class BackgroundJobSchema(schema_base.SchemaBase):
}, },
'property_spec': { 'property_spec': {
'type': 'string', 'type': 'string',
'enum': ['akamai_https_san_config_numbers'] 'enum': [
'akamai_https_san_config_numbers'
'akamai_https_sni_config_numbers'
]
}, },
'san_cert_domain_suffix': { 'san_cert_domain_suffix': {
'type': 'string' 'type': 'string'

View File

@ -112,6 +112,7 @@ class ServiceSchema(schema_base.SchemaBase):
'type': 'string', 'type': 'string',
'enum': [ 'enum': [
'san', 'san',
'sni',
'custom'] 'custom']
}, },
}, },

View File

@ -37,7 +37,7 @@ class SSLCertificateSchema(schema_base.SchemaBase):
'cert_type': { 'cert_type': {
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'enum': ['san'], 'enum': ['san', 'sni'],
}, },
'domain_name': { 'domain_name': {
'type': 'string', 'type': 'string',
@ -151,17 +151,28 @@ class SSLCertificateSchema(schema_base.SchemaBase):
'extra_info': { 'extra_info': {
'type': 'object', 'type': 'object',
'required': True, 'required': True,
'additionalProperties': False,
'properties': { 'properties': {
'san cert': { 'san cert': {
'type': 'string', 'type': 'string',
'required': True, 'required': False,
'minLength': 3,
'maxLength': 253
},
'sni_cert': {
'type': 'string',
'required': False,
'minLength': 3, 'minLength': 3,
'maxLength': 253 'maxLength': 253
}, },
'akamai_spsId': { 'akamai_spsId': {
'type': 'integer', 'type': 'integer',
'required': True 'required': False
} },
'change_url': {
'type': 'string',
'required': False,
},
} }
} }
} }
@ -181,8 +192,12 @@ class SSLCertificateSchema(schema_base.SchemaBase):
'cert_type': { 'cert_type': {
'type': 'string', 'type': 'string',
'required': True, 'required': True,
'enum': ['san'], 'enum': ['san', 'sni'],
} },
'property_activated': {
'type': 'boolean',
'required': False,
},
} }
} }
} }

View File

@ -25,6 +25,7 @@ consumer_key = "MYCONSUMERKEY"
[drivers:provider:akamai] [drivers:provider:akamai]
akamai_https_access_url_suffix = "my_https_url_suffix" akamai_https_access_url_suffix = "my_https_url_suffix"
akamai_https_san_config_numbers = 200000 akamai_https_san_config_numbers = 200000
akamai_https_sni_config_numbers = 200000
akamai_https_custom_config_numbers = 200000 akamai_https_custom_config_numbers = 200000
san_cert_cnames = secure1.test-san.com,secure2.test-san.com san_cert_cnames = secure1.test-san.com,secure2.test-san.com
cert_info_storage_type = cassandra cert_info_storage_type = cassandra

View File

@ -19,7 +19,6 @@ import uuid
import ddt import ddt
import mock import mock
from poppy.transport.validators import helpers as validators
from tests.functional.transport.pecan import base from tests.functional.transport.pecan import base
@ -29,6 +28,14 @@ class SSLCertificateControllerTest(base.FunctionalTest):
def setUp(self): def setUp(self):
super(SSLCertificateControllerTest, self).setUp() super(SSLCertificateControllerTest, self).setUp()
tld_patcher = mock.patch('tld.get_tld')
tld_patcher.start()
self.addCleanup(tld_patcher.stop)
dns_resolver_patcher = mock.patch('dns.resolver')
dns_resolver_patcher.start()
self.addCleanup(dns_resolver_patcher.stop)
self.project_id = str(uuid.uuid1()) self.project_id = str(uuid.uuid1())
self.service_name = str(uuid.uuid1()) self.service_name = str(uuid.uuid1())
self.flavor_id = str(uuid.uuid1()) self.flavor_id = str(uuid.uuid1())
@ -58,7 +65,6 @@ class SSLCertificateControllerTest(base.FunctionalTest):
@ddt.file_data("data_create_ssl_certificate.json") @ddt.file_data("data_create_ssl_certificate.json")
def test_create_ssl_certificate(self, ssl_certificate_json): def test_create_ssl_certificate(self, ssl_certificate_json):
validators.is_valid_tld = mock.Mock(return_value=True)
# override the hardcoded flavor_id in the ddt file with # override the hardcoded flavor_id in the ddt file with
# a custom one defined in setUp() # a custom one defined in setUp()
@ -84,7 +90,7 @@ class SSLCertificateControllerTest(base.FunctionalTest):
self.assertEqual(404, response.status_code) self.assertEqual(404, response.status_code)
def test_get_ssl_certificate_existing_domain(self): def test_get_ssl_certificate_existing_domain(self):
validators.is_valid_tld = mock.Mock(return_value=True) # validators.is_valid_tld = mock.Mock(return_value=True)
domain = 'www.iexist.com' domain = 'www.iexist.com'
ssl_certificate_json = { ssl_certificate_json = {
"cert_type": "san", "cert_type": "san",
@ -108,16 +114,15 @@ class SSLCertificateControllerTest(base.FunctionalTest):
response_list = json.loads(response.body.decode("utf-8")) response_list = json.loads(response.body.decode("utf-8"))
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertEqual(ssl_certificate_json["cert_type"], self.assertEqual(ssl_certificate_json["cert_type"],
response_list[0]["cert_type"]) response_list["cert_type"])
self.assertEqual(ssl_certificate_json["domain_name"], self.assertEqual(ssl_certificate_json["domain_name"],
response_list[0]["domain_name"]) response_list["domain_name"])
self.assertEqual(ssl_certificate_json["flavor_id"], self.assertEqual(ssl_certificate_json["flavor_id"],
response_list[0]["flavor_id"]) response_list["flavor_id"])
self.assertEqual(ssl_certificate_json["project_id"], self.assertEqual(ssl_certificate_json["project_id"],
response_list[0]["project_id"]) response_list["project_id"])
def test_get_ssl_certificate_existing_domain_different_project_id(self): def test_get_ssl_certificate_existing_domain_different_project_id(self):
validators.is_valid_tld = mock.Mock(return_value=True)
domain = 'www.iexist.com' domain = 'www.iexist.com'
ssl_certificate_json = { ssl_certificate_json = {
"cert_type": "san", "cert_type": "san",
@ -159,11 +164,23 @@ class SSLCertificateControllerTest(base.FunctionalTest):
expect_errors=True) expect_errors=True)
self.assertEqual(400, response.status_code) self.assertEqual(400, response.status_code)
def test_delete_cert(self): @ddt.file_data("data_create_ssl_certificate.json")
# create with erroneous data: invalid json data def test_delete_cert(self, ssl_certificate_json):
response = self.app.delete('/v1.0/ssl_certificate/blog.test.com', # create with good data
headers={'X-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)
# delete cert
response = self.app.delete(
'/v1.0/ssl_certificate/{0}'.format(
ssl_certificate_json['domain_name']
),
headers={'X-Project-ID': self.project_id}
)
self.assertEqual(202, response.status_code) self.assertEqual(202, response.status_code)
def test_delete_cert_non_exist(self): def test_delete_cert_non_exist(self):

View File

@ -186,6 +186,27 @@ class TestFlowRuns(base.TestCase):
dns_controller.create._mock_return_value = [] dns_controller.create._mock_return_value = []
common.create_log_delivery_container = mock.Mock() common.create_log_delivery_container = mock.Mock()
@staticmethod
def patch_delete_ssl_certificate_flow(
service_controller,
storage_controller,
dns_controller,
ssl_cert_controller
):
storage_controller.get = mock.Mock()
storage_controller.update = mock.Mock()
ssl_cert_controller.storage.delete_certificate = mock.Mock()
storage_controller._driver.close_connection = mock.Mock()
service_controller.provider_wrapper.delete_certificate = mock.Mock()
service_controller.provider_wrapper.delete_certificate.\
_mock_return_value = []
service_controller._driver = mock.Mock()
service_controller._driver.providers.__getitem__ = mock.Mock()
service_controller._driver.notification = [mock.Mock()]
dns_controller.create = mock.Mock()
dns_controller.create._mock_return_value = []
common.create_log_delivery_container = mock.Mock()
@staticmethod @staticmethod
def patch_recreate_ssl_certificate_flow( def patch_recreate_ssl_certificate_flow(
service_controller, storage_controller, dns_controller): service_controller, storage_controller, dns_controller):
@ -1031,11 +1052,18 @@ class TestFlowRuns(base.TestCase):
store=kwargs) store=kwargs)
def test_delete_ssl_certificate_normal(self): def test_delete_ssl_certificate_normal(self):
providers = ['cdn_provider']
cert_obj_json = ssl_certificate.SSLCertificate(
'cdn',
'mytestsite.com',
'san'
)
kwargs = { kwargs = {
'cert_type': "san", 'cert_type': "san",
'project_id': json.dumps(str(uuid.uuid4())), 'project_id': json.dumps(str(uuid.uuid4())),
'domain_name': "san.san.com", 'domain_name': "mytestsite.com",
'cert_obj_json': json.dumps(cert_obj_json.to_dict()),
'providers_list_json': json.dumps(providers),
'context_dict': context_utils.RequestContext().to_dict() 'context_dict': context_utils.RequestContext().to_dict()
} }
@ -1052,10 +1080,11 @@ class TestFlowRuns(base.TestCase):
ssl_cert_controller, ssl_cert_controller,
memoized_controllers.task_controllers): memoized_controllers.task_controllers):
self.patch_create_ssl_certificate_flow( self.patch_delete_ssl_certificate_flow(
service_controller, service_controller,
storage_controller, storage_controller,
dns_controller dns_controller,
ssl_cert_controller
) )
engines.run( engines.run(
delete_ssl_certificate.delete_ssl_certificate(), delete_ssl_certificate.delete_ssl_certificate(),

View File

@ -59,6 +59,19 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
'san_cert_cnames', 'san_cert_cnames',
group=aka_driver.AKAMAI_GROUP group=aka_driver.AKAMAI_GROUP
) )
conf.set_override(
'sni_cert_cnames',
[
"sni.example.com", "sni2.example.com"
],
group=aka_driver.AKAMAI_GROUP,
enforce_type=True
)
self.addCleanup(
conf.clear_override,
'sni_cert_cnames',
group=aka_driver.AKAMAI_GROUP
)
conf.set_override( conf.set_override(
'akamai_https_access_url_suffix', 'akamai_https_access_url_suffix',
'edge.key.net', 'edge.key.net',
@ -208,7 +221,8 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
@ddt.data( @ddt.data(
"akamai_check_and_update_cert_status", "akamai_check_and_update_cert_status",
"akamai_update_papi_property_for_mod_san" "akamai_update_papi_property_for_mod_san",
"akamai_update_papi_property_for_mod_sni"
) )
def test_post_job_no_akamai_driver(self, job_type): def test_post_job_no_akamai_driver(self, job_type):
del self.provider_mocks['akamai'] del self.provider_mocks['akamai']
@ -219,10 +233,12 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
self.assertEqual(0, len(ignore_list)) self.assertEqual(0, len(ignore_list))
@ddt.data( @ddt.data(
"akamai_check_and_update_cert_status", ("akamai_check_and_update_cert_status", "san"),
"akamai_update_papi_property_for_mod_san" ("akamai_update_papi_property_for_mod_san", "san"),
("akamai_update_papi_property_for_mod_sni", "sni")
) )
def test_post_job_positive(self, job_type): def test_post_job_positive(self, job_tuple):
job_type, cert_type = job_tuple
# mock ssl storage returning a cert # mock ssl storage returning a cert
self.mock_storage.certificates_controller.\ self.mock_storage.certificates_controller.\
get_certs_by_domain.return_value = [ get_certs_by_domain.return_value = [
@ -238,7 +254,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
domain.Domain( domain.Domain(
"www.example.com", "www.example.com",
protocol='https', protocol='https',
certificate='san' certificate=cert_type
) )
], ],
[], [],
@ -248,16 +264,17 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
san_mapping_queue = self.manager_driver.providers[ san_mapping_queue = self.manager_driver.providers[
'akamai'].obj.san_mapping_queue 'akamai'].obj.san_mapping_queue
cert_key = ('san cert' if cert_type == 'san' else 'sni_cert')
san_mapping_queue.traverse_queue.return_value = [ san_mapping_queue.traverse_queue.return_value = [
json.dumps({ json.dumps({
"domain_name": "www.example.com", "domain_name": "www.example.com",
"flavor_id": "flavor_id", "flavor_id": "flavor_id",
"project_id": "project_id", "project_id": "project_id",
"cert_type": "san", "cert_type": cert_type,
"cert_details": { "cert_details": {
"Akamai": { "Akamai": {
"extra_info": { "extra_info": {
"san cert": "san.example.com", cert_key: "{0}.example.com".format(cert_type),
"akamai_spsId": 1 "akamai_spsId": 1
} }
} }
@ -278,10 +295,11 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
self.bgc.distributed_task_controller.submit_task.called self.bgc.distributed_task_controller.submit_task.called
) )
def test_post_job_ignored_cert_no_longer_exists(self): @ddt.data("san", "sni")
def test_post_job_ignored_cert_no_longer_exists(self, cert_type):
self.mock_storage.certificates_controller.\ self.mock_storage.certificates_controller.\
get_certs_by_domain.return_value = [] get_certs_by_domain.return_value = []
cert_key = ('san cert' if cert_type == 'san' else 'sni_cert')
san_mapping_queue = self.manager_driver.providers[ san_mapping_queue = self.manager_driver.providers[
'akamai'].obj.san_mapping_queue 'akamai'].obj.san_mapping_queue
san_mapping_queue.traverse_queue.return_value = [ san_mapping_queue.traverse_queue.return_value = [
@ -289,11 +307,11 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
"domain_name": "www.example.com", "domain_name": "www.example.com",
"flavor_id": "flavor_id", "flavor_id": "flavor_id",
"project_id": "project_id", "project_id": "project_id",
"cert_type": "san", "cert_type": cert_type,
"cert_details": { "cert_details": {
"Akamai": { "Akamai": {
"extra_info": { "extra_info": {
"san cert": "san.example.com", cert_key: "san.example.com",
"akamai_spsId": 1 "akamai_spsId": 1
} }
} }
@ -303,7 +321,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
] ]
run_list, ignore_list = self.bgc.post_job( run_list, ignore_list = self.bgc.post_job(
"akamai_update_papi_property_for_mod_san", "akamai_update_papi_property_for_mod_{0}".format(cert_type),
{'project_id': 'project_id'} {'project_id': 'project_id'}
) )
self.assertEqual(0, len(run_list)) self.assertEqual(0, len(run_list))
@ -314,7 +332,8 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
self.bgc.distributed_task_controller.submit_task.called self.bgc.distributed_task_controller.submit_task.called
) )
def test_post_job_domain_type_modified_on_service(self): @ddt.data("san", "sni")
def test_post_job_domain_type_modified_on_service(self, cert_type):
self.mock_storage.certificates_controller.\ self.mock_storage.certificates_controller.\
get_certs_by_domain.return_value = [ get_certs_by_domain.return_value = [
mock.Mock() mock.Mock()
@ -334,6 +353,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
'flavor_id', 'flavor_id',
project_id='project_id' project_id='project_id'
) )
cert_key = ('san cert' if cert_type == 'san' else 'sni_cert')
san_mapping_queue = self.manager_driver.providers[ san_mapping_queue = self.manager_driver.providers[
'akamai'].obj.san_mapping_queue 'akamai'].obj.san_mapping_queue
san_mapping_queue.traverse_queue.return_value = [ san_mapping_queue.traverse_queue.return_value = [
@ -341,11 +361,11 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
"domain_name": "www.example.com", "domain_name": "www.example.com",
"flavor_id": "flavor_id", "flavor_id": "flavor_id",
"project_id": "project_id", "project_id": "project_id",
"cert_type": "san", "cert_type": cert_type,
"cert_details": { "cert_details": {
"Akamai": { "Akamai": {
"extra_info": { "extra_info": {
"san cert": "san.example.com", cert_key: "san.example.com",
"akamai_spsId": 1 "akamai_spsId": 1
} }
} }
@ -355,7 +375,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
] ]
run_list, ignore_list = self.bgc.post_job( run_list, ignore_list = self.bgc.post_job(
"akamai_update_papi_property_for_mod_san", "akamai_update_papi_property_for_mod_{0}".format(cert_type),
{'project_id': 'project_id'} {'project_id': 'project_id'}
) )
self.assertEqual(0, len(run_list)) self.assertEqual(0, len(run_list))
@ -366,8 +386,55 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
self.bgc.distributed_task_controller.submit_task.called self.bgc.distributed_task_controller.submit_task.called
) )
@ddt.data("akamai_update_papi_property_for_mod_san") @ddt.data("san", "sni")
def test_post_job_invalid_san_cert_cname(self, job_type): def test_post_job_service_no_longer_exists(self, cert_type):
self.mock_storage.certificates_controller.\
get_certs_by_domain.return_value = [
mock.Mock()
]
# simulate domain being changed from https+san to http
self.mock_storage.services_controller. \
get_service_details_by_domain_name.return_value = None
cert_key = ('san cert' if cert_type == 'san' else 'sni_cert')
san_mapping_queue = self.manager_driver.providers[
'akamai'].obj.san_mapping_queue
san_mapping_queue.traverse_queue.return_value = [
json.dumps({
"domain_name": "www.example.com",
"flavor_id": "flavor_id",
"project_id": "project_id",
"cert_type": cert_type,
"cert_details": {
"Akamai": {
"extra_info": {
cert_key: "san.example.com",
"akamai_spsId": 1
}
}
},
'property_activated': True
})
]
run_list, ignore_list = self.bgc.post_job(
"akamai_update_papi_property_for_mod_{0}".format(cert_type),
{'project_id': 'project_id'}
)
self.assertEqual(0, len(run_list))
self.assertEqual(1, len(ignore_list))
self.assertEqual(
False,
self.bgc.distributed_task_controller.submit_task.called
)
@ddt.data(
("akamai_update_papi_property_for_mod_san", "san"),
("akamai_update_papi_property_for_mod_sni", "sni"),
)
def test_post_job_invalid_cert_cname(self, job_tuple):
job_type, cert_type = job_tuple
# mock ssl storage returning a cert # mock ssl storage returning a cert
self.mock_storage.certificates_controller.\ self.mock_storage.certificates_controller.\
get_certs_by_domain.return_value = [ get_certs_by_domain.return_value = [
@ -383,13 +450,14 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
domain.Domain( domain.Domain(
"www.example.com", "www.example.com",
protocol='https', protocol='https',
certificate='san' certificate=cert_type
) )
], ],
[], [],
'flavor_id', 'flavor_id',
project_id='project_id' project_id='project_id'
) )
cert_key = ('san cert' if cert_type == 'san' else 'sni_cert')
san_mapping_queue = self.manager_driver.providers[ san_mapping_queue = self.manager_driver.providers[
'akamai'].obj.san_mapping_queue 'akamai'].obj.san_mapping_queue
san_mapping_queue.traverse_queue.return_value = [ san_mapping_queue.traverse_queue.return_value = [
@ -397,11 +465,11 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
"domain_name": "www.example.com", "domain_name": "www.example.com",
"flavor_id": "flavor_id", "flavor_id": "flavor_id",
"project_id": "project_id", "project_id": "project_id",
"cert_type": "san", "cert_type": cert_type,
"cert_details": { "cert_details": {
"Akamai": { "Akamai": {
"extra_info": { "extra_info": {
"san cert": "not.exist.com", cert_key: "not.exist.com",
"akamai_spsId": 1 "akamai_spsId": 1
} }
} }

View File

@ -237,6 +237,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
json.dumps({ json.dumps({
"domain_name": "a_domain", "domain_name": "a_domain",
"project_id": "00000", "project_id": "00000",
"cert_type": "san",
"flavor_id": "flavor", "flavor_id": "flavor",
"validate_service": True "validate_service": True
}) })
@ -323,6 +324,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
bytearray(json.dumps({ bytearray(json.dumps({
"domain_name": "a_domain", "domain_name": "a_domain",
"project_id": "00000", "project_id": "00000",
"cert_type": "san",
"flavor_id": "flavor", "flavor_id": "flavor",
"validate_service": True "validate_service": True
}), encoding='utf-8') }), encoding='utf-8')
@ -442,6 +444,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
bytearray(json.dumps({ bytearray(json.dumps({
"domain_name": "a_domain", "domain_name": "a_domain",
"project_id": "00000", "project_id": "00000",
"cert_type": "san",
"flavor_id": "flavor", "flavor_id": "flavor",
"validate_service": True "validate_service": True
}), encoding='utf-8') }), encoding='utf-8')
@ -470,6 +473,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
bytearray(json.dumps({ bytearray(json.dumps({
"domain_name": "a_domain", "domain_name": "a_domain",
"project_id": "00000", "project_id": "00000",
"cert_type": "san",
"flavor_id": "flavor", "flavor_id": "flavor",
"validate_service": True "validate_service": True
}), encoding='utf-8') }), encoding='utf-8')

View File

@ -76,7 +76,7 @@ class TestSSLCertificate(base.TestCase):
ssl_cert.cert_details = cert_details ssl_cert.cert_details = cert_details
self.assertEqual(ssl_cert.get_cert_status(), 'deployed') self.assertEqual(ssl_cert.get_cert_status(), 'deployed')
# check san edge on cert_type custom # check san edge on cert_type custom
self.assertEqual(ssl_cert.get_san_edge_name(), None) self.assertEqual(ssl_cert.get_edge_host_name(), None)
cert_details['mock']['extra_info'] = { cert_details['mock']['extra_info'] = {
'status': 'whatever' 'status': 'whatever'
} }
@ -101,7 +101,7 @@ class TestSSLCertificate(base.TestCase):
cert_details=cert_details) cert_details=cert_details)
self.assertEqual(ssl_cert.get_cert_status(), 'create_in_progress') self.assertEqual(ssl_cert.get_cert_status(), 'create_in_progress')
self.assertEqual(ssl_cert.get_san_edge_name(), None) self.assertEqual(ssl_cert.get_edge_host_name(), None)
def test_cert_details_is_none(self): def test_cert_details_is_none(self):
project_id = '12345' project_id = '12345'
@ -117,7 +117,7 @@ class TestSSLCertificate(base.TestCase):
cert_details=cert_details) cert_details=cert_details)
self.assertEqual(ssl_cert.get_cert_status(), 'create_in_progress') self.assertEqual(ssl_cert.get_cert_status(), 'create_in_progress')
self.assertEqual(ssl_cert.get_san_edge_name(), None) self.assertEqual(ssl_cert.get_edge_host_name(), None)
def test_get_san_edge_positive(self): def test_get_san_edge_positive(self):
project_id = '12345' project_id = '12345'
@ -138,7 +138,7 @@ class TestSSLCertificate(base.TestCase):
cert_type=cert_type, cert_type=cert_type,
cert_details=cert_details) cert_details=cert_details)
self.assertEqual( self.assertEqual(
ssl_cert.get_san_edge_name(), 'secureX.sanX.content.com') ssl_cert.get_edge_host_name(), 'secureX.sanX.content.com')
def test_init_from_dict_positive(self): def test_init_from_dict_positive(self):
ssl_cert = ssl_certificate.SSLCertificate.init_from_dict( ssl_cert = ssl_certificate.SSLCertificate.init_from_dict(

View File

@ -29,6 +29,21 @@
], ],
"flavor_id" : "standard" "flavor_id" : "standard"
}, },
"single_one_origin_https_sni": {
"name" : "mysite.com",
"domains": [
{"domain": "parsely.sage.com"},
{"domain": "densely.sage.com",
"protocol": "https",
"certificate": "sni"},
{"domain": "rosemary.thyme.net",
"protocol": "http"}
],
"origins": [
{"origin": "mockdomain.com", "ssl": false, "port": 80}
],
"flavor_id" : "standard"
},
"multiple_origins": { "multiple_origins": {
"name" : "mysite.com", "name" : "mysite.com",
"domains": [ "domains": [

View File

@ -30,12 +30,17 @@ class TestCertificates(base.TestCase):
super(TestCertificates, self).setUp() super(TestCertificates, self).setUp()
self.driver = mock.Mock() self.driver = mock.Mock()
self.driver.provider_name = 'Akamai' self.driver.provider_name = 'Akamai'
self.san_cert_cnames = [str(x) for x in range(7)] self.san_cert_cnames = [str(x) for x in range(7)]
self.driver.san_cert_cnames = self.san_cert_cnames self.driver.san_cert_cnames = self.san_cert_cnames
self.sni_cert_cnames = [str(x) for x in range(7)]
self.driver.sni_cert_cnames = self.sni_cert_cnames
self.driver.akamai_https_access_url_suffix = ( self.driver.akamai_https_access_url_suffix = (
'example.net' 'example.net'
) )
self.driver.akamai_conf.policy_api_base_url = 'https://aka-base.com/'
san_by_host_patcher = mock.patch( san_by_host_patcher = mock.patch(
'poppy.provider.akamai.utils.get_sans_by_host' 'poppy.provider.akamai.utils.get_sans_by_host'
) )
@ -51,6 +56,21 @@ class TestCertificates(base.TestCase):
self.mock_get_sans_by_host.return_value = [] self.mock_get_sans_by_host.return_value = []
self.mock_get_ssl_number_of_hosts.return_value = 10 self.mock_get_ssl_number_of_hosts.return_value = 10
sans_alternate_patcher = mock.patch(
'poppy.provider.akamai.utils.get_sans_by_host_alternate'
)
self.mock_sans_alternate = sans_alternate_patcher.start()
self.addCleanup(sans_alternate_patcher.stop)
num_hosts_alternate_patcher = mock.patch(
'poppy.provider.akamai.utils.get_ssl_number_of_hosts_alternate'
)
self.mock_num_hosts_alternate = num_hosts_alternate_patcher.start()
self.addCleanup(num_hosts_alternate_patcher.stop)
self.mock_sans_alternate.return_value = []
self.mock_num_hosts_alternate.return_value = 10
self.controller = certificates.CertificateController(self.driver) self.controller = certificates.CertificateController(self.driver)
@ddt.data(("SPS Request Complete", ""), @ddt.data(("SPS Request Complete", ""),
@ -402,7 +422,7 @@ class TestCertificates(base.TestCase):
responder = controller.create_certificate( responder = controller.create_certificate(
ssl_certificate.load_from_json(data), ssl_certificate.load_from_json(data),
True enqueue=True
) )
self.assertIsNone(responder['Akamai']['cert_domain']) self.assertIsNone(responder['Akamai']['cert_domain'])
@ -414,3 +434,623 @@ class TestCertificates(base.TestCase):
'Domain www.abc.com already exists on san cert 0.', 'Domain www.abc.com already exists on san cert 0.',
responder['Akamai']['extra_info']['action'] responder['Akamai']['extra_info']['action']
) )
def test_cert_create_get_sans_by_host_name_exception(self):
data = {
"cert_type": "sni",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
self.mock_sans_alternate.side_effect = Exception(
"Mock -- Error getting sans by host name!"
)
controller = certificates.CertificateController(self.driver)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=True
)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertEqual(
'create_in_progress',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'SNI cert request for www.abc.com has been enqueued.',
responder['Akamai']['extra_info']['action']
)
def test_create_san_exception_enqueue_failure(self):
data = {
"cert_type": "san",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
self.mock_get_sans_by_host.side_effect = Exception(
"Mock -- Error getting sans by host name!"
)
self.driver.mod_san_queue.enqueue_mod_san_request.side_effect = (
Exception("Mock -- Error sending object back to queue!")
)
controller = certificates.CertificateController(self.driver)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=True
)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertIsNone(responder['Akamai']['extra_info']['san cert'])
self.assertEqual(
'failed',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Waiting for action... Provision '
'san cert failed for www.abc.com failed.',
responder['Akamai']['extra_info']['action']
)
def test_create_sni_exception_enqueue_failure(self):
data = {
"cert_type": "sni",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
self.mock_sans_alternate.side_effect = Exception(
"Mock -- Error getting sans by host name!"
)
self.driver.mod_san_queue.enqueue_mod_san_request.side_effect = (
Exception("Mock -- Error sending object back to queue!")
)
controller = certificates.CertificateController(self.driver)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=True
)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertIsNone(responder['Akamai']['extra_info']['sni_cert'])
self.assertEqual(
'failed',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Waiting for action... Provision '
'sni cert failed for www.abc.com failed.',
responder['Akamai']['extra_info']['action']
)
def test_cert_create_domain_exists_on_sni_certs(self):
data = {
"cert_type": "sni",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
self.mock_sans_alternate.return_value = [data["domain_name"]]
controller = certificates.CertificateController(self.driver)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=True
)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertEqual(
'failed',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Domain www.abc.com already exists on sni cert 0.',
responder['Akamai']['extra_info']['action']
)
@ddt.data(True, False)
def test_cert_create_sni_send_to_queue(self, https_upgrade):
data = {
"cert_type": "sni",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller = certificates.CertificateController(self.driver)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=True,
https_upgrade=https_upgrade
)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertEqual(
'create_in_progress',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'SNI cert request for www.abc.com has been enqueued.',
responder['Akamai']['extra_info']['action']
)
if https_upgrade is True:
self.assertTrue(
'https upgrade notes' in responder['Akamai']['extra_info'])
else:
self.assertFalse(
'https upgrade notes' in responder['Akamai']['extra_info'])
mod_san_q = self.driver.mod_san_queue
mod_san_q.enqueue_mod_san_request.assert_called_once_with(
json.dumps(ssl_certificate.load_from_json(data).to_dict())
)
@ddt.data(True, False)
def test_cert_create_san_send_to_queue(self, https_upgrade):
data = {
"cert_type": "san",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller = certificates.CertificateController(self.driver)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=True,
https_upgrade=https_upgrade
)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertEqual(
'create_in_progress',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'San cert request for www.abc.com has been enqueued.',
responder['Akamai']['extra_info']['action']
)
if https_upgrade is True:
self.assertTrue(
'https upgrade notes' in responder['Akamai']['extra_info'])
else:
self.assertFalse(
'https upgrade notes' in responder['Akamai']['extra_info'])
mod_san_q = self.driver.mod_san_queue
mod_san_q.enqueue_mod_san_request.assert_called_once_with(
json.dumps(ssl_certificate.load_from_json(data).to_dict())
)
def test_cert_create_san_exceeds_host_name_limit(self):
self.mock_get_ssl_number_of_hosts.return_value = 100
data = {
"cert_type": "san",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller = certificates.CertificateController(self.driver)
controller.cert_info_storage.get_san_cert_hostname_limit. \
return_value = 80
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=False
)
self.assertIsNone(responder['Akamai']['extra_info']['san cert'])
self.assertTrue('created_at' in responder['Akamai']['extra_info'])
self.assertEqual(
'create_in_progress',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'No available san cert for www.abc.com right now, or no '
'san cert info available. Support:Please write down the '
'domain and keep an eye on next available freed-up SAN certs. '
'More provisioning might be needed',
responder['Akamai']['extra_info']['action']
)
def test_cert_create_san_cert_disabled(self):
data = {
"cert_type": "san",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller = certificates.CertificateController(self.driver)
controller.cert_info_storage.get_san_cert_hostname_limit. \
return_value = 80
controller.cert_info_storage.get_enabled_status. \
return_value = False
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=False
)
self.assertIsNone(responder['Akamai']['extra_info']['san cert'])
self.assertTrue('created_at' in responder['Akamai']['extra_info'])
self.assertEqual(
'create_in_progress',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'No available san cert for www.abc.com right now, or no '
'san cert info available. Support:Please write down the '
'domain and keep an eye on next available freed-up SAN certs. '
'More provisioning might be needed',
responder['Akamai']['extra_info']['action']
)
def test_cert_create_sni_exceeds_host_name_limit(self):
self.mock_num_hosts_alternate.return_value = 100
data = {
"cert_type": "sni",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller = certificates.CertificateController(self.driver)
controller.cert_info_storage.get_san_cert_hostname_limit. \
return_value = 80
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=False
)
self.assertIsNone(responder['Akamai']['extra_info']['sni_cert'])
self.assertTrue('created_at' in responder['Akamai']['extra_info'])
self.assertEqual(
'create_in_progress',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'No available sni cert for www.abc.com right now, or no '
'sni cert info available. Support:Please write down the '
'domain and keep an eye on next available freed-up SNI certs. '
'More provisioning might be needed',
responder['Akamai']['extra_info']['action']
)
def test_cert_create_sni_cert_positive(self):
data = {
"cert_type": "sni",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller = certificates.CertificateController(self.driver)
controller.cert_info_storage.get_san_cert_hostname_limit. \
return_value = 80
controller.cert_info_storage.get_entrollment_id.return_value = 1234
controller.cps_api_client.get.return_value = mock.Mock(
status_code=200,
text=json.dumps({
"csr": {
"cn": "www.example.com",
"c": "US",
"st": "MA",
"l": "Cambridge",
"o": "Akamai",
"ou": "WebEx",
"sans": [
"example.com",
"test.example.com"
]
},
"pendingChanges": []
})
)
controller.cps_api_client.put.return_value = mock.Mock(
status_code=202,
text=json.dumps({
"enrollment": "/cps/v2/enrollments/234",
"changes": [
"/cps/v2/enrollments/234/changes/10001"
]
})
)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=False
)
self.assertEqual(
self.sni_cert_cnames[0],
responder['Akamai']['cert_domain']
)
self.assertEqual(
'create_in_progress',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Waiting for customer domain validation for www.abc.com',
responder['Akamai']['extra_info']['action']
)
self.assertEqual(
'create_in_progress',
responder['Akamai']['extra_info']['status']
)
def test_cert_create_sni_cert_modify_enrollment_failure(self):
data = {
"cert_type": "sni",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller = certificates.CertificateController(self.driver)
controller.cert_info_storage.get_san_cert_hostname_limit. \
return_value = 80
controller.cert_info_storage.get_entrollment_id.return_value = 1234
controller.cps_api_client.get.return_value = mock.Mock(
status_code=200,
text=json.dumps({
"csr": {
"cn": "www.example.com",
"c": "US",
"st": "MA",
"l": "Cambridge",
"o": "Akamai",
"ou": "WebEx",
"sans": [
"example.com",
"test.example.com"
]
},
"pendingChanges": []
})
)
controller.cps_api_client.put.return_value = mock.Mock(
status_code=500,
text='INTERNAL SERVER ERROR'
)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=False
)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertIsNone(responder['Akamai']['extra_info']['sni_cert'])
self.assertEqual(
'failed',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Waiting for action... CPS API provision '
'DV SNI cert failed for www.abc.com failed.',
responder['Akamai']['extra_info']['action']
)
def test_cert_create_sni_cert_get_enrollment_failure(self):
data = {
"cert_type": "sni",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller = certificates.CertificateController(self.driver)
controller.cert_info_storage.get_san_cert_hostname_limit. \
return_value = 80
controller.cert_info_storage.get_entrollment_id.return_value = 1234
controller.cps_api_client.get.return_value = mock.Mock(
status_code=404,
text='Enrollment not found.'
)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=False
)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertIsNone(responder['Akamai']['extra_info']['sni_cert'])
self.assertEqual(
'failed',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Waiting for action... CPS API provision '
'DV SNI cert failed for www.abc.com failed.',
responder['Akamai']['extra_info']['action']
)
def test_cert_create_sni_cert_pending_changes(self):
data = {
"cert_type": "sni",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller = certificates.CertificateController(self.driver)
controller.cert_info_storage.get_san_cert_hostname_limit. \
return_value = 80
controller.cert_info_storage.get_entrollment_id.return_value = 1234
controller.cps_api_client.get.return_value = mock.Mock(
status_code=200,
text=json.dumps({
"csr": {
"cn": "www.example.com",
"c": "US",
"st": "MA",
"l": "Cambridge",
"o": "Akamai",
"ou": "WebEx",
"sans": [
"example.com",
"test.example.com"
]
},
"pendingChanges": [
"/cps/v2/enrollments/234/changes/10000"
]
})
)
controller.cps_api_client.put.return_value = mock.Mock(
status_code=500,
text='INTERNAL SERVER ERROR'
)
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
enqueue=False
)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertIsNone(responder['Akamai']['extra_info']['sni_cert'])
self.assertTrue('created_at' in responder['Akamai']['extra_info'])
self.assertEqual(
'create_in_progress',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'No available sni cert for www.abc.com right now, or no '
'sni cert info available. Support:Please write down the '
'domain and keep an eye on next available freed-up SNI certs. '
'More provisioning might be needed',
responder['Akamai']['extra_info']['action']
)
def test_delete_certificate_positive(self):
cert_obj = ssl_certificate.load_from_json({
"flavor_id": "flavor_id",
"domain_name": "www.abc.com",
"cert_type": "sni",
"project_id": "project_id",
"cert_details": {
'Akamai': {
'extra_info': {
'change_url': "/cps/v2/enrollments/234/changes/100"
}
}
}
})
controller = certificates.CertificateController(self.driver)
controller.cps_api_client.delete.return_value = mock.Mock(
status_code=200,
text=json.dumps({
"change": "/cps/v2/enrollments/234/changes/100"
})
)
responder = controller.delete_certificate(cert_obj)
self.assertEqual('www.abc.com', responder['Akamai']['cert_domain'])
self.assertTrue('deleted_at' in responder['Akamai']['extra_info'])
self.assertEqual(
'deleted',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Delete request for www.abc.com succeeded.',
responder['Akamai']['extra_info']['reason']
)
def test_delete_certificate_cps_api_failure(self):
cert_obj = ssl_certificate.load_from_json({
"flavor_id": "flavor_id",
"domain_name": "www.abc.com",
"cert_type": "sni",
"project_id": "project_id",
"cert_details": {
'Akamai': {
'extra_info': {
'change_url': "/cps/v2/enrollments/234/changes/100"
}
}
}
})
controller = certificates.CertificateController(self.driver)
controller.cps_api_client.delete.return_value = mock.Mock(
status_code=500,
text='INTERNAL SERVER ERROR'
)
responder = controller.delete_certificate(cert_obj)
self.assertEqual('www.abc.com', responder['Akamai']['cert_domain'])
self.assertEqual(
'failed',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Delete request for www.abc.com failed.',
responder['Akamai']['extra_info']['reason']
)
def test_delete_certificate_missing_provider_details(self):
cert_obj = ssl_certificate.load_from_json({
"flavor_id": "flavor_id",
"domain_name": "www.abc.com",
"cert_type": "sni",
"project_id": "project_id",
"cert_details": {
'Akamai': {
'extra_info': {}
}
}
})
controller = certificates.CertificateController(self.driver)
controller.cps_api_client.delete.return_value = mock.Mock(
status_code=500,
text='INTERNAL SERVER ERROR'
)
responder = controller.delete_certificate(cert_obj)
self.assertEqual('www.abc.com', responder['Akamai']['cert_domain'])
self.assertEqual(
'failed',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Cert is missing details required for delete operation {}.',
responder['Akamai']['extra_info']['reason']
)
def test_delete_certificate_unsupported_cert_type(self):
cert_obj = ssl_certificate.load_from_json({
"flavor_id": "flavor_id",
"domain_name": "www.abc.com",
"cert_type": "san",
"project_id": "project_id",
"cert_details": {}
})
controller = certificates.CertificateController(self.driver)
responder = controller.delete_certificate(cert_obj)
self.assertIsNone(responder['Akamai']['cert_domain'])
self.assertEqual(
'ignored',
responder['Akamai']['extra_info']['status']
)
self.assertEqual(
'Delete cert type san not supported.',
responder['Akamai']['extra_info']['reason']
)

View File

@ -73,6 +73,12 @@ AKAMAI_OPTIONS = [
help='A list of Akamai configuration number for ' help='A list of Akamai configuration number for '
'SAN cert https policies' 'SAN cert https policies'
), ),
cfg.ListOpt(
'akamai_https_sni_config_numbers',
default=[str(random.randint(10000, 99999))],
help='A list of Akamai configuration number for '
'SNI cert https policies'
),
cfg.ListOpt( cfg.ListOpt(
'akamai_https_custom_config_numbers', 'akamai_https_custom_config_numbers',
default=[str(random.randint(10000, 99999))], default=[str(random.randint(10000, 99999))],
@ -80,11 +86,11 @@ AKAMAI_OPTIONS = [
'Custom cert https policies' 'Custom cert https policies'
), ),
cfg.ListOpt('sni_cert_cnames', default='secure.san.test.com', cfg.ListOpt('sni_cert_cnames', default='secure.sni.test.com',
help='A list of sni certs cname host names'), help='A list of sni certs cname host names'),
# SANCERT related configs # SANCERT related configs
cfg.ListOpt('san_cert_cnames', default='secure.san.test.com', cfg.ListOpt('san_cert_cnames', default='secure.san.test.com',
help='A list of san certs cnamehost names'), help='A list of san certs cname host names'),
cfg.IntOpt('san_cert_hostname_limit', default=80, cfg.IntOpt('san_cert_hostname_limit', default=80,
help='default limit on how many hostnames can' help='default limit on how many hostnames can'
' be held by a SAN cert'), ' be held by a SAN cert'),

View File

@ -153,9 +153,14 @@ class TestServices(base.TestCase):
) )
for curr_domain in service_obj.domains: for curr_domain in service_obj.domains:
if ( if (
curr_domain.certificate == 'san' and curr_domain.certificate in ['san', 'sni'] and
curr_domain.protocol == 'https' curr_domain.protocol == 'https'
): ):
cert_key = (
'san cert'
if curr_domain.certificate == 'san'
else 'sni_cert'
)
curr_domain.cert_info = ssl_certificate.SSLCertificate( curr_domain.cert_info = ssl_certificate.SSLCertificate(
'flavor_id', 'flavor_id',
curr_domain.domain, curr_domain.domain,
@ -166,7 +171,7 @@ class TestServices(base.TestCase):
cert_domain='1', cert_domain='1',
extra_info={ extra_info={
'status': 'deployed', 'status': 'deployed',
'san cert': '1', cert_key: '1',
'created_at': str(datetime.datetime.now()) 'created_at': str(datetime.datetime.now())
} }
) )
@ -195,7 +200,7 @@ class TestServices(base.TestCase):
num_domains_not_deployed = 0 num_domains_not_deployed = 0
for curr_domain in service_obj.domains: for curr_domain in service_obj.domains:
if ( if (
curr_domain.certificate == 'san' and curr_domain.certificate in ['san', 'sni'] and
curr_domain.protocol == 'https' curr_domain.protocol == 'https'
): ):
num_domains_not_deployed += 1 num_domains_not_deployed += 1
@ -615,9 +620,14 @@ class TestServices(base.TestCase):
san_domains = [] san_domains = []
for curr_domain in service_obj.domains: for curr_domain in service_obj.domains:
if ( if (
curr_domain.certificate == 'san' and curr_domain.certificate in ['san', 'sni'] and
curr_domain.protocol == 'https' curr_domain.protocol == 'https'
): ):
cert_key = (
'san cert'
if curr_domain.certificate == 'san'
else 'sni_cert'
)
curr_domain.cert_info = ssl_certificate.SSLCertificate( curr_domain.cert_info = ssl_certificate.SSLCertificate(
'flavor_id', 'flavor_id',
curr_domain.domain, curr_domain.domain,
@ -628,7 +638,7 @@ class TestServices(base.TestCase):
cert_domain='1', cert_domain='1',
extra_info={ extra_info={
'status': 'deployed', 'status': 'deployed',
'san cert': '1', cert_key: '1',
'created_at': str(datetime.datetime.now()) 'created_at': str(datetime.datetime.now())
} }
) )

View File

@ -0,0 +1,174 @@
# Copyright (c) 2016 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 ssl
import mock
from poppy.provider.akamai import utils
from tests.unit import base
class TestAkamaiUtils(base.TestCase):
def setUp(self):
super(TestAkamaiUtils, self).setUp()
ssl_server_cert_patcher = mock.patch('ssl.get_server_certificate')
self.mock_get_server_cert = ssl_server_cert_patcher.start()
self.addCleanup(ssl_server_cert_patcher.stop)
ssl_crypto_patcher = mock.patch('OpenSSL.crypto.load_certificate')
self.mock_ssl_crypto = ssl_crypto_patcher.start()
self.addCleanup(ssl_crypto_patcher.stop)
ssl_context_patcher = mock.patch('ssl.create_default_context')
self.mock_ssl_context = ssl_context_patcher.start()
self.addCleanup(ssl_context_patcher.stop)
self.mock_ssl_context.return_value.wrap_socket.return_value. \
getpeercert.return_value = {
'issuer': (
(('countryName', 'IL'),),
(('organizationName', 'Issuer Ltd.'),),
(('organizationalUnitName', 'Secure Cert Signing'),),
(('commonName', 'Secure CA'),)
),
'notAfter': 'Nov 22 08:15:19 2013 GMT',
'notBefore': 'Nov 21 03:09:52 2011 GMT',
'serialNumber': 'DEAD',
'subject': (
(('description', 'Some-DESCRIPTION'),),
(('countryName', 'US'),),
(('stateOrProvinceName', 'Georgia'),),
(('localityName', 'Atlanta'),),
(('organizationName', 'R_Host, Inc.'),),
(('commonName', '*.r_host'),),
(('emailAddress', 'host_master@r_host'),)
),
'subjectAltName': (('DNS', '*.r_host'), ('DNS', 'r_host')),
'version': 3
}
def test_get_ssl_number_of_hosts_alternate(self):
self.assertEqual(
2, utils.get_ssl_number_of_hosts_alternate('remote_host'))
def test_get_sans_by_host_alternate(self):
self.assertEqual(
['*.r_host', 'r_host'],
utils.get_sans_by_host_alternate('remote_host')
)
def test_get_ssl_positive(self):
def get_cert(tuple, ssl_version):
if ssl_version == ssl.PROTOCOL_TLSv1:
return mock.MagicMock()
else:
raise ssl.SSLError()
self.mock_get_server_cert.side_effect = get_cert
id_mock = mock.MagicMock()
id_mock.get_short_name.return_value = 'subjectAltName'
id_mock.__str__.return_value = 'r_host'
def get_ext(index):
if index == 0:
return id_mock
else:
return mock.Mock()
self.mock_ssl_crypto.return_value.\
get_extension_count.return_value = 2
self.mock_ssl_crypto.return_value.\
get_extension.side_effect = get_ext
self.assertEqual(1, utils.get_ssl_number_of_hosts('remote_host'))
self.assertEqual(['r_host'], utils.get_sans_by_host('remote_host'))
def test_get_ssl_no_extensions_on_cert(self):
def get_cert(tuple, ssl_version):
if ssl_version == ssl.PROTOCOL_TLSv1:
return mock.MagicMock()
else:
raise ssl.SSLError()
self.mock_get_server_cert.side_effect = get_cert
id_mock = mock.MagicMock()
id_mock.get_short_name.return_value = 'subjectAltName'
id_mock.__str__.return_value = 'r_host'
def get_ext(index):
if index == 0:
return id_mock
else:
return mock.Mock()
self.mock_ssl_crypto.return_value.\
get_extension_count.return_value = 0
self.mock_ssl_crypto.return_value.\
get_extension.side_effect = get_ext
self.assertEqual(0, utils.get_ssl_number_of_hosts('remote_host'))
self.assertEqual([], utils.get_sans_by_host('remote_host'))
def test_get_ssl_no_san_extension(self):
def get_cert(tuple, ssl_version):
if ssl_version == ssl.PROTOCOL_TLSv1:
return mock.MagicMock()
else:
raise ssl.SSLError()
self.mock_get_server_cert.side_effect = get_cert
id_mock = mock.MagicMock()
def get_ext(index):
if index == 0:
return id_mock
else:
return mock.Mock()
self.mock_ssl_crypto.return_value.\
get_extension_count.return_value = 2
self.mock_ssl_crypto.return_value.\
get_extension.side_effect = get_ext
self.assertEqual(0, utils.get_ssl_number_of_hosts('remote_host'))
self.assertEqual([], utils.get_sans_by_host('remote_host'))
def test_get_ssl_number_of_hosts_exception(self):
self.mock_get_server_cert.side_effect = ssl.SSLError()
id_mock = mock.MagicMock()
id_mock.get_short_name.return_value = 'subjectAltName'
id_mock.__str__.return_value = 'r_host'
def get_ext(index):
if index == 0:
return id_mock
else:
return mock.Mock()
self.mock_ssl_crypto.return_value.\
get_extension_count.return_value = 2
self.mock_ssl_crypto.return_value.\
get_extension.side_effect = get_ext
self.assertRaises(
ValueError, utils.get_ssl_number_of_hosts, 'remote_host')
self.assertRaises(ValueError, utils.get_sans_by_host, 'remote_host')