Add admin endpoint to patch access urls

Change-Id: I30da22d2a229b346212e53abd7bff50884929ab6
This commit is contained in:
Isaac Mungai 2016-09-27 16:38:24 -04:00
parent 0458de585d
commit 18f6f0ec86
8 changed files with 642 additions and 39 deletions

View File

@ -68,10 +68,8 @@ class ServicesController(base.ServicesBase):
# ex. cdnXXX.altcdn.com # ex. cdnXXX.altcdn.com
subdomain_name = '{0}{1}.{2}'.format(shard_prefix, shard_id, subdomain_name = '{0}{1}.{2}'.format(shard_prefix, shard_id,
cdn_domain_name) cdn_domain_name)
subdomain = self._get_subdomain(subdomain_name)
# create CNAME record for adding # create CNAME record for adding
cname_records = [] cname_records = []
dns_links = {} dns_links = {}
shared_ssl_subdomain_name = None shared_ssl_subdomain_name = None
@ -85,40 +83,13 @@ class ServicesController(base.ServicesBase):
name = domain_name name = domain_name
else: else:
if old_operator_url is not None: if old_operator_url is not None:
# verify sub-domain exists created_dns_links = self._create_preferred_cname_record(
regex_match = re.match( domain_name,
r'^.*(' + shard_prefix + '[0-9]+\.' + certificate,
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, old_operator_url,
links[link] links[link]
) )
) dns_links.update(created_dns_links)
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 continue
else: else:
name = '{0}.{1}'.format(domain_name, subdomain_name) name = '{0}.{1}'.format(domain_name, subdomain_name)
@ -140,11 +111,71 @@ class ServicesController(base.ServicesBase):
else: else:
cname_records.append(cname_record) cname_records.append(cname_record)
# add the cname records # add the cname records
if cname_records != []: if len(cname_records) > 0:
subdomain = self._get_subdomain(subdomain_name)
LOG.info("Creating DNS Record - {0}".format(cname_records)) LOG.info("Creating DNS Record - {0}".format(cname_records))
subdomain.add_records(cname_records) subdomain.add_records(cname_records)
return dns_links return dns_links
def _create_preferred_cname_record(
self, domain_name, certificate, operator_url, provider_url):
"""Creates a CNAME chain with designated operator_url
:param domain_name: domain name that CNAME chain is created for
:param certificate: certificate type
:operator_url: The preferred operator url
:provider_url: provider url
:return dns_links: Map from provider access URL to DNS access URL
"""
shard_prefix = self._driver.rackdns_conf.shard_prefix
cdn_domain_name = self._driver.rackdns_conf.url
dns_links = {}
# verify sub-domain exists
regex_match = re.match(
r'^.*(' + shard_prefix + '[0-9]+\.' +
re.escape(cdn_domain_name) + ')$',
operator_url
)
my_sub_domain_name = regex_match.groups(-1)[0]
if my_sub_domain_name is None:
raise ValueError('Unable to parse old operator url')
# add to cname record
my_sub_domain = self._get_subdomain(my_sub_domain_name)
LOG.info(
"Updating dns record {0}. "
"CNAME create/update from {1} to {2}".format(
my_sub_domain_name,
operator_url,
provider_url
)
)
try:
old_dns_record = my_sub_domain.find_record('CNAME', operator_url)
except exc.DomainRecordNotFound:
my_sub_domain.add_records(
[{
'type': 'CNAME',
'name': operator_url,
'data': provider_url,
'ttl': 300
}]
)
else:
my_sub_domain.update_record(old_dns_record, data=provider_url)
dns_links[(domain_name, certificate, operator_url)] = {
'provider_url': provider_url,
'operator_url': operator_url
}
return dns_links
def _search_cname_record(self, access_url, shared_ssl_flag): def _search_cname_record(self, access_url, shared_ssl_flag):
"""Search a CNAME record """Search a CNAME record
@ -158,7 +189,7 @@ class ServicesController(base.ServicesBase):
suffix = self._driver.rackdns_conf.shared_ssl_domain_suffix suffix = self._driver.rackdns_conf.shared_ssl_domain_suffix
else: else:
suffix = self._driver.rackdns_conf.url suffix = self._driver.rackdns_conf.url
# Note: use rindex to find last occurence of the suffix # Note: use rindex to find last occurrence of the suffix
shard_name = access_url[:access_url.rindex(suffix)-1].split('.')[-1] shard_name = access_url[:access_url.rindex(suffix)-1].split('.')[-1]
subdomain_name = '.'.join([shard_name, suffix]) subdomain_name = '.'.join([shard_name, suffix])
@ -184,6 +215,8 @@ class ServicesController(base.ServicesBase):
:param shared_ssl_flag: flag indicating if this is a shared ssl domain :param shared_ssl_flag: flag indicating if this is a shared ssl domain
:return error_msg: returns error message, if any :return error_msg: returns error message, if any
""" """
LOG.info('Attempting to delete DNS records for : {0}'.format(
access_url))
records = self._search_cname_record(access_url, shared_ssl_flag) records = self._search_cname_record(access_url, shared_ssl_flag)
# delete the record # delete the record

View File

@ -812,3 +812,81 @@ class DefaultServicesController(base.ServicesController):
is_upgrade = True is_upgrade = True
break break
return is_upgrade return is_upgrade
def update_access_url_service(
self, project_id, service_id, access_url_changes):
try:
service_old = self.storage_controller.get_service(
project_id,
service_id
)
except ValueError as e:
# If service is not found
LOG.warning('Get service {0} failed. '
'Error message: {1}'.format(service_id, e))
raise errors.ServiceNotFound(e)
updated_details = False
provider_details = service_old.provider_details
domain_name = access_url_changes.get('domain_name')
for provider in provider_details:
for access_url in provider_details[provider].access_urls:
if access_url.get('domain') == domain_name:
if (
'operator_url' in access_url and
'provider_url' in access_url
):
new_access_url = access_url_changes['operator_url']
new_provider_url = access_url_changes['provider_url']
if access_url.get('shared_ssl_flag', False) is True:
raise errors.InvalidOperation(
'Changing access urls for shared ssl domains '
'is not supported.')
if not new_access_url.startswith(domain_name):
LOG.info('Invalid access_url/domain_name.')
raise errors.InvalidResourceName(
'Invalid access_url/domain_name.')
if new_access_url == access_url['operator_url']:
LOG.info(
"No changes made, both old and new access "
"urls are the same. "
"Domain '{0}'.".format(domain_name))
return False
if new_provider_url != access_url['provider_url']:
raise errors.InvalidOperation(
'Please use the migrate domain functionality '
'to migrate the domain to a new cert.'
)
certificate = (
"shared"
if access_url.get('shared_ssl_flag', False) is True
else None
)
self.dns_controller._create_preferred_cname_record(
domain_name,
certificate,
new_access_url,
new_provider_url
)
self.dns_controller._delete_cname_record(
access_url['operator_url'],
access_url.get('shared_ssl_flag', False)
)
access_url['provider_url'] = new_provider_url
access_url['operator_url'] = new_access_url
updated_details = True
break
if updated_details is True:
self.storage_controller.update_provider_details(
project_id,
service_id,
provider_details
)
else:
err_msg = 'Domain {0} could not be found on service {1}.'.format(
domain_name, service_id)
LOG.error(err_msg)
raise ValueError(err_msg)
return updated_details

View File

@ -21,7 +21,9 @@ CERTIFICATE_OPTIONS = [
None, None,
u'shared', u'shared',
u'san', u'san',
u'custom'] u'sni',
u'custom'
]
from poppy.model import common from poppy.model import common

View File

@ -155,7 +155,7 @@ class ProviderDetail(common.DictSerializableModel):
"""Return an access url object for a domain. """Return an access url object for a domain.
:param domain: domain to use as search key :param domain: domain to use as search key
:type domain: poppy.model.helpers.domain.Domain :type domain: str
:returns: access_url -- dict containing matching domain :returns: access_url -- dict containing matching domain
""" """

View File

@ -25,6 +25,7 @@ from poppy.transport.pecan.models.response import service as resp_service_model
from poppy.transport.validators import helpers from poppy.transport.validators import helpers
from poppy.transport.validators.schemas import background_jobs from poppy.transport.validators.schemas import background_jobs
from poppy.transport.validators.schemas import domain_migration from poppy.transport.validators.schemas import domain_migration
from poppy.transport.validators.schemas import provider_details_update
from poppy.transport.validators.schemas import service_action from poppy.transport.validators.schemas import service_action
from poppy.transport.validators.schemas import service_limit from poppy.transport.validators.schemas import service_limit
from poppy.transport.validators.schemas import service_status from poppy.transport.validators.schemas import service_status
@ -77,6 +78,60 @@ class DomainMigrationController(base.Controller, hooks.HookController):
return pecan.Response(None, 202) return pecan.Response(None, 202)
class AdminProviderDetailsController(base.Controller, hooks.HookController):
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
def __init__(self, driver):
super(AdminProviderDetailsController, self).__init__(driver)
@pecan.expose('json')
@decorators.validate(
service_id=rule.Rule(
helpers.is_valid_service_id(),
helpers.abort_with_message),
request=rule.Rule(
helpers.json_matches_service_schema(
provider_details_update.ProviderDetailsUpdateSchema.get_schema(
"update_provider_access_url", "PATCH")
),
helpers.abort_with_message,
stoplight_helpers.pecan_getter))
def patch_one(self, service_id):
request_json = json.loads(pecan.request.body.decode('utf-8'))
project_id = request_json.get('project_id', None)
domain_name = request_json.get('domain_name', None)
operator_url = request_json.get('operator_url', None)
provider_url = request_json.get('provider_url', None)
if not helpers.is_valid_domain_name(domain_name):
pecan.abort(400, detail='Domain {0} is not valid'.format(
domain_name))
changes_made = False
try:
changes_made = self._driver.manager.services_controller.\
update_access_url_service(
project_id,
service_id,
access_url_changes={
'domain_name': domain_name,
'operator_url': operator_url,
'provider_url': provider_url
}
)
except errors.ServiceNotFound:
pecan.abort(404, detail='Service {0} could not be found'.format(
service_id))
except (errors.InvalidOperation, errors.InvalidResourceName) as e:
pecan.abort(400, detail='{0}'.format(e))
except (LookupError, ValueError):
pecan.abort(404, detail='Domain {0} could not be found'.format(
domain_name))
status_code = 201 if changes_made is True else 202
return pecan.Response(None, status=status_code)
class BackgroundJobController(base.Controller, hooks.HookController): class BackgroundJobController(base.Controller, hooks.HookController):
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()] __hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
@ -552,6 +607,9 @@ class AdminServiceController(base.Controller, hooks.HookController):
super(AdminServiceController, self).__init__(driver) super(AdminServiceController, self).__init__(driver)
self.__class__.action = OperatorServiceActionController(driver) self.__class__.action = OperatorServiceActionController(driver)
self.__class__.status = ServiceStatusController(driver) self.__class__.status = ServiceStatusController(driver)
self.__class__.provider_details = AdminProviderDetailsController(
driver
)
@pecan.expose('json') @pecan.expose('json')
@decorators.validate( @decorators.validate(

View File

@ -0,0 +1,54 @@
# Copyright (c) 2015 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.
from poppy.transport.validators import schema_base
class ProviderDetailsUpdateSchema(schema_base.SchemaBase):
"""JSON Schema validation for /admin/services/provider_details."""
schema = {
'update_provider_access_url': {
'PATCH': {
'type': 'object',
'additionalProperties': False,
'properties': {
'project_id': {
'type': 'string',
'required': True
},
'domain_name': {
'type': 'string',
'required': True,
'minLength': 3,
'maxLength': 253
},
'operator_url': {
'type': 'string',
'required': True,
'minLength': 3,
'maxLength': 253
},
'provider_url': {
'type': 'string',
'required': True,
'minLength': 3,
'maxLength': 253
}
}
}
}
}

View File

@ -973,6 +973,115 @@ class TestServicesUpdate(base.TestCase):
self.assertIsNotNone( self.assertIsNotNone(
access_urls_map[provider_name][domain_new.domain]) access_urls_map[provider_name][domain_new.domain])
@mock.patch('re.match')
def test_update_add_domains_https_upgrade_regex_exception(self, re_mock):
re_mock.return_value.groups.return_value = (None,)
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
)
self.assertTrue('error' in dns_details['Fastly'])
self.assertTrue('error_detail' in dns_details['Fastly'])
self.assertTrue('error_class' in dns_details['Fastly'])
self.assertTrue('ValueError' in dns_details['Fastly']['error_class'])
def test_update_add_domains_https_upgrade_create_cname_record(self):
subdomain = mock.Mock()
subdomain.add_records = mock.Mock()
subdomain.find_record.side_effect = exc.DomainRecordNotFound(
"Mock -- couldn't find cname record."
)
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_update_add_domains_keeps_log_delivery(self): def test_update_add_domains_keeps_log_delivery(self):
subdomain = mock.Mock() subdomain = mock.Mock()
subdomain.add_records = mock.Mock() subdomain.add_records = mock.Mock()

View File

@ -1724,3 +1724,272 @@ class DefaultManagerServiceTests(base.TestCase):
self.assertEqual(domains, self.assertEqual(domains,
self.sc.get_domains_by_provider_url('provider_url')) self.sc.get_domains_by_provider_url('provider_url'))
def test_update_access_url_positive(self):
service_obj = service.load_from_json(self.service_json)
service_obj.status = u'deployed'
service_obj.provider_details = {
'Akamai': provider_details.ProviderDetail(
provider_service_id=[
{
"protocol": "http",
"certificate": None,
"policy_name": "www.test1.com"
}
],
access_urls=[
{
"provider_url": "altcdn.com.mdc.edgesuite.net",
"domain": "www.test1.com",
"operator_url": "www.test1.com.cdn136.myraxcdn.net"
}
],
status="deployed",
)
}
self.sc.storage_controller.get_service.return_value = service_obj
updated = self.sc.update_access_url_service(
"project_id",
"service_id",
{
'domain_name': 'www.test1.com',
'operator_url': 'www.test1.com.cdn137.myraxcdn.net',
'provider_url': 'altcdn.com.mdc.edgesuite.net'
}
)
self.assertTrue(updated)
def test_update_access_url_service_not_found(self):
self.sc.storage_controller.get_service.side_effect = (
ValueError('Mock -- Service not found.')
)
self.assertRaises(
errors.ServiceNotFound,
self.sc.update_access_url_service,
"project_id",
"service_id",
{
'domain_name': 'www.test1.com',
'operator_url': 'www.test1.com.cdn137.myraxcdn.net',
'provider_url': 'altcdn.com.mdc.edgesuite.net'
}
)
def test_update_access_url_no_op_patch(self):
service_obj = service.load_from_json(self.service_json)
service_obj.status = u'deployed'
service_obj.provider_details = {
'Akamai': provider_details.ProviderDetail(
provider_service_id=[
{
"protocol": "http",
"certificate": None,
"policy_name": "www.test1.com"
}
],
access_urls=[
{
"provider_url": "altcdn.com.mdc.edgesuite.net",
"domain": "www.test1.com",
"operator_url": "www.test1.com.cdn136.myraxcdn.net"
}
],
status="deployed",
)
}
self.sc.storage_controller.get_service.return_value = service_obj
updated = self.sc.update_access_url_service(
"project_id",
"service_id",
{
'domain_name': 'www.test1.com',
'operator_url': 'www.test1.com.cdn136.myraxcdn.net',
'provider_url': 'altcdn.com.mdc.edgesuite.net'
}
)
self.assertFalse(updated)
def test_update_access_url_provider_url_mismatch(self):
service_obj = service.load_from_json(self.service_json)
service_obj.status = u'deployed'
service_obj.provider_details = {
'Akamai': provider_details.ProviderDetail(
provider_service_id=[
{
"protocol": "http",
"certificate": None,
"policy_name": "www.test1.com"
}
],
access_urls=[
{
"provider_url": "altcdn.com.mdc.edgesuite.net",
"domain": "www.test1.com",
"operator_url": "www.test1.com.cdn136.myraxcdn.net"
}
],
status="deployed",
)
}
self.sc.storage_controller.get_service.return_value = service_obj
self.assertRaises(
errors.InvalidOperation,
self.sc.update_access_url_service,
"project_id",
"service_id",
{
'domain_name': 'www.test1.com',
'operator_url': 'www.test1.com.cdn137.myraxcdn.net',
'provider_url': 'raxcdn.com.mdc.edgesuite.net'
}
)
def test_update_access_url_mismatch_access_url_and_domain(self):
service_obj = service.load_from_json(self.service_json)
service_obj.status = u'deployed'
service_obj.provider_details = {
'Akamai': provider_details.ProviderDetail(
provider_service_id=[
{
"protocol": "http",
"certificate": None,
"policy_name": "www.test1.com"
}
],
access_urls=[
{
"provider_url": "altcdn.com.mdc.edgesuite.net",
"domain": "www.test1.com",
"operator_url": "www.test1.com.cdn136.myraxcdn.net"
}
],
status="deployed",
)
}
self.sc.storage_controller.get_service.return_value = service_obj
self.assertRaises(
errors.InvalidResourceName,
self.sc.update_access_url_service,
"project_id",
"service_id",
{
'domain_name': 'www.test1.com',
'operator_url': 'www.test2.com.cdn137.myraxcdn.net',
'provider_url': 'altcdn.com.mdc.edgesuite.net'
}
)
def test_update_access_url_missing_provider_url(self):
service_obj = service.load_from_json(self.service_json)
service_obj.status = u'deployed'
service_obj.provider_details = {
'Akamai': provider_details.ProviderDetail(
provider_service_id=[
{
"protocol": "http",
"certificate": None,
"policy_name": "www.test1.com"
}
],
access_urls=[
{
"domain": "www.test1.com",
"operator_url": "www.test1.com.cdn136.myraxcdn.net"
}
],
status="deployed",
)
}
self.sc.storage_controller.get_service.return_value = service_obj
self.assertRaises(
ValueError,
self.sc.update_access_url_service,
"project_id",
"service_id",
{
'domain_name': 'www.test1.com',
'operator_url': 'www.test1.com.cdn137.myraxcdn.net',
'provider_url': 'altcdn.com.mdc.edgesuite.net'
}
)
def test_update_access_url_no_matching_access_urls(self):
service_obj = service.load_from_json(self.service_json)
service_obj.status = u'deployed'
service_obj.provider_details = {
'Akamai': provider_details.ProviderDetail(
provider_service_id=[
{
"protocol": "http",
"certificate": None,
"policy_name": "www.test1.com"
}
],
access_urls=[
{
"provider_url": "altcdn.com.mdc.edgesuite.net",
"domain": "www.test2.com",
"operator_url": "www.test2.com.cdn136.myraxcdn.net"
}
],
status="deployed",
)
}
self.sc.storage_controller.get_service.return_value = service_obj
self.assertRaises(
ValueError,
self.sc.update_access_url_service,
"project_id",
"service_id",
{
'domain_name': 'www.test1.com',
'operator_url': 'www.test1.com.cdn137.myraxcdn.net',
'provider_url': 'altcdn.com.mdc.edgesuite.net'
}
)
def test_update_access_url_shared_ssl_domain(self):
service_obj = service.load_from_json(self.service_json)
service_obj.status = u'deployed'
service_obj.provider_details = {
'Akamai': provider_details.ProviderDetail(
provider_service_id=[
{
"protocol": "https",
"certificate": "shared",
"policy_name": "test99.scdn1.secure.cdn.net"
}
],
access_urls=[
{
"provider_url": "scdn1.secure.cdn.net.edgekey.net",
"domain": "test99.scdn1.secure.cdn.net",
"shared_ssl_flag": True,
"operator_url": "test99.scdn1.secure.cdn.net"
}
],
status="deployed",
)
}
self.sc.storage_controller.get_service.return_value = service_obj
self.assertRaises(
errors.InvalidOperation,
self.sc.update_access_url_service,
"project_id",
"service_id",
{
'domain_name': 'test99.scdn1.secure.cdn.net',
'operator_url': 'test99.scdn2.secure.cdn.net',
'provider_url': 'scdn2.secure.cdn.net.edgekey.net'
}
)