diff --git a/etc/poppy.conf b/etc/poppy.conf index 334671df..0322df04 100644 --- a/etc/poppy.conf +++ b/etc/poppy.conf @@ -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" diff --git a/poppy/distributed_task/taskflow/flow/delete_service.py b/poppy/distributed_task/taskflow/flow/delete_service.py index a3144534..63346662 100644 --- a/poppy/distributed_task/taskflow/flow/delete_service.py +++ b/poppy/distributed_task/taskflow/flow/delete_service.py @@ -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( diff --git a/poppy/distributed_task/taskflow/task/common.py b/poppy/distributed_task/taskflow/task/common.py index 7dab3ccf..70645508 100644 --- a/poppy/distributed_task/taskflow/task/common.py +++ b/poppy/distributed_task/taskflow/task/common.py @@ -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): diff --git a/poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py b/poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py index 5da51f87..f2f52219 100644 --- a/poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py +++ b/poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py @@ -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)) diff --git a/poppy/distributed_task/taskflow/task/delete_service_tasks.py b/poppy/distributed_task/taskflow/task/delete_service_tasks.py index d93b23e1..ac49aee7 100644 --- a/poppy/distributed_task/taskflow/task/delete_service_tasks.py +++ b/poppy/distributed_task/taskflow/task/delete_service_tasks.py @@ -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( diff --git a/poppy/distributed_task/taskflow/task/delete_ssl_certificate_tasks.py b/poppy/distributed_task/taskflow/task/delete_ssl_certificate_tasks.py index f27efcfd..c9adf941 100644 --- a/poppy/distributed_task/taskflow/task/delete_ssl_certificate_tasks.py +++ b/poppy/distributed_task/taskflow/task/delete_ssl_certificate_tasks.py @@ -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 diff --git a/poppy/manager/base/providers.py b/poppy/manager/base/providers.py index 28e76356..e831c7bc 100644 --- a/poppy/manager/base/providers.py +++ b/poppy/manager/base/providers.py @@ -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) diff --git a/poppy/manager/default/background_job.py b/poppy/manager/default/background_job.py index e0e16189..df7e8c1b 100644 --- a/poppy/manager/default/background_job.py +++ b/poppy/manager/default/background_job.py @@ -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. diff --git a/poppy/manager/default/ssl_certificate.py b/poppy/manager/default/ssl_certificate.py index 0abc6372..ed187373 100644 --- a/poppy/manager/default/ssl_certificate.py +++ b/poppy/manager/default/ssl_certificate.py @@ -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, diff --git a/poppy/model/helpers/domain.py b/poppy/model/helpers/domain.py index cf7dcd98..1dd699d7 100644 --- a/poppy/model/helpers/domain.py +++ b/poppy/model/helpers/domain.py @@ -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) diff --git a/poppy/model/ssl_certificate.py b/poppy/model/ssl_certificate.py index 5ead6176..d75da6ef 100644 --- a/poppy/model/ssl_certificate.py +++ b/poppy/model/ssl_certificate.py @@ -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 diff --git a/poppy/notification/base/services.py b/poppy/notification/base/services.py index 9f55d4c3..e5e9c249 100644 --- a/poppy/notification/base/services.py +++ b/poppy/notification/base/services.py @@ -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 diff --git a/poppy/notification/mailgun/services.py b/poppy/notification/mailgun/services.py index ab990394..72931947 100644 --- a/poppy/notification/mailgun/services.py +++ b/poppy/notification/mailgun/services.py @@ -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: diff --git a/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_tasks.py b/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_tasks.py index 77d9a4a7..6b97e85d 100644 --- a/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_tasks.py +++ b/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_tasks.py @@ -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): diff --git a/poppy/provider/akamai/cert_info_storage/base.py b/poppy/provider/akamai/cert_info_storage/base.py index 165f6208..f13fabd4 100644 --- a/poppy/provider/akamai/cert_info_storage/base.py +++ b/poppy/provider/akamai/cert_info_storage/base.py @@ -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 diff --git a/poppy/provider/akamai/cert_info_storage/cassandra_storage.py b/poppy/provider/akamai/cert_info_storage/cassandra_storage.py index 283343c6..9abe951e 100644 --- a/poppy/provider/akamai/cert_info_storage/cassandra_storage.py +++ b/poppy/provider/akamai/cert_info_storage/cassandra_storage.py @@ -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 diff --git a/poppy/provider/akamai/cert_info_storage/zookeeper_storage.py b/poppy/provider/akamai/cert_info_storage/zookeeper_storage.py index 97f0b751..b1ace4ee 100644 --- a/poppy/provider/akamai/cert_info_storage/zookeeper_storage.py +++ b/poppy/provider/akamai/cert_info_storage/zookeeper_storage.py @@ -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 diff --git a/poppy/provider/akamai/certificates.py b/poppy/provider/akamai/certificates.py index ef5ed8a5..d6d70bf8 100644 --- a/poppy/provider/akamai/certificates.py +++ b/poppy/provider/akamai/certificates.py @@ -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 + ) + }) diff --git a/poppy/provider/akamai/driver.py b/poppy/provider/akamai/driver.py index ea221a7e..e1e98ac9 100644 --- a/poppy/provider/akamai/driver.py +++ b/poppy/provider/akamai/driver.py @@ -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)) diff --git a/poppy/provider/akamai/services.py b/poppy/provider/akamai/services.py index 26c0f3fa..14b485ec 100644 --- a/poppy/provider/akamai/services.py +++ b/poppy/provider/akamai/services.py @@ -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]) diff --git a/poppy/provider/akamai/utils.py b/poppy/provider/akamai/utils.py index 4b17fbdc..c376777e 100644 --- a/poppy/provider/akamai/utils.py +++ b/poppy/provider/akamai/utils.py @@ -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' % ( diff --git a/poppy/provider/base/responder.py b/poppy/provider/base/responder.py index 6d3cd148..860491ee 100644 --- a/poppy/provider/base/responder.py +++ b/poppy/provider/base/responder.py @@ -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 + } + } diff --git a/poppy/storage/mockdb/certificates.py b/poppy/storage/mockdb/certificates.py index 0f5ffde5..cd2eb811 100644 --- a/poppy/storage/mockdb/certificates.py +++ b/poppy/storage/mockdb/certificates.py @@ -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 diff --git a/poppy/transport/validators/schemas/background_jobs.py b/poppy/transport/validators/schemas/background_jobs.py index 9b446dc8..426ae7b0 100644 --- a/poppy/transport/validators/schemas/background_jobs.py +++ b/poppy/transport/validators/schemas/background_jobs.py @@ -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' diff --git a/poppy/transport/validators/schemas/service.py b/poppy/transport/validators/schemas/service.py index f07021f7..237cb904 100644 --- a/poppy/transport/validators/schemas/service.py +++ b/poppy/transport/validators/schemas/service.py @@ -112,6 +112,7 @@ class ServiceSchema(schema_base.SchemaBase): 'type': 'string', 'enum': [ 'san', + 'sni', 'custom'] }, }, diff --git a/poppy/transport/validators/schemas/ssl_certificate.py b/poppy/transport/validators/schemas/ssl_certificate.py index a7bf683c..0fcb430c 100644 --- a/poppy/transport/validators/schemas/ssl_certificate.py +++ b/poppy/transport/validators/schemas/ssl_certificate.py @@ -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, + }, } } } diff --git a/tests/etc/default_functional.conf b/tests/etc/default_functional.conf index 696bd92e..cd4459e2 100644 --- a/tests/etc/default_functional.conf +++ b/tests/etc/default_functional.conf @@ -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 diff --git a/tests/functional/transport/pecan/controllers/test_ssl_certificate.py b/tests/functional/transport/pecan/controllers/test_ssl_certificate.py index 1b5e5a86..edef8124 100644 --- a/tests/functional/transport/pecan/controllers/test_ssl_certificate.py +++ b/tests/functional/transport/pecan/controllers/test_ssl_certificate.py @@ -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): diff --git a/tests/unit/distributed_task/taskflow/test_flows.py b/tests/unit/distributed_task/taskflow/test_flows.py index 48cff374..42bb37cd 100644 --- a/tests/unit/distributed_task/taskflow/test_flows.py +++ b/tests/unit/distributed_task/taskflow/test_flows.py @@ -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(), diff --git a/tests/unit/manager/default/test_background_job.py b/tests/unit/manager/default/test_background_job.py index 53b491bc..76d64d62 100644 --- a/tests/unit/manager/default/test_background_job.py +++ b/tests/unit/manager/default/test_background_job.py @@ -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 } } diff --git a/tests/unit/manager/default/test_ssl_certificate.py b/tests/unit/manager/default/test_ssl_certificate.py index aed0451f..847191a6 100644 --- a/tests/unit/manager/default/test_ssl_certificate.py +++ b/tests/unit/manager/default/test_ssl_certificate.py @@ -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') diff --git a/tests/unit/model/helpers/test_ssl_certificate.py b/tests/unit/model/helpers/test_ssl_certificate.py index dddb272e..63f53ace 100644 --- a/tests/unit/model/helpers/test_ssl_certificate.py +++ b/tests/unit/model/helpers/test_ssl_certificate.py @@ -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( diff --git a/tests/unit/provider/akamai/data_service.json b/tests/unit/provider/akamai/data_service.json index 547cc296..ac82201e 100644 --- a/tests/unit/provider/akamai/data_service.json +++ b/tests/unit/provider/akamai/data_service.json @@ -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": [ diff --git a/tests/unit/provider/akamai/test_certificates.py b/tests/unit/provider/akamai/test_certificates.py index ecb81288..4df3c492 100644 --- a/tests/unit/provider/akamai/test_certificates.py +++ b/tests/unit/provider/akamai/test_certificates.py @@ -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'] + ) diff --git a/tests/unit/provider/akamai/test_driver.py b/tests/unit/provider/akamai/test_driver.py index 490a52e1..af97037f 100644 --- a/tests/unit/provider/akamai/test_driver.py +++ b/tests/unit/provider/akamai/test_driver.py @@ -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'), diff --git a/tests/unit/provider/akamai/test_services.py b/tests/unit/provider/akamai/test_services.py index 75390016..b3edf0c4 100644 --- a/tests/unit/provider/akamai/test_services.py +++ b/tests/unit/provider/akamai/test_services.py @@ -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()) } ) diff --git a/tests/unit/provider/akamai/test_utils.py b/tests/unit/provider/akamai/test_utils.py new file mode 100644 index 00000000..96044b98 --- /dev/null +++ b/tests/unit/provider/akamai/test_utils.py @@ -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')