shared ssl domain implmentation

Change-Id: I81ed3dfd073c00aa56bd2a7968c06ab2798d25b7
This commit is contained in:
tonytan4ever 2015-03-06 12:31:33 -05:00 committed by tonytan4ever
parent 6406a3f96d
commit e84df4ff3e
9 changed files with 208 additions and 33 deletions

View File

@ -101,6 +101,9 @@ api_key = "<operator_api_key>"
use_shards = True
num_shards = 400
shard_prefix = "cdn"
shared_ssl_num_shards = 5
shared_ssl_shard_prefix = "scdn"
shared_ssl_domain_suffix = "secure.poppycdn.net"
url = "poppycdn.net"
# You email associated with DNS, for notifications
email = "your@email.com"

View File

@ -59,3 +59,13 @@ class ServicesControllerBase(controller.DNSControllerBase):
"""
raise NotImplementedError
def generate_shared_ssl_domain_suffix(self):
"""Generate a shared ssl domain suffix,
to be used with manager for shared ssl feature
:raises NotImplementedError
"""
raise NotImplementedError

View File

@ -77,3 +77,11 @@ class ServicesController(base.ServicesBase):
access_urls.append(access_url)
dns_details[provider_name] = {'access_urls': access_urls}
return self.responder.created(dns_details)
def generate_shared_ssl_domain_suffix(self):
"""Default DNS Generate a shared ssl domain suffix,
to be used with manager for shared ssl feature
"""
return 'scdn023.secure.defaultcdn.com'

View File

@ -33,6 +33,14 @@ RACKSPACE_OPTIONS = [
cfg.IntOpt('num_shards', default=10, help='Number of Shards to use'),
cfg.StrOpt('shard_prefix', default='cdn',
help='The shard prefix to use'),
cfg.IntOpt('shared_ssl_num_shards', default=5, help='Number of Shards '
'to use in generating shared ssl domain suffix'),
cfg.StrOpt('shared_ssl_shard_prefix', default='scdn',
help='The shard prefix to use '
'in generating shared ssl domain suffix'),
cfg.StrOpt('shared_ssl_domain_suffix', default='',
help='The shared ssl domain suffix to generate'
' shared ssl domain'),
cfg.StrOpt('url', default='',
help='The url for customers to CNAME to'),
cfg.StrOpt('email', help='The email to be provided to Rackspace DNS for'

View File

@ -68,9 +68,21 @@ class ServicesController(base.ServicesBase):
subdomain = self._get_subdomain(subdomain_name)
# create CNAME record for adding
cname_records = []
dns_links = {}
shared_ssl_subdomain_name = None
for link in links:
name = '{0}.{1}'.format(link, subdomain_name)
# pick out shared ssl domains here
domain_name, certificate = 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)
cname_record = {'type': 'CNAME',
'name': name,
'data': links[link],
@ -79,12 +91,19 @@ class ServicesController(base.ServicesBase):
'provider_url': links[link],
'operator_url': name
}
cname_records.append(cname_record)
if certificate == "shared":
shared_ssl_subdomain = self._get_subdomain(
shared_ssl_subdomain_name)
shared_ssl_subdomain.add_records([cname_record])
else:
cname_records.append(cname_record)
# add the cname records
subdomain.add_records(cname_records)
if cname_records != []:
subdomain.add_records(cname_records)
return dns_links
def _delete_cname_record(self, access_url):
def _delete_cname_record(self, access_url, shared_ssl_flag):
"""Delete a CNAME record
:param access_url: DNS Access URL
@ -92,8 +111,18 @@ class ServicesController(base.ServicesBase):
"""
# extract shard name
shard_name = access_url.split('.')[-3]
subdomain_name = '.'.join([shard_name, self._driver.rackdns_conf.url])
if shared_ssl_flag:
suffix = self._driver.rackdns_conf.shared_ssl_domain_suffix
else:
suffix = self._driver.rackdns_conf.url
# Note: use rindex to find last occurence of the suffix
shard_name = access_url[:access_url.rindex(suffix)-1].split('.')[-1]
subdomain_name = '.'.join([shard_name, suffix])
# for sharding is disabled, the suffix is the subdomain_name
if shared_ssl_flag and (
self._driver.rackdns_conf.shared_ssl_num_shards == 0):
subdomain_name = suffix
# get subdomain
subdomain = self.client.find(name=subdomain_name)
# search and find the CNAME record
@ -113,6 +142,33 @@ class ServicesController(base.ServicesBase):
return error_msg
return
def _generate_sharded_domain_name(self, shard_prefix, num_shards, suffix):
"""Generate a sharded domain name based on the scheme:
'{shard_prefix}{a random shard_id}.{suffix}'
:return A string of sharded domain name
"""
if num_shards == 0:
# shard disabled, just use the suffix
return suffix
else:
# shard enabled, randomly select a shard
shard_id = random.randint(1, num_shards)
return '{0}{1}.{2}'.format(shard_prefix, shard_id, suffix)
def generate_shared_ssl_domain_suffix(self):
"""Rackespace DNS scheme to generate a shared ssl domain suffix,
to be used with manager for shared ssl feature
:return A string of shared ssl domain name
"""
return self._generate_sharded_domain_name(
self._driver.rackdns_conf.shared_ssl_shard_prefix,
self._driver.rackdns_conf.shared_ssl_num_shards,
self._driver.rackdns_conf.shared_ssl_domain_suffix)
def create(self, responders):
"""Create CNAME record for a service.
@ -137,7 +193,11 @@ class ServicesController(base.ServicesBase):
for provider_name in responder:
for link in responder[provider_name]['links']:
if link['rel'] == 'access_url':
links[link['domain']] = link['href']
# 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']
# create CNAME records
try:
@ -157,9 +217,19 @@ class ServicesController(base.ServicesBase):
access_url = {
'domain': link['domain'],
'provider_url':
dns_links[link['domain']]['provider_url'],
dns_links[(link['domain'],
link.get('certificate', None)
)]['provider_url'],
'operator_url':
dns_links[link['domain']]['operator_url']}
dns_links[(link['domain'],
link.get('certificate', 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
if link.get('certificate', None) == 'shared':
access_url['shared_ssl_flag'] = True
access_urls.append(access_url)
dns_details[provider_name] = {'access_urls': access_urls}
return self.responder.created(dns_details)
@ -181,7 +251,9 @@ class ServicesController(base.ServicesBase):
access_urls = provider_details[provider_name].access_urls
for access_url in access_urls:
try:
msg = self._delete_cname_record(access_url['operator_url'])
msg = self._delete_cname_record(
access_url['operator_url'],
access_url.get('shared_ssl_flag', False))
if msg:
error_msg = error_msg + msg
except exc.NotFound as e:
@ -228,7 +300,8 @@ class ServicesController(base.ServicesBase):
domain_added = (link['rel'] == 'access_url' and
link['domain'] in added_domains)
if domain_added:
links[link['domain']] = link['href']
links[(link['domain'], link.get('certificate',
None))] = link['href']
# create CNAME records for added domains
try:
@ -247,9 +320,19 @@ class ServicesController(base.ServicesBase):
access_url = {
'domain': link['domain'],
'provider_url':
dns_links[link['domain']]['provider_url'],
dns_links[(link['domain'],
link.get('certificate', None)
)]['provider_url'],
'operator_url':
dns_links[link['domain']]['operator_url']}
dns_links[(link['domain'],
link.get('certificate', 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
if link.get('certificate', None) == 'shared':
access_url['shared_ssl_flag'] = True
access_urls.append(access_url)
dns_details[provider_name] = {'access_urls': access_urls}
return dns_details
@ -276,7 +359,10 @@ class ServicesController(base.ServicesBase):
if access_url['domain'] not in removed_domains:
continue
try:
msg = self._delete_cname_record(access_url['operator_url'])
msg = self._delete_cname_record(access_url['operator_url'],
access_url.get(
'shared_ssl_flag',
False))
if msg:
error_msg = error_msg + msg
except exc.NotFound as e:
@ -386,6 +472,9 @@ class ServicesController(base.ServicesBase):
'domain': link['domain'],
'provider_url': link['href'],
'operator_url': operator_url}
# if it is a shared ssl access url, we need to store it
if new_access_url.get('shared_ssl_flag', False):
access_url['shared_ssl_flag'] = True
access_urls.append(access_url)
elif link['domain'] in common_domains:
# iterate through old access urls and get access url
@ -398,6 +487,9 @@ class ServicesController(base.ServicesBase):
'domain': link['domain'],
'provider_url': link['href'],
'operator_url': operator_url}
# if it is a shared ssl access url, we need to store it
if old_access_url.get('shared_ssl_flag', False):
access_url['shared_ssl_flag'] = True
access_urls.append(access_url)
dns_details[provider_name] = {'access_urls': access_urls}

View File

@ -122,6 +122,13 @@ class DefaultServicesController(base.ServicesController):
providers = [p.provider_id for p in flavor.providers]
service_id = service_obj.service_id
# deal with shared ssl domains
for domain in service_obj.domains:
if domain.protocol == 'https' and domain.certificate == 'shared':
domain.domain = self._generate_shared_ssl_domain(
domain.domain
)
try:
self.storage_controller.create(
project_id,
@ -160,6 +167,14 @@ class DefaultServicesController(base.ServicesController):
raise errors.ServiceStatusNeitherDeployedNorFailed(
u'Service {0} neither deployed nor failed'.format(service_id))
# Fixing the operator_url domain for ssl
existing_shared_domains = {}
for domain in service_old.domains:
if domain.protocol == 'https' and domain.certificate == 'shared':
customer_domain = domain.domain.split('.')[0]
existing_shared_domains[customer_domain] = domain.domain
domain.domain = customer_domain
service_old_json = json.loads(json.dumps(service_old.to_dict()))
# remove fields that cannot be part of PATCH
@ -192,6 +207,18 @@ class DefaultServicesController(base.ServicesController):
raise ValueError(
"Domain {0} has already been taken".format(d.domain))
# fixing the old and new shared ssl domains in service_new
for domain in service_new.domains:
if domain.protocol == 'https' and domain.certificate == 'shared':
customer_domain = domain.domain.split('.')[0]
# if this domain is from service_old
if customer_domain in existing_shared_domains:
domain.domain = existing_shared_domains[customer_domain]
else:
domain.domain = self._generate_shared_ssl_domain(
domain.domain
)
# set status in provider details to u'update_in_progress'
provider_details = service_old.provider_details
for provider in provider_details:
@ -260,3 +287,8 @@ class DefaultServicesController(base.ServicesController):
purge_service.purge_service, **kwargs)
return
def _generate_shared_ssl_domain(self, domain_name):
shared_ssl_domain_suffix = (
self.dns_controller.generate_shared_ssl_domain_suffix())
return '.'.join([domain_name, shared_ssl_domain_suffix])

View File

@ -32,7 +32,7 @@ class ProviderDetail(common.DictSerializableModel):
"""ProviderDetail object for each provider."""
def __init__(self, provider_service_id=None, access_urls={},
def __init__(self, provider_service_id=None, access_urls=[],
status=u"deploy_in_progress", name=None, error_info=None,
error_message=None):
self._provider_service_id = provider_service_id
@ -122,7 +122,7 @@ class ProviderDetail(common.DictSerializableModel):
o = cls("unnamed")
o.provider_service_id = dict_obj.get("id",
"unknown_id")
o.access_urls = dict_obj.get("access_urls", {})
o.access_urls = dict_obj.get("access_urls", [])
o.status = dict_obj.get("status", u"deploy_in_progress")
o.name = dict_obj.get("name", None)
o.error_info = dict_obj.get("error_info", None)

View File

@ -113,7 +113,8 @@ class ServiceController(base.ServiceBase):
raise RuntimeError(resp.text)
dp_obj = {'policy_name': dp,
'protocol': classified_domain.protocol}
'protocol': classified_domain.protocol,
'certificate': classified_domain.certificate}
ids.append(dp_obj)
# TODO(tonytan4ever): leave empty links for now
# may need to work with dns integration
@ -123,7 +124,8 @@ class ServiceController(base.ServiceBase):
classified_domain, dp)
links.append({'href': provider_access_url,
'rel': 'access_url',
'domain': classified_domain.domain
'domain': classified_domain.domain,
'certificate': classified_domain.certificate
})
except Exception as e:
LOG.error('Creating policy failed: %s' % traceback.format_exc())
@ -252,7 +254,9 @@ class ServiceController(base.ServiceBase):
data=json.dumps(policy_content),
headers=self.request_header)
dp_obj = {'policy_name': dp,
'protocol': classified_domain.protocol}
'protocol': classified_domain.protocol,
'certificate':
classified_domain.certificate}
policies.remove(dp_obj)
else:
LOG.info('Start to create new policy %s' % dp)
@ -268,7 +272,8 @@ class ServiceController(base.ServiceBase):
if resp.status_code != 200:
raise RuntimeError(resp.text)
dp_obj = {'policy_name': dp,
'protocol': classified_domain.protocol}
'protocol': classified_domain.protocol,
'certificate': classified_domain.certificate}
ids.append(dp_obj)
# TODO(tonytan4ever): leave empty links for now
# may need to work with dns integration
@ -278,7 +283,9 @@ class ServiceController(base.ServiceBase):
classified_domain, dp)
links.append({'href': provider_access_url,
'rel': 'access_url',
'domain': dp
'domain': dp,
'certificate':
classified_domain.certificate
})
except Exception:
return self.responder.failed("failed to update service")
@ -371,7 +378,8 @@ class ServiceController(base.ServiceBase):
util.dict2obj(policy), policy['policy_name'])
links.append({'href': provider_access_url,
'rel': 'access_url',
'domain': policy['policy_name']
'domain': policy['policy_name'],
'certificate': policy['certificate']
})
ids = policies
return self.responder.updated(json.dumps(ids), links)
@ -619,6 +627,11 @@ class ServiceController(base.ServiceBase):
if domain_obj.protocol == 'http':
provider_access_url = self.driver.akamai_access_url_link
elif domain_obj.protocol == 'https':
provider_access_url = '.'.join(
[dp, self.driver.akamai_https_access_url_suffix])
if domain_obj.certificate == "shared":
provider_access_url = '.'.join(
['.'.join(dp.split('.')[1:]),
self.driver.akamai_https_access_url_suffix])
else:
provider_access_url = '.'.join(
[dp, self.driver.akamai_https_access_url_suffix])
return provider_access_url

View File

@ -101,7 +101,8 @@ class TestServices(base.TestCase):
def test_delete_with_exception(self):
provider_service_id = json.dumps([{'policy_name': str(uuid.uuid1()),
'protocol': 'http'}])
'protocol': 'http',
'certificate': None}])
# test exception
exception = RuntimeError('ding')
@ -118,7 +119,8 @@ class TestServices(base.TestCase):
def test_delete_with_4xx_return(self):
provider_service_id = json.dumps([{'policy_name': str(uuid.uuid1()),
'protocol': 'http'}])
'protocol': 'http',
'certificate': None}])
# test exception
self.controller.policy_api_client.delete.return_value = mock.Mock(
@ -131,7 +133,8 @@ class TestServices(base.TestCase):
def test_delete(self):
provider_service_id = json.dumps([{'policy_name': str(uuid.uuid1()),
'protocol': 'http'}])
'protocol': 'http',
'certificate': None}])
self.controller.delete(provider_service_id)
self.controller.policy_api_client.delete.assert_called_once()
@ -170,7 +173,8 @@ class TestServices(base.TestCase):
@ddt.file_data('data_update_service.json')
def test_update(self, service_json):
provider_service_id = json.dumps([{'policy_name': str(uuid.uuid1()),
'protocol': 'http'}])
'protocol': 'http',
'certificate': None}])
controller = services.ServiceController(self.driver)
controller.policy_api_client.get.return_value = mock.Mock(
status_code=200,
@ -192,7 +196,8 @@ class TestServices(base.TestCase):
@ddt.file_data('data_update_service.json')
def test_update_with_domain_protocol_change(self, service_json):
provider_service_id = json.dumps([{'policy_name': "densely.sage.com",
'protocol': 'http'}])
'protocol': 'http',
'certificate': None}])
controller = services.ServiceController(self.driver)
controller.policy_api_client.get.return_value = mock.Mock(
status_code=200,
@ -214,7 +219,8 @@ class TestServices(base.TestCase):
@ddt.file_data('data_upsert_service.json')
def test_upsert(self, service_json):
provider_service_id = json.dumps([{'policy_name': "densely.sage.com",
'protocol': 'http'}])
'protocol': 'http',
'certificate': None}])
controller = services.ServiceController(self.driver)
controller.policy_api_client.get.return_value = mock.Mock(
status_code=404,
@ -231,7 +237,8 @@ class TestServices(base.TestCase):
def test_purge_all(self):
provider_service_id = json.dumps([{'policy_name': str(uuid.uuid1()),
'protocol': 'http'}])
'protocol': 'http',
'certificate': None}])
controller = services.ServiceController(self.driver)
resp = controller.purge(provider_service_id, None)
self.assertIn('error', resp[self.driver.provider_name])
@ -244,7 +251,8 @@ class TestServices(base.TestCase):
def test_purge_with_ccu_exception(self):
provider_service_id = json.dumps([{'policy_name': str(uuid.uuid1()),
'protocol': 'http'}])
'protocol': 'http',
'certificate': None}])
controller = services.ServiceController(self.driver)
controller.ccu_api_client.post.return_value = mock.Mock(
status_code=400,
@ -255,7 +263,8 @@ class TestServices(base.TestCase):
def test_purge(self):
provider_service_id = json.dumps([{'policy_name': str(uuid.uuid1()),
'protocol': 'https'}])
'protocol': 'https',
'certificate': 'shared'}])
controller = services.ServiceController(self.driver)
controller.ccu_api_client.post.return_value = mock.Mock(
status_code=201,