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_https_shared_config_number = 'MY_AKAMAI_HTTPS_CONFIG_SHARED_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'
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"
group_id = "MY_GROUP_ID"
property_id = "MY_PROPERTY_ID"

View File

@ -43,7 +43,7 @@ def delete_service():
delete_service_tasks.GatherProviderDetailsTask(
rebind=['responders', 'dns_responder']),
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(
common.UpdateProviderDetailIfNotEmptyTask(

View File

@ -219,8 +219,8 @@ class UpdateProviderDetailIfNotEmptyTask(task.Task):
project_id,
service_id))
LOG.info('Updating service detail task'
'complete for Changed Provider Details :'
LOG.info('Updating service detail task '
'complete for Changed Provider Details : '
'{0}'.format(changed_provider_details_dict))
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))
responders = []
# try to create all service from each provider
# try to create all certificates from each provider
for provider in providers_list:
LOG.info('Starting to create ssl certificate: {0}'
'from {1}'.format(cert_obj.to_dict(), provider))

View File

@ -218,7 +218,8 @@ class DeleteStorageServiceTask(task.Task):
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):
service_controller, self.storage_controller = \
@ -231,16 +232,21 @@ class DeleteCertificatesForServiceSanDomains(task.Task):
kwargs = {
'project_id': project_id,
'cert_type': 'san',
'context_dict': context_utils.get_current().to_dict()
}
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["cert_type"] = domain.certificate
LOG.info(
"Delete service submit task san_cert deletion {0}".format(
domain.domain
"Delete service submit task {0} cert delete "
"domain {1}.".format(
domain.certificate,
domain.domain,
)
)
service_controller.distributed_task_controller.submit_task(

View File

@ -13,11 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from oslo_config import cfg
from oslo_log import log
from taskflow import task
from poppy.distributed_task.utils import memoized_controllers
from poppy.transport.pecan.models.request import ssl_certificate
LOG = log.getLogger(__name__)
@ -28,11 +31,24 @@ conf(project='poppy', prog='poppy', args=[])
class DeleteProviderSSLCertificateTask(task.Task):
default_provides = "responders"
def execute(self):
# Note(tonytan4ever): For right now there is no
# way to code the process of deleting a certificate object
# from Akamai
def execute(self, providers_list_json, cert_obj_json):
service_controller = memoized_controllers.task_controllers('poppy')
cert_obj = ssl_certificate.load_from_json(json.loads(cert_obj_json))
providers_list = json.loads(providers_list_json)
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
@ -45,10 +61,23 @@ class SendNotificationTask(task.Task):
"Project ID: %s, Domain Name: %s, Cert type: %s" %
(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:
service_controller.notification_wrapper.send(
n_driver,
n_driver.obj.notification_subject,
"Poppy Certificate Deleted",
notification_content)
return

View File

@ -74,12 +74,14 @@ class ProviderWrapper(object):
purge_url)
def create_certificate(self, ext, cert_obj, enqueue, https_upgrade):
"""Create a certificate
"""Create a certificate.
:param ext
:param cert_obj
: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(
@ -87,3 +89,13 @@ class ProviderWrapper(object):
enqueue,
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
self.akamai_san_cert_cname_list = self.driver.conf[
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[
n_driver.MAIL_NOTIFICATION_GROUP].recipients
self.cert_storage = self._driver.storage.certificates_controller
@ -127,96 +129,109 @@ class BackgroundJobController(base.BackgroundJobController):
cert_dict = dict()
try:
cert_dict = json.loads(cert)
# add validation 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'],
'san',
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
)
if cert_dict['cert_type'] == 'san':
# add validation 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']
)
continue
service_obj = self.service_storage.\
get_service_details_by_domain_name(
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_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
)
)
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,
kwargs.get(
"property_spec",
'akamai_https_san_config_numbers'
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
# Note(tonytan4ever): Put this check here so erroneous san
# cert params will not pass. Support occasionally put in
# the ending "edgekey.net"
# (e.g: securexxx.san1.abc.com.edgekey.net), this check
# will effectively error that out
if san_cert not in self.akamai_san_cert_cname_list:
raise ValueError(
"Not A valid san cert cname: {0}, "
"valid san cert cnames are: {1}".format(
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 == '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,
self.akamai_san_cert_cname_list
kwargs.get(
"property_spec",
'akamai_https_san_config_numbers'
)
)
)
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)
# Note(tonytan4ever): Put this check here so erroneous
# san cert params will not pass. Support occasionally
# put in the ending "edgekey.net"
# (e.g: securexxx.san1.abc.com.edgekey.net), this check
# will effectively error that out
if san_cert not in self.akamai_san_cert_cname_list:
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:
cert_dict['error_message'] = str(e)
ignore_list.append(cert_dict)
@ -239,6 +254,156 @@ class BackgroundJobController(base.BackgroundJobController):
"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
# the task, avoids creating a new property version when there are
# no changes to be made.

View File

@ -86,10 +86,22 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
return kwargs
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 = {
'project_id': project_id,
'domain_name': domain_name,
'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()
}
self.distributed_task_controller.submit_task(
@ -124,6 +136,7 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
{"domain_name": r['domain_name'],
"project_id": r['project_id'],
"flavor_id": r['flavor_id'],
"cert_type": r['cert_type'],
"validate_service": r.get('validate_service', True)}
for r in res
]
@ -238,7 +251,7 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
cert_obj = ssl_certificate.SSLCertificate(
cert_obj_dict['flavor_id'],
cert_obj_dict['domain_name'],
'san',
cert_obj_dict['cert_type'],
project_id=cert_obj_dict['project_id']
)
@ -254,6 +267,8 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
# If this cert has been deployed through manual
# process we ignore the rerun process for this entry
if cert_for_domain.get_cert_status() == 'deployed':
run_list.remove(cert_obj_dict)
ignore_list.append(cert_obj_dict)
continue
# rerun the san process
try:
@ -266,7 +281,7 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
kwargs = {
'project_id': cert_obj.project_id,
'domain_name': cert_obj.domain_name,
'cert_type': 'san',
'cert_type': cert_obj.cert_type,
'providers_list_json': json.dumps(providers),
'cert_obj_json': json.dumps(cert_obj.to_dict()),
'enqueue': False,

View File

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

View File

@ -16,7 +16,7 @@
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 = [
u'deployed',
u'create_in_progress',
@ -120,8 +120,8 @@ class SSLCertificate(common.DictSerializableModel):
)
return result
def get_san_edge_name(self):
if self.cert_type == 'san':
def get_edge_host_name(self):
if self.cert_type in ['san', 'sni']:
if self.cert_details is None or self.cert_details == {}:
return None
first_provider_cert_details = (
@ -129,7 +129,10 @@ class SSLCertificate(common.DictSerializableModel):
if first_provider_cert_details is None:
return None
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:
return None

View File

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

View File

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

View File

@ -57,59 +57,113 @@ class CheckCertStatusTask(task.Task):
if cert_obj_json != "":
cert_obj = ssl_certificate.load_from_json(
json.loads(cert_obj_json))
latest_sps_id = cert_obj.cert_details['Akamai']['extra_info'].get(
'akamai_spsId')
current_status = cert_obj.cert_details['Akamai']['extra_info'].get(
'status')
if cert_obj.cert_type == 'san':
latest_sps_id = cert_obj.\
cert_details['Akamai']['extra_info'].get(
'akamai_spsId')
current_status = cert_obj.\
cert_details['Akamai']['extra_info'].get(
'status')
if latest_sps_id is None:
return current_status
if latest_sps_id is None:
return current_status
resp = self.akamai_driver.akamai_sps_api_client.get(
self.akamai_driver.akamai_sps_api_base_url.format(
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
resp = self.akamai_driver.akamai_sps_api_client.get(
self.akamai_driver.akamai_sps_api_base_url.format(
spsId=latest_sps_id
)
)
# 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 ""
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_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):

View File

@ -43,3 +43,15 @@ class BaseAkamaiSanInfoStorage(object):
@abc.abstractmethod
def list_all_san_cert_names(self):
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')
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):
the_san_cert_info = self._get_akamai_san_certs_info().get(
san_cert_name

View File

@ -98,8 +98,27 @@ class ZookeeperSanInfoStorage(base.BaseAkamaiSanInfoStorage):
spsId, _ = self.zookeeper_client.get(my_sps_id_path)
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,
property_name, value):
property_name_path = self._zk_path(san_cert_name, property_name)
self.zookeeper_client.ensure_path(property_name_path)
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
from oslo_log import log
from six.moves import urllib
from poppy.provider.akamai import utils
from poppy.provider import base
@ -35,6 +36,10 @@ class CertificateController(base.CertificateBase):
def san_cert_cnames(self):
return self.driver.san_cert_cnames
@property
def sni_cert_cnames(self):
return self.driver.sni_cert_cnames
@property
def cert_info_storage(self):
return self.driver.cert_info_storage
@ -47,11 +52,16 @@ class CertificateController(base.CertificateBase):
def sps_api_client(self):
return self.driver.akamai_sps_api_client
@property
def cps_api_client(self):
return self.driver.akamai_cps_api_client
def __init__(self, driver):
super(CertificateController, self).__init__(driver)
self.driver = driver
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):
if cert_obj.cert_type == 'san':
@ -280,6 +290,10 @@ class CertificateController(base.CertificateBase):
'san cert failed for {0} failed.'.format(
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:
return self.responder.ssl_certificate_provisioned(None, {
'status': 'failed',
@ -308,3 +322,289 @@ class CertificateController(base.CertificateBase):
break
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 '
'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(
'akamai_https_custom_config_numbers',
help='A list of Akamai configuration number for '
@ -94,7 +99,7 @@ AKAMAI_OPTIONS = [
help='A list of sni certs cname host names'),
# SANCERT related configs
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,
help='default limit on how many hostnames can'
' be held by a SAN cert'),
@ -122,6 +127,7 @@ VALID_PROPERTY_SPEC = [
"akamai_http_config_number",
"akamai_https_shared_config_number",
"akamai_https_san_config_numbers",
"akamai_https_sni_config_numbers",
"akamai_https_custom_config_numbers"]
@ -153,6 +159,8 @@ class CDNProvider(base.Driver):
self.akamai_conf.akamai_https_shared_config_number)
self.https_san_conf_number = (
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.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([
str(self.akamai_conf.policy_api_base_url),
'papi/v0/{middle_part}/'
@ -198,6 +211,7 @@ class CDNProvider(base.Driver):
self.akamai_sps_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.mod_san_queue = (
zookeeper_queue.ZookeeperModSanQueue(self._conf))

View File

@ -174,7 +174,7 @@ class ServiceController(base.ServiceBase):
(dp, classified_domain.domain))
# pick a san cert for this domain
edge_host_name = None
if classified_domain.certificate == 'san':
if classified_domain.certificate in ['san', 'sni']:
cert_info = getattr(classified_domain, 'cert_info', None)
if cert_info is None:
domains_certificate_status[
@ -182,7 +182,7 @@ class ServiceController(base.ServiceBase):
continue
else:
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] \
= (classified_domain.cert_info.get_cert_status())
if edge_host_name is None:
@ -411,7 +411,7 @@ class ServiceController(base.ServiceBase):
'complete' % (dp, classified_domain.domain))
edge_host_name = None
old_operator_url = None
if classified_domain.certificate == 'san':
if classified_domain.certificate in ['san', 'sni']:
cert_info = getattr(classified_domain, 'cert_info',
None)
if cert_info is None:
@ -422,7 +422,7 @@ class ServiceController(base.ServiceBase):
else:
edge_host_name = (
classified_domain.cert_info.
get_san_edge_name())
get_edge_host_name())
domain_access_url = service_obj.provider_details[
self.driver.provider_name
].get_domain_access_url(classified_domain.domain)
@ -1075,6 +1075,8 @@ class ServiceController(base.ServiceBase):
configuration_number = self.driver.https_shared_conf_number
elif domain_obj.certificate == 'san':
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':
configuration_number = self.driver.https_custom_conf_number
else:
@ -1093,7 +1095,8 @@ class ServiceController(base.ServiceBase):
self.driver.akamai_https_access_url_suffix])
elif domain_obj.certificate == 'san':
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
# have to take it for now
elif edge_host_name.endswith(
@ -1103,6 +1106,10 @@ class ServiceController(base.ServiceBase):
provider_access_url = '.'.join(
[edge_host_name,
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':
provider_access_url = '.'.join(
[dp, self.driver.akamai_https_access_url_suffix])

View File

@ -13,13 +13,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import socket
import ssl
import sys
from kazoo import client
from OpenSSL import crypto
from oslo_log import log
import six
LOG = log.getLogger(__name__)
# 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
# new versions
@ -38,20 +42,20 @@ ssl_versions = [
]
try:
# Warning from python: "documentation SSL version 3 is insecure.
# Warning from python documentation "SSL version 3 is insecure.
# Its use is highly discouraged."
# https://docs.python.org/2/library/ssl.html#ssl.PROTOCOL_SSLv3
ssl_versions.append(ssl.PROTOCOL_SSLv3)
except AttributeError:
pass
except AttributeError: # pragma: no cover
pass # pragma: no cover
ssl_versions.extend(extra_versions)
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:
try:
cert = ssl.get_server_certificate((remote_host, 443),
@ -76,13 +80,15 @@ def get_ssl_number_of_hosts(remote_host):
result = len(sans)
break
else:
raise ValueError('Get remote host certificate info failed...')
raise ValueError(
'Get remote host certificate {0} info failed.'.format(remote_host))
return result
def get_sans_by_host(remote_host):
"""Get Subject Alternative Names for a (SAN) Cert."""
LOG.info("Retrieving sans for {0}".format(remote_host))
for ssl_version in ssl_versions:
try:
cert = ssl.get_server_certificate(
@ -109,10 +115,45 @@ def get_sans_by_host(remote_host):
result = sans
break
else:
raise ValueError('Get remote host certificate info failed...')
raise ValueError(
'Get remote host certificate {0} info failed.'.format(remote_host))
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):
"""Connect to a zookeeper cluster"""
storage_backend_hosts = ','.join(['%s:%s' % (

View File

@ -139,3 +139,17 @@ class Responder(object):
'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]
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': {
'type': 'string',
'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': {
'type': 'string',
@ -57,7 +60,10 @@ class BackgroundJobSchema(schema_base.SchemaBase):
},
'property_spec': {
'type': 'string',
'enum': ['akamai_https_san_config_numbers']
'enum': [
'akamai_https_san_config_numbers'
'akamai_https_sni_config_numbers'
]
},
'san_cert_domain_suffix': {
'type': 'string'

View File

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

View File

@ -37,7 +37,7 @@ class SSLCertificateSchema(schema_base.SchemaBase):
'cert_type': {
'type': 'string',
'required': True,
'enum': ['san'],
'enum': ['san', 'sni'],
},
'domain_name': {
'type': 'string',
@ -151,17 +151,28 @@ class SSLCertificateSchema(schema_base.SchemaBase):
'extra_info': {
'type': 'object',
'required': True,
'additionalProperties': False,
'properties': {
'san cert': {
'type': 'string',
'required': True,
'required': False,
'minLength': 3,
'maxLength': 253
},
'sni_cert': {
'type': 'string',
'required': False,
'minLength': 3,
'maxLength': 253
},
'akamai_spsId': {
'type': 'integer',
'required': True
}
'required': False
},
'change_url': {
'type': 'string',
'required': False,
},
}
}
}
@ -181,8 +192,12 @@ class SSLCertificateSchema(schema_base.SchemaBase):
'cert_type': {
'type': 'string',
'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]
akamai_https_access_url_suffix = "my_https_url_suffix"
akamai_https_san_config_numbers = 200000
akamai_https_sni_config_numbers = 200000
akamai_https_custom_config_numbers = 200000
san_cert_cnames = secure1.test-san.com,secure2.test-san.com
cert_info_storage_type = cassandra

View File

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

View File

@ -186,6 +186,27 @@ class TestFlowRuns(base.TestCase):
dns_controller.create._mock_return_value = []
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
def patch_recreate_ssl_certificate_flow(
service_controller, storage_controller, dns_controller):
@ -1031,11 +1052,18 @@ class TestFlowRuns(base.TestCase):
store=kwargs)
def test_delete_ssl_certificate_normal(self):
providers = ['cdn_provider']
cert_obj_json = ssl_certificate.SSLCertificate(
'cdn',
'mytestsite.com',
'san'
)
kwargs = {
'cert_type': "san",
'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()
}
@ -1052,10 +1080,11 @@ class TestFlowRuns(base.TestCase):
ssl_cert_controller,
memoized_controllers.task_controllers):
self.patch_create_ssl_certificate_flow(
self.patch_delete_ssl_certificate_flow(
service_controller,
storage_controller,
dns_controller
dns_controller,
ssl_cert_controller
)
engines.run(
delete_ssl_certificate.delete_ssl_certificate(),

View File

@ -59,6 +59,19 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
'san_cert_cnames',
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(
'akamai_https_access_url_suffix',
'edge.key.net',
@ -208,7 +221,8 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
@ddt.data(
"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):
del self.provider_mocks['akamai']
@ -219,10 +233,12 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
self.assertEqual(0, len(ignore_list))
@ddt.data(
"akamai_check_and_update_cert_status",
"akamai_update_papi_property_for_mod_san"
("akamai_check_and_update_cert_status", "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
self.mock_storage.certificates_controller.\
get_certs_by_domain.return_value = [
@ -238,7 +254,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
domain.Domain(
"www.example.com",
protocol='https',
certificate='san'
certificate=cert_type
)
],
[],
@ -248,16 +264,17 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
san_mapping_queue = self.manager_driver.providers[
'akamai'].obj.san_mapping_queue
cert_key = ('san cert' if cert_type == 'san' else 'sni_cert')
san_mapping_queue.traverse_queue.return_value = [
json.dumps({
"domain_name": "www.example.com",
"flavor_id": "flavor_id",
"project_id": "project_id",
"cert_type": "san",
"cert_type": cert_type,
"cert_details": {
"Akamai": {
"extra_info": {
"san cert": "san.example.com",
cert_key: "{0}.example.com".format(cert_type),
"akamai_spsId": 1
}
}
@ -278,10 +295,11 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
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.\
get_certs_by_domain.return_value = []
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 = [
@ -289,11 +307,11 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
"domain_name": "www.example.com",
"flavor_id": "flavor_id",
"project_id": "project_id",
"cert_type": "san",
"cert_type": cert_type,
"cert_details": {
"Akamai": {
"extra_info": {
"san cert": "san.example.com",
cert_key: "san.example.com",
"akamai_spsId": 1
}
}
@ -303,7 +321,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
]
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'}
)
self.assertEqual(0, len(run_list))
@ -314,7 +332,8 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
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.\
get_certs_by_domain.return_value = [
mock.Mock()
@ -334,6 +353,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
'flavor_id',
project_id='project_id'
)
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 = [
@ -341,11 +361,11 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
"domain_name": "www.example.com",
"flavor_id": "flavor_id",
"project_id": "project_id",
"cert_type": "san",
"cert_type": cert_type,
"cert_details": {
"Akamai": {
"extra_info": {
"san cert": "san.example.com",
cert_key: "san.example.com",
"akamai_spsId": 1
}
}
@ -355,7 +375,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
]
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'}
)
self.assertEqual(0, len(run_list))
@ -366,8 +386,55 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
self.bgc.distributed_task_controller.submit_task.called
)
@ddt.data("akamai_update_papi_property_for_mod_san")
def test_post_job_invalid_san_cert_cname(self, job_type):
@ddt.data("san", "sni")
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
self.mock_storage.certificates_controller.\
get_certs_by_domain.return_value = [
@ -383,13 +450,14 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
domain.Domain(
"www.example.com",
protocol='https',
certificate='san'
certificate=cert_type
)
],
[],
'flavor_id',
project_id='project_id'
)
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 = [
@ -397,11 +465,11 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
"domain_name": "www.example.com",
"flavor_id": "flavor_id",
"project_id": "project_id",
"cert_type": "san",
"cert_type": cert_type,
"cert_details": {
"Akamai": {
"extra_info": {
"san cert": "not.exist.com",
cert_key: "not.exist.com",
"akamai_spsId": 1
}
}

View File

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

View File

@ -76,7 +76,7 @@ class TestSSLCertificate(base.TestCase):
ssl_cert.cert_details = cert_details
self.assertEqual(ssl_cert.get_cert_status(), 'deployed')
# 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'] = {
'status': 'whatever'
}
@ -101,7 +101,7 @@ class TestSSLCertificate(base.TestCase):
cert_details=cert_details)
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):
project_id = '12345'
@ -117,7 +117,7 @@ class TestSSLCertificate(base.TestCase):
cert_details=cert_details)
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):
project_id = '12345'
@ -138,7 +138,7 @@ class TestSSLCertificate(base.TestCase):
cert_type=cert_type,
cert_details=cert_details)
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):
ssl_cert = ssl_certificate.SSLCertificate.init_from_dict(

View File

@ -29,6 +29,21 @@
],
"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": {
"name" : "mysite.com",
"domains": [

View File

@ -30,12 +30,17 @@ class TestCertificates(base.TestCase):
super(TestCertificates, self).setUp()
self.driver = mock.Mock()
self.driver.provider_name = 'Akamai'
self.san_cert_cnames = [str(x) for x in range(7)]
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 = (
'example.net'
)
self.driver.akamai_conf.policy_api_base_url = 'https://aka-base.com/'
san_by_host_patcher = mock.patch(
'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_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)
@ddt.data(("SPS Request Complete", ""),
@ -402,7 +422,7 @@ class TestCertificates(base.TestCase):
responder = controller.create_certificate(
ssl_certificate.load_from_json(data),
True
enqueue=True
)
self.assertIsNone(responder['Akamai']['cert_domain'])
@ -414,3 +434,623 @@ class TestCertificates(base.TestCase):
'Domain www.abc.com already exists on san cert 0.',
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 '
'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(
'akamai_https_custom_config_numbers',
default=[str(random.randint(10000, 99999))],
@ -80,11 +86,11 @@ AKAMAI_OPTIONS = [
'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'),
# SANCERT related configs
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,
help='default limit on how many hostnames can'
' be held by a SAN cert'),

View File

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