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 1c31b98b..5da51f87 100644 --- a/poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py +++ b/poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py @@ -31,7 +31,8 @@ conf(project='poppy', prog='poppy', args=[]) class CreateProviderSSLCertificateTask(task.Task): default_provides = "responders" - def execute(self, providers_list_json, cert_obj_json, enqueue=True): + def execute(self, providers_list_json, cert_obj_json, enqueue=True, + https_upgrade=False): service_controller = memoized_controllers.task_controllers('poppy') # call provider create_ssl_certificate function @@ -46,7 +47,8 @@ class CreateProviderSSLCertificateTask(task.Task): responder = service_controller.provider_wrapper.create_certificate( service_controller._driver.providers[provider], cert_obj, - enqueue + enqueue, + https_upgrade ) responders.append(responder) @@ -55,7 +57,7 @@ class CreateProviderSSLCertificateTask(task.Task): class SendNotificationTask(task.Task): - def execute(self, project_id, responders): + def execute(self, project_id, responders, upgrade=False): service_controller = memoized_controllers.task_controllers('poppy') notification_content = "" @@ -65,6 +67,13 @@ class SendNotificationTask(task.Task): "Project ID: %s, Provider: %s, Detail: %s" % (project_id, provider, str(responder[provider]))) + if upgrade is True: + notification_content += ( + " The domain was upgraded from HTTP to HTTPS SAN. " + "If applicable, take note of the domain name and " + "delete the old HTTP policy in the provider." + ) + for n_driver in service_controller._driver.notification: service_controller.notification_wrapper.send( n_driver, diff --git a/poppy/dns/rackspace/services.py b/poppy/dns/rackspace/services.py index 7748fdd6..a12923f3 100644 --- a/poppy/dns/rackspace/services.py +++ b/poppy/dns/rackspace/services.py @@ -14,6 +14,7 @@ # limitations under the License. import random +import re try: set except NameError: # noqa pragma: no cover @@ -64,6 +65,7 @@ class ServicesController(base.ServicesBase): # randomly select a shard shard_id = random.randint(1, num_shards) + # ex. cdnXXX.altcdn.com subdomain_name = '{0}{1}.{2}'.format(shard_prefix, shard_id, cdn_domain_name) subdomain = self._get_subdomain(subdomain_name) @@ -75,14 +77,51 @@ class ServicesController(base.ServicesBase): shared_ssl_subdomain_name = None for link in links: # pick out shared ssl domains here - domain_name, certificate = link + domain_name, certificate, old_operator_url = link if certificate == "shared": shared_ssl_subdomain_name = ( '.'.join(domain_name.split('.')[1:])) # perform shared ssl cert logic name = domain_name else: - name = '{0}.{1}'.format(domain_name, subdomain_name) + if old_operator_url is not None: + # verify sub-domain exists + regex_match = re.match( + r'^.*(' + shard_prefix + '[0-9]+\.' + + re.escape(cdn_domain_name) + ')$', + old_operator_url + ) + my_sub_domain_name = regex_match.groups(-1)[0] + if my_sub_domain_name is None: + raise ValueError('Unable to parse old provider url') + + # add to cname record + my_sub_domain = self._get_subdomain(my_sub_domain_name) + LOG.info( + "Updating DNS Record for HTTPS upgrade " + "domain {0}. CNAME update from {1} to {2}".format( + my_sub_domain_name, + old_operator_url, + links[link] + ) + ) + + old_dns_record = my_sub_domain.find_record( + 'CNAME', + old_operator_url + ) + my_sub_domain.update_record( + old_dns_record, + data=links[link] + ) + + dns_links[link] = { + 'provider_url': links[link], + 'operator_url': old_operator_url + } + continue + else: + name = '{0}.{1}'.format(domain_name, subdomain_name) cname_record = {'type': 'CNAME', 'name': name, @@ -245,8 +284,11 @@ class ServicesController(base.ServicesBase): # We need to distinguish shared ssl domains in # which case the we will use different shard prefix and # and shard number - links[(link['domain'], link.get('certificate', - None))] = link['href'] + links[( + link['domain'], + link.get('certificate', None), + None # new link no pref operator url + )] = link['href'] # create CNAME records try: @@ -269,14 +311,16 @@ class ServicesController(base.ServicesBase): if link['rel'] == 'access_url': access_url = { 'domain': link['domain'], - 'provider_url': - dns_links[(link['domain'], - link.get('certificate', None) - )]['provider_url'], - 'operator_url': - dns_links[(link['domain'], - link.get('certificate', None) - )]['operator_url']} + 'provider_url': dns_links[( + link['domain'], + link.get('certificate', None), + None + )]['provider_url'], + 'operator_url': dns_links[( + link['domain'], + link.get('certificate', None), + None + )]['operator_url']} # Need to indicate if this access_url is a shared ssl # access url, since its has different shard_prefix and # num_shard @@ -310,7 +354,7 @@ class ServicesController(base.ServicesBase): access_url['operator_url'], access_url.get('shared_ssl_flag', False)) if msg: - error_msg = error_msg + msg + error_msg += msg except exc.NotFound as e: LOG.error('Can not access the subdomain. Please make ' 'sure it exists and you have permissions ' @@ -320,8 +364,8 @@ class ServicesController(base.ServicesBase): error_class = e.__class__ except Exception as e: LOG.error('Rackspace DNS Exception: {0}'.format(e)) - error_msg = error_msg + 'Rackspace DNS ' \ - 'Exception: {0}'.format(e) + error_msg += 'Rackspace DNS ' \ + 'Exception: {0}'.format(e) error_class = e.__class__ # format the error message for this provider if not error_msg: @@ -361,8 +405,11 @@ class ServicesController(base.ServicesBase): domain_added = (link['rel'] == 'access_url' and link['domain'] in added_domains) if domain_added: - links[(link['domain'], link.get('certificate', - None))] = link['href'] + links[( + link['domain'], + link.get('certificate', None), + link.get('old_operator_url', None) + )] = link['href'] # create CNAME records for added domains try: @@ -387,11 +434,13 @@ class ServicesController(base.ServicesBase): 'domain': link['domain'], 'provider_url': dns_links[(link['domain'], - link.get('certificate', None) + link.get('certificate', None), + link.get('old_operator_url', None) )]['provider_url'], 'operator_url': dns_links[(link['domain'], - link.get('certificate', None) + link.get('certificate', None), + link.get('old_operator_url', None) )]['operator_url']} # Need to indicate if this access_url is a shared ssl # access url, since its has different shard_prefix and @@ -497,11 +546,43 @@ class ServicesController(base.ServicesBase): for link in links: new_domains.add(link['domain']) + # find http -> https+san upgrade domains + upgraded_domains = set() + for domain in service_updates.domains: + for old_domain in service_old.domains: + if old_domain.domain == domain.domain: + if ( + old_domain.protocol == 'http' and + domain.protocol == 'https' and + domain.certificate == 'san' + ): + upgraded_domains.add(domain.domain) + break + # if domains have not been updated, return if not service_updates.domains: return old_access_urls_map - # if the old set of domains is the same as new set of domains, return + # force dns update when we encounter an upgraded domain + common_domains = new_domains.intersection(old_domains) + for domain_name in common_domains: + upgrade = False + for responder in responders: + for provider_name in responder: + links = responder[provider_name]['links'] + for link in links: + if ( + link['domain'] == domain_name and + link.get('certificate', None) == 'san' and + link['href'] is not None and + link['old_operator_url'] is not None + ): + upgrade = True + + if upgrade is True: + old_domains.remove(domain_name) + + # if the old set of domains is the same as new set of domains return if old_domains == new_domains: return old_access_urls_map @@ -510,6 +591,10 @@ class ServicesController(base.ServicesBase): removed_domains = old_domains.difference(new_domains) common_domains = new_domains.intersection(old_domains) + # prevent dns records for upgrade domains from being deleted + retain_domains = removed_domains.intersection(upgraded_domains) + removed_domains = removed_domains.difference(retain_domains) + LOG.info("Added Domains : {0} on service_id : {1} " "for project_id: {2}".format(added_domains, service_id, @@ -589,6 +674,37 @@ class ServicesController(base.ServicesBase): if old_access_url.get('shared_ssl_flag', False): access_url['shared_ssl_flag'] = True access_urls.append(access_url) + + # find upgraded domains and create placeholders for them + for domain in service_updates.domains: + is_upgrade = False + for old_domain in service_old.domains: + if old_domain.domain == domain.domain: + if ( + old_domain.protocol == 'http' and + domain.protocol == 'https' and + domain.certificate == 'san' + ): + is_upgrade = True + break + if is_upgrade is True: + old_access_url_for_domain = ( + service_old.provider_details.values()[0]. + get_domain_access_url(domain.domain)) + + # add placeholder access url for upgraded domain + # the access_url dict here should be missing an entry + # for http san domain since provider url is + # determined only after an ssl cert is provisioned + access_urls.append({ + 'domain': domain.domain, + 'provider_url': None, + 'operator_url': None, + 'old_operator_url': old_access_url_for_domain[ + 'operator_url' + ] + }) + dns_details[provider_name] = {'access_urls': access_urls} return self.responder.updated(dns_details) diff --git a/poppy/manager/base/providers.py b/poppy/manager/base/providers.py index 02aa77e8..28e76356 100644 --- a/poppy/manager/base/providers.py +++ b/poppy/manager/base/providers.py @@ -73,7 +73,7 @@ class ProviderWrapper(object): hard, purge_url) - def create_certificate(self, ext, cert_obj, enqueue): + def create_certificate(self, ext, cert_obj, enqueue, https_upgrade): """Create a certificate :param ext @@ -84,5 +84,6 @@ class ProviderWrapper(object): return ext.obj.certificate_controller.create_certificate( cert_obj, - enqueue + enqueue, + https_upgrade ) diff --git a/poppy/manager/default/services.py b/poppy/manager/default/services.py index fa980dbd..c38f8940 100644 --- a/poppy/manager/default/services.py +++ b/poppy/manager/default/services.py @@ -53,6 +53,7 @@ class DefaultServicesController(base.ServicesController): self.ssl_certificate_storage = ( self._driver.storage.certificates_controller ) + self.ssl_cert_manager = self._driver.ssl_certificate_controller self.flavor_controller = self._driver.storage.flavors_controller self.dns_controller = self._driver.dns.services_controller self.distributed_task_controller = ( @@ -371,7 +372,7 @@ class DefaultServicesController(base.ServicesController): # only one provider per flavor, that's # why we use values()[0] access_url_for_domain = ( - service_new.provider_details.values()[0]. + list(service_new.provider_details.values())[0]. get_domain_access_url(domain.domain)) if access_url_for_domain is not None: providers = ( @@ -380,21 +381,26 @@ class DefaultServicesController(base.ServicesController): ) san_cert_url = access_url_for_domain.get( 'provider_url') - # Note(tonytan4ever): stored san_cert_url - # for two times, that's intentional - # a little extra info does not hurt - new_cert_detail = { - providers[0].provider_id.title(): - json.dumps(dict( - cert_domain=san_cert_url, - extra_info={ - 'status': 'deployed', - 'san cert': san_cert_url, - 'created_at': str( - datetime.datetime.now()) - } - )) - } + https_upgrade = self._detect_upgrade_http_to_https( + service_old.domains, domain) + if https_upgrade is True: + new_cert_detail = None + else: + # Note(tonytan4ever): stored san_cert_url + # for two times, that's intentional + # a little extra info does not hurt + new_cert_detail = { + providers[0].provider_id.title(): + json.dumps(dict( + cert_domain=san_cert_url, + extra_info={ + 'status': 'deployed', + 'san cert': san_cert_url, + 'created_at': str( + datetime.datetime.now()) + } + )) + } new_cert_obj = ssl_certificate.SSLCertificate( service_new.flavor_id, domain.domain, @@ -402,16 +408,32 @@ class DefaultServicesController(base.ServicesController): project_id, new_cert_detail ) - self.ssl_certificate_storage.create_certificate( - project_id, - new_cert_obj - ) + if https_upgrade is True: + # request a new ssl cert the same way + # ssl_cert creation is done using taskflow + LOG.debug('Sending request to create ssl cert') + self.ssl_cert_manager.create_ssl_certificate( + project_id, + new_cert_obj, + https_upgrade=True + ) + else: + self.ssl_certificate_storage.\ + create_certificate( + project_id, + new_cert_obj + ) # deserialize cert_details dict - new_cert_obj.cert_details[ - providers[0].provider_id.title()] = json.loads( + try: new_cert_obj.cert_details[ - providers[0].provider_id.title()] - ) + providers[0].provider_id.title() + ] = json.loads( + new_cert_obj.cert_details[ + providers[0].provider_id.title()] + ) + except Exception: + new_cert_obj.cert_details[ + providers[0].provider_id.title()] = {} domain.cert_info = new_cert_obj if hasattr(self, store): @@ -773,3 +795,15 @@ class DefaultServicesController(base.ServicesController): service_id, provider_details ) + + def _detect_upgrade_http_to_https(self, old_domains, new_domain): + is_upgrade = False + for old_domain in old_domains: + if old_domain.domain == new_domain.domain: + if ( + old_domain.protocol == 'http' and + new_domain.protocol == 'https' + ): + is_upgrade = True + break + return is_upgrade diff --git a/poppy/manager/default/ssl_certificate.py b/poppy/manager/default/ssl_certificate.py index d728373b..f2137aac 100644 --- a/poppy/manager/default/ssl_certificate.py +++ b/poppy/manager/default/ssl_certificate.py @@ -43,7 +43,8 @@ class DefaultSSLCertificateController(base.SSLCertificateController): self.service_storage = self._driver.storage.services_controller self.flavor_controller = self._driver.storage.flavors_controller - def create_ssl_certificate(self, project_id, cert_obj): + def create_ssl_certificate( + self, project_id, cert_obj, https_upgrade=False): if (not validators.is_valid_domain_name(cert_obj.domain_name)) or \ (validators.is_root_domain( @@ -75,6 +76,9 @@ class DefaultSSLCertificateController(base.SSLCertificateController): 'cert_obj_json': json.dumps(cert_obj.to_dict()), 'context_dict': context_utils.get_current().to_dict() } + if https_upgrade is True: + kwargs['https_upgrade'] = True + self.distributed_task_controller.submit_task( create_ssl_certificate.create_ssl_certificate, **kwargs) diff --git a/poppy/model/helpers/provider_details.py b/poppy/model/helpers/provider_details.py index 29d09a58..a24bc9ea 100644 --- a/poppy/model/helpers/provider_details.py +++ b/poppy/model/helpers/provider_details.py @@ -152,10 +152,13 @@ class ProviderDetail(common.DictSerializableModel): self._error_class = value def get_domain_access_url(self, domain): - '''Find an access url of a domain. + """Return an access url object for a domain. - :param domain - ''' + :param domain: domain to use as search key + :type domain: poppy.model.helpers.domain.Domain + + :returns: access_url -- dict containing matching domain + """ for access_url in self.access_urls: if access_url.get('domain') == domain: return access_url diff --git a/poppy/provider/akamai/certificates.py b/poppy/provider/akamai/certificates.py index 97f99757..ef5ed8a5 100644 --- a/poppy/provider/akamai/certificates.py +++ b/poppy/provider/akamai/certificates.py @@ -53,7 +53,7 @@ class CertificateController(base.CertificateBase): self.driver = driver self.sps_api_base_url = self.driver.akamai_sps_api_base_url - def create_certificate(self, cert_obj, enqueue=True): + def create_certificate(self, cert_obj, enqueue=True, https_upgrade=False): if cert_obj.cert_type == 'san': try: found, found_cert = ( @@ -77,7 +77,7 @@ class CertificateController(base.CertificateBase): if enqueue: self.mod_san_queue.enqueue_mod_san_request( json.dumps(cert_obj.to_dict())) - return self.responder.ssl_certificate_provisioned(None, { + extras = { 'status': 'create_in_progress', 'san cert': None, # Add logging so it is easier for testing @@ -86,7 +86,18 @@ class CertificateController(base.CertificateBase): 'San 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 SAN." + "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 + ) san_cert_hostname_limit = ( self.cert_info_storage.get_san_cert_hostname_limit() diff --git a/poppy/provider/akamai/services.py b/poppy/provider/akamai/services.py index a8929add..e22513ac 100644 --- a/poppy/provider/akamai/services.py +++ b/poppy/provider/akamai/services.py @@ -397,6 +397,7 @@ class ServiceController(base.ServiceBase): LOG.info('Creating/Updating policy %s on domain %s ' 'complete' % (dp, classified_domain.domain)) edge_host_name = None + old_operator_url = None if classified_domain.certificate == 'san': cert_info = getattr(classified_domain, 'cert_info', None) @@ -409,6 +410,13 @@ class ServiceController(base.ServiceBase): edge_host_name = ( classified_domain.cert_info. get_san_edge_name()) + domain_access_url = service_obj.provider_details[ + self.driver.provider_name + ].get_domain_access_url(classified_domain.domain) + old_operator_url = ( + None if domain_access_url is None else + domain_access_url.get('old_operator_url', None) + ) domains_certificate_status[ classified_domain.domain] = ( classified_domain.cert_info.get_cert_status()) @@ -416,19 +424,41 @@ class ServiceController(base.ServiceBase): continue provider_access_url = self._get_provider_access_url( classified_domain, dp, edge_host_name) - links.append({'href': provider_access_url, - 'rel': 'access_url', - 'domain': dp, - 'certificate': - classified_domain.certificate - }) + links.append({ + 'href': provider_access_url, + 'rel': 'access_url', + 'domain': dp, + 'certificate': classified_domain.certificate, + 'old_operator_url': old_operator_url + }) except Exception: LOG.exception("Failed to Update Service - {0}". format(provider_service_id)) return self.responder.failed("failed to update service") + # check to see if a domain was upgraded from http -> https+san + # and keep the policy if it was an upgrade try: for policy in policies: + + is_upgrade = False + for link_id in ids: + if ( + link_id['policy_name'] == policy['policy_name'] and + link_id['protocol'] == 'https' and + policy['protocol'] == 'http' + ): + is_upgrade = True + + # skip policy delete if a http -> https+san + # upgrade is detected + if is_upgrade is True: + LOG.info( + "{0} was upgraded from http to https san. " + "Skipping old policy delete.".format( + policy['policy_name'])) + continue + configuration_number = self._get_configuration_number( util.dict2obj(policy)) diff --git a/poppy/storage/cassandra/certificates.py b/poppy/storage/cassandra/certificates.py index b6b895d2..f51d9405 100644 --- a/poppy/storage/cassandra/certificates.py +++ b/poppy/storage/cassandra/certificates.py @@ -116,16 +116,20 @@ class CertificatesController(base.CertificatesController): consistency_level=self._driver.consistency_level) self.session.execute(stmt, args) + cert_status = None try: provider_status = json.loads( list(cert_obj.cert_details.values())[0] ) cert_status = provider_status['extra_info']['status'] - except (IndexError, IndexError, ValueError) as e: - LOG.error("Certificate details in inconsistent " - "state: {0}".format(cert_obj.cert_details)) - LOG.error(e) - else: + except (IndexError, KeyError, ValueError) as e: + LOG.warning( + "Create certificate missing extra info " + "status {0}: Error {1}. " + "Using 'create_in_progress' instead. ".format( + cert_obj.cert_details, e)) + cert_status = 'create_in_progress' + finally: # insert/update for cassandra self.insert_cert_status(cert_obj.domain_name, cert_status) @@ -178,13 +182,14 @@ class CertificatesController(base.CertificatesController): try: provider_status = json.loads(list(cert_details.values())[0]) cert_status = provider_status['extra_info']['status'] - except (IndexError, IndexError, ValueError) as e: - LOG.error("Certificate details in inconsistent " - "state: {0}".format(cert_details)) - LOG.error(e) - else: - # insert/update for cassandra self.insert_cert_status(domain_name, cert_status) + except (IndexError, KeyError, ValueError) as e: + # certs already existing in DB should have all + # the necessary fields + LOG.error( + "Unable to update cert_status because certificate " + "details are in an inconsistent " + "state: {0}: {1}".format(cert_details, e)) def insert_cert_status(self, domain_name, cert_status): cert_args = { diff --git a/poppy/transport/pecan/models/response/service.py b/poppy/transport/pecan/models/response/service.py index 9d31c7c4..7e1ac51b 100644 --- a/poppy/transport/pecan/models/response/service.py +++ b/poppy/transport/pecan/models/response/service.py @@ -95,7 +95,10 @@ class Model(collections.OrderedDict): # add the access urls access_urls = provider_detail.access_urls for access_url in access_urls: - if 'operator_url' in access_url: + if ( + 'operator_url' in access_url and + access_url['operator_url'] is not None + ): self['links'].append(link.Model( access_url['operator_url'], 'access_url')) diff --git a/poppy/transport/validators/helpers.py b/poppy/transport/validators/helpers.py index e5c7e36d..77cfba97 100644 --- a/poppy/transport/validators/helpers.py +++ b/poppy/transport/validators/helpers.py @@ -170,7 +170,7 @@ def is_valid_ip_address(ip_address): # "{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1" # "}[0-9]))$" - # Note(tonytan4ever): make it more clear because re.mtach will return + # Note(tonytan4ever): make it more clear because re.match will return # a match object is there is a match, None if there is no match. return re.match(ipv4_regex, ip_address) is not None diff --git a/tests/functional/transport/pecan/controllers/test_set_service_status.py b/tests/functional/transport/pecan/controllers/test_set_service_status.py index 5e2ec220..c509e70e 100644 --- a/tests/functional/transport/pecan/controllers/test_set_service_status.py +++ b/tests/functional/transport/pecan/controllers/test_set_service_status.py @@ -155,7 +155,7 @@ class TestServicesState(base.FunctionalTest): @given(strategies.text(min_size=257)) def test_services_state_invalid_project_id(self, project_id): # NOTE(TheSriram): the min size is assigned to 257, since - # project_id regex allows upto 256 chars + # project_id regex allows up to 256 chars # invalid project_id field self.req_body['project_id'] = project_id self.req_body['status'] = 'deployed' diff --git a/tests/unit/dns/rackspace/test_services.py b/tests/unit/dns/rackspace/test_services.py index 10e644c0..00b82ca0 100644 --- a/tests/unit/dns/rackspace/test_services.py +++ b/tests/unit/dns/rackspace/test_services.py @@ -878,7 +878,9 @@ class TestServicesUpdate(base.TestCase): { 'domain': u'pictures.domain.com', 'href': u'pictures.domain.com.global.prod.fastly.net', - 'rel': 'access_url' + 'rel': 'access_url', + 'certificate': 'san', + 'old_operator_url': 'old.operator.url.cdn99.mycdn.com' } ]} }] @@ -900,6 +902,63 @@ class TestServicesUpdate(base.TestCase): self.assertIsNotNone( access_urls_map[provider_name][domain_new.domain]) + def test_update_add_domains_http_to_https_upgrade(self): + subdomain = mock.Mock() + subdomain.add_records = mock.Mock() + self.client.find = mock.Mock(return_value=subdomain) + + domains_new = [ + domain.Domain('test.domain.com'), + domain.Domain('blog.domain.com') + ] + + self.service_old.domains = domains_new + service_new = service.Service( + service_id=self.service_old.service_id, + name='myservice', + domains=domains_new, + origins=[], + flavor_id='standard') + + responders = [{ + 'Fastly': { + 'id': str(uuid.uuid4()), + 'links': [ + { + 'domain': u'test.domain.com', + 'href': u'test.domain.com.global.prod.fastly.net', + 'rel': 'access_url' + }, + { + 'domain': u'blog.domain.com', + 'href': u'blog.domain.com.global.prod.fastly.net', + 'rel': 'access_url', + 'certificate': 'san', + 'old_operator_url': 'old.operator.url.cdn99.mycdn.com' + } + ]} + }] + + dns_details = self.controller.update( + self.service_old, + service_new, + responders + ) + + access_urls_map = {} + for provider_name in dns_details: + access_urls_map[provider_name] = {} + access_urls_list = dns_details[provider_name]['access_urls'] + for access_urls in access_urls_list: + access_urls_map[provider_name][access_urls['domain']] = ( + access_urls['operator_url']) + + for responder in responders: + for provider_name in responder: + for domain_new in domains_new: + self.assertIsNotNone( + access_urls_map[provider_name][domain_new.domain]) + def test_gather_cname_links_positive(self): cname_links = self.controller.gather_cname_links(self.service_old) # TODO(isaacm): Add assertions on the returned object diff --git a/tests/unit/manager/default/test_services.py b/tests/unit/manager/default/test_services.py index 83ca31c2..edb398a3 100644 --- a/tests/unit/manager/default/test_services.py +++ b/tests/unit/manager/default/test_services.py @@ -669,8 +669,7 @@ class DefaultManagerServiceTests(base.TestCase): return_value=Response(False)): self.mock_update_service(provider_details_json) - @ddt.file_data('service_update.json') - def test_update(self, update_json): + def test_update(self): provider_details_dict = { "MaxCDN": {"id": 11942, "access_urls": ["mypullzone.netdata.com"]}, "Mock": {"id": 73242, "access_urls": ["mycdn.mock.com"]}, @@ -713,6 +712,177 @@ class DefaultManagerServiceTests(base.TestCase): # ensure the manager calls the storage driver with the appropriate data self.sc.storage_controller.update_service.assert_called_once() + def test_upgrade_http_to_https_san(self): + provider_details_dict = { + "MaxCDN": { + "id": 11942, + "access_urls": [{ + "domain": "www.mywebsite.com", + "access_url": "mypullzone.netdata.com", + "provider_url": 'maxcdn.provider.com' + }] + }, + "Mock": { + "id": 73242, + "access_urls": [{ + "domain": "www.mywebsite.com", + "access_url": "mycdn.mock.com", + "provider_url": 'mock.provider.com' + }] + }, + "CloudFront": { + "id": "5ABC892", + "access_urls": [{ + "access_url": "cf123.cloudcf.com", + "domain": "www.mywebsite.com", + "provider_url": 'cf.provider.com' + }] + }, + "Fastly": { + "id": 3488, + "access_urls": [{ + "access_url": "mockcf123.fastly.prod.com", + "domain": "www.mywebsite.com", + "provider_url": 'fastly.provider.com' + }] + } + } + + providers_details_dict = {} + for name in provider_details_dict: + details = provider_details_dict[name] + provider_detail_obj = provider_details.ProviderDetail( + provider_service_id=details['id'], + access_urls=details['access_urls'], + status=details.get('status', u'unknown')) + providers_details_dict[name] = provider_detail_obj + + self.sc.storage_controller.get_provider_details.return_value = ( + providers_details_dict + ) + + service_obj = service.load_from_json(self.service_json) + service_obj.provider_details = providers_details_dict + service_obj.status = u'deployed' + self.sc.storage_controller.get_service.return_value = service_obj + self.sc.ssl_certificate_storage.get_certs_by_domain.return_value = [] + self.sc.flavor_controller.get.return_value = flavor.Flavor( + 'standard', + providers=[ + flavor.Provider('MaxCDN', 'http://maxcdn.com'), + flavor.Provider('Mock', 'http://www.mock.com'), + flavor.Provider('CloudFront', 'http://www.cloudfront.com'), + flavor.Provider('Fastly', 'http://www.fastly.com') + ] + ) + service_updates = json.dumps([ + { + "op": "replace", + "path": "/domains/0", + "value": { + "domain": "www.mywebsite.com", + "protocol": "https", + "certificate": "san" + } + } + ]) + + self.sc.update_service( + self.project_id, + self.service_id, + self.auth_token, + service_updates + ) + + # ensure the manager calls the storage driver with the appropriate data + self.sc.storage_controller.update_service.assert_called_once() + + def test_update_service_operator_status_disabled_error(self): + provider_details_dict = { + "MaxCDN": {"id": 11942, "access_urls": ["mypullzone.netdata.com"]}, + "Mock": {"id": 73242, "access_urls": ["mycdn.mock.com"]}, + "CloudFront": { + "id": "5ABC892", "access_urls": ["cf123.cloudcf.com"]}, + "Fastly": { + "id": 3488, "access_urls": ["mockcf123.fastly.prod.com"]} + } + providers_details = {} + for name in provider_details_dict: + details = provider_details_dict[name] + provider_detail_obj = provider_details.ProviderDetail( + provider_service_id=details['id'], + access_urls=details['access_urls'], + status=details.get('status', u'unknown')) + providers_details[name] = provider_detail_obj + + self.sc.storage_controller.get_provider_details.return_value = ( + providers_details + ) + + service_obj = service.load_from_json(self.service_json) + service_obj.status = u'deployed' + service_obj.operator_status = 'disabled' + self.sc.storage_controller.get_service.return_value = service_obj + service_updates = json.dumps([ + { + "op": "replace", + "path": "/domains/0", + "value": {"domain": "added.mocksite4.com"} + } + ]) + + with testtools.ExpectedException(errors.ServiceStatusDisabled): + self.sc.update_service( + self.project_id, + self.service_id, + self.auth_token, + service_updates + ) + + def test_update_service_status_not_failed_or_deployed_error(self): + provider_details_dict = { + "MaxCDN": {"id": 11942, "access_urls": ["mypullzone.netdata.com"]}, + "Mock": {"id": 73242, "access_urls": ["mycdn.mock.com"]}, + "CloudFront": { + "id": "5ABC892", "access_urls": ["cf123.cloudcf.com"]}, + "Fastly": { + "id": 3488, "access_urls": ["mockcf123.fastly.prod.com"]} + } + providers_details = {} + for name in provider_details_dict: + details = provider_details_dict[name] + provider_detail_obj = provider_details.ProviderDetail( + provider_service_id=details['id'], + access_urls=details['access_urls'], + status=details.get('status', u'unknown')) + providers_details[name] = provider_detail_obj + + self.sc.storage_controller.get_provider_details.return_value = ( + providers_details + ) + + service_obj = service.load_from_json(self.service_json) + service_obj.status = u'create_in_progress' + + self.sc.storage_controller.get_service.return_value = service_obj + service_updates = json.dumps([ + { + "op": "replace", + "path": "/domains/0", + "value": {"domain": "added.mocksite4.com"} + } + ]) + + with testtools.ExpectedException( + errors.ServiceStatusNeitherDeployedNorFailed + ): + self.sc.update_service( + self.project_id, + self.service_id, + self.auth_token, + service_updates + ) + @ddt.file_data('data_provider_details.json') def test_delete(self, provider_details_json): self.provider_details = {} diff --git a/tests/unit/storage/cassandra/data_get_certs_by_domain.json b/tests/unit/storage/cassandra/data_get_certs_by_domain.json index 4eb19aba..295f5c5f 100644 --- a/tests/unit/storage/cassandra/data_get_certs_by_domain.json +++ b/tests/unit/storage/cassandra/data_get_certs_by_domain.json @@ -1,73 +1,73 @@ [ [ - [ - { - "project_id": 12345, - "flavor_id": "flavor1", - "cert_type": "san", - "domain_name": "www.mydomain.com", - "cert_details": { - "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" + [ + { + "project_id": 12345, + "flavor_id": "flavor1", + "cert_type": "san", + "domain_name": "www.mydomain.com", + "cert_details": { + "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" + } + }, + { + "project_id": 12345, + "flavor_id": "flavor2", + "cert_type": "custom", + "domain_name": "www.mydomain.com", + "cert_details": { + "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_custom\", \"action\": \"Ready\"}}" + } } - }, - { - "project_id": 12345, - "flavor_id": "flavor2", - "cert_type": "custom", - "domain_name": "www.mydomain.com", - "cert_details": { - "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_custom\", \"action\": \"Ready\"}}" + ], + [ + { + "project_id": 12345, + "flavor_id": "flavor1", + "cert_type": "custom", + "domain_name": "www.example.com", + "cert_details": { + "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_custom\", \"action\": \"Ready\"}}" + } + }, + { + "project_id": 12345, + "flavor_id": "flavor1", + "cert_type": "san", + "domain_name": "www.example.com", + "cert_details": { + "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" + } + }, + { + "project_id": 12346, + "flavor_id": "flavor2", + "cert_type": "san", + "domain_name": "www.mydomain2.com", + "cert_details": { + "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" + } } - } - ], - [ - { - "project_id": 12345, - "flavor_id": "flavor1", - "cert_type": "custom", - "domain_name": "www.example.com", - "cert_details": { - "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_custom\", \"action\": \"Ready\"}}" + ], + [ + { + "project_id": 12345, + "flavor_id": "flavor1", + "cert_type": "san", + "domain_name": "www.mydomain.com", + "cert_details": { + "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" + } + }, + { + "project_id": 12346, + "flavor_id": "flavor2", + "cert_type": "san", + "domain_name": "www.mydomain2.com", + "cert_details": { + "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" + } } - }, - { - "project_id": 12345, - "flavor_id": "flavor1", - "cert_type": "san", - "domain_name": "www.example.com", - "cert_details": { - "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" - } - }, - { - "project_id": 12346, - "flavor_id": "flavor2", - "cert_type": "san", - "domain_name": "www.mydomain2.com", - "cert_details": { - "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" - } - } - ], - [ - { - "project_id": 12345, - "flavor_id": "flavor1", - "cert_type": "san", - "domain_name": "www.mydomain.com", - "cert_details": { - "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" - } - }, - { - "project_id": 12346, - "flavor_id": "flavor2", - "cert_type": "san", - "domain_name": "www.mydomain2.com", - "cert_details": { - "provider": "{\"cert_domain\": \"abc\", \"extra_info\": { \"status\": \"deployed\", \"san_cert\": \"awesome_san\", \"action\": \"Ready\"}}" - } - } - ] + ] ] ] \ No newline at end of file diff --git a/tests/unit/storage/cassandra/test_certificates.py b/tests/unit/storage/cassandra/test_certificates.py index c7fb4cb4..35dce2e2 100644 --- a/tests/unit/storage/cassandra/test_certificates.py +++ b/tests/unit/storage/cassandra/test_certificates.py @@ -15,7 +15,6 @@ import uuid -import cassandra import ddt import mock from oslo_config import cfg @@ -27,10 +26,10 @@ from tests.unit import base @ddt.ddt -class CassandraStorageServiceTests(base.TestCase): +class CassandraStorageCertificateTests(base.TestCase): def setUp(self): - super(CassandraStorageServiceTests, self).setUp() + super(CassandraStorageCertificateTests, self).setUp() # mock arguments to use self.project_id = '123456' @@ -54,16 +53,18 @@ class CassandraStorageServiceTests(base.TestCase): migrations_patcher.start() self.addCleanup(migrations_patcher.stop) + cluster_patcher = mock.patch('cassandra.cluster.Cluster') + self.mock_cluster = cluster_patcher.start() + self.mock_session = self.mock_cluster().connect() + self.addCleanup(cluster_patcher.stop) + # stubbed cassandra driver self.cc = certificates.CertificatesController(cassandra_driver) @ddt.file_data('data_get_certs_by_domain.json') - @mock.patch.object(certificates.CertificatesController, 'session') - @mock.patch.object(cassandra.cluster.Session, 'execute') - def test_get_certs_by_domain(self, cert_details_json, - mock_session, mock_execute): + def test_get_certs_by_domain(self, cert_details_json): # mock the response from cassandra - mock_execute.execute.return_value = cert_details_json[0] + self.mock_session.execute.return_value = cert_details_json[0] actual_response = self.cc.get_certs_by_domain( domain_name="www.mydomain.com" ) @@ -71,7 +72,7 @@ class CassandraStorageServiceTests(base.TestCase): self.assertTrue(all([isinstance(ssl_cert, ssl_certificate.SSLCertificate) for ssl_cert in actual_response])) - mock_execute.execute.return_value = cert_details_json[1] + self.mock_session.execute.return_value = cert_details_json[1] actual_response = self.cc.get_certs_by_domain( domain_name="www.example.com", flavor_id="flavor1") @@ -79,7 +80,7 @@ class CassandraStorageServiceTests(base.TestCase): self.assertTrue(all([isinstance(ssl_cert, ssl_certificate.SSLCertificate) for ssl_cert in actual_response])) - mock_execute.execute.return_value = cert_details_json[2] + self.mock_session.execute.return_value = cert_details_json[2] actual_response = self.cc.get_certs_by_domain( domain_name="www.mydomain.com", flavor_id="flavor1", @@ -87,20 +88,82 @@ class CassandraStorageServiceTests(base.TestCase): self.assertTrue(isinstance(actual_response, ssl_certificate.SSLCertificate)) - @mock.patch.object(certificates.CertificatesController, 'session') - @mock.patch.object(cassandra.cluster.Session, 'execute') - def test_get_certs_by_status(self, mock_session, mock_execute): + def test_get_certs_by_status(self): # mock the response from cassandra - mock_execute.execute.return_value = \ + self.mock_session.execute.return_value = \ [{"domain_name": "www.example.com"}] actual_response = self.cc.get_certs_by_status( status="deployed") self.assertEqual(actual_response, [{"domain_name": "www.example.com"}]) - mock_execute.execute.return_value = \ + self.mock_session.execute.return_value = \ [{"domain_name": "www.example1.com"}] actual_response = self.cc.get_certs_by_status( status="failed") self.assertEqual(actual_response, [{"domain_name": "www.example1.com"}]) + + @ddt.file_data('data_get_certs_by_domain.json') + def test_create_cert_already_exists(self, cert_details_json): + # mock the response from cassandra + self.mock_session.execute.return_value = cert_details_json[0] + + ssl_cert_obj = ssl_certificate.SSLCertificate( + 'flavor1', + 'www.mydomain.com', + 'san' + ) + self.assertRaises( + ValueError, + self.cc.create_certificate, '12345', ssl_cert_obj + ) + + @ddt.file_data('data_get_certs_by_domain.json') + def test_delete_cert(self, cert_details_json): + # mock the response from cassandra + self.mock_session.execute.return_value = cert_details_json[0] + + try: + self.cc.delete_certificate('12345', 'www.mydomain.com', 'san') + except Exception as e: + self.fail(e) + + def test_create_certificate_with_cert_status_in_details(self): + ssl_cert_obj = ssl_certificate.SSLCertificate( + 'flavor1', + 'www.mydomain.com', + 'san', + project_id='12345', + cert_details={ + "provider": "{\"cert_domain\": \"abc\", \"extra_info\": " + "{ \"status\": \"deployed\", \"san_cert\": \"" + "awesome_san\", \"action\": \"Ready\"}}" + } + ) + + try: + self.cc.create_certificate('12345', ssl_cert_obj) + except Exception as e: + self.fail(e) + + def test_update_certificate_with_cert_status_in_details(self): + ssl_cert_obj = ssl_certificate.SSLCertificate( + 'flavor1', + 'www.mydomain.com', + 'san', + project_id='12345', + cert_details={ + "provider": "{\"cert_domain\": \"abc\", \"extra_info\": " + "{ \"status\": \"deployed\", \"san_cert\": \"" + "awesome_san\", \"action\": \"Ready\"}}" + } + ) + + try: + self.cc.update_certificate( + 'www.mydomain.com', 'san', 'flavor1', + ssl_cert_obj.cert_details + ) + except Exception as e: + self.fail(e)