Adds API tests for admin endpoint to migrate SAN domain

Change-Id: Ic71fefc67108e022cd438fda017413fdc108ee1e
This commit is contained in:
Anantha Arunachalam 2015-07-01 16:45:32 -04:00
parent 38061e8e48
commit 20ab7f58de
12 changed files with 556 additions and 1 deletions

View File

@ -46,4 +46,4 @@ format=%(asctime)s %(levelname)s %(message)s
format=(%(name)s): %(asctime)s %(levelname)s %(message)s
[formatter_debug]
format=(%(name)s): %(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s
format=(%(name)s): %(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s

View File

@ -585,3 +585,7 @@ class ServicesController(base.ServicesBase):
self._change_cname_record(operator_url,
provider_url,
shared_ssl_flag)
def modify_cname(self, access_url, new_cert):
self._change_cname_record(access_url=access_url,
target_url=new_cert, shared_ssl_flag=False)

View File

@ -406,3 +406,38 @@ class DefaultServicesController(base.ServicesController):
shared_ssl_domain_suffix = (
self.dns_controller.generate_shared_ssl_domain_suffix())
return '.'.join([domain_name, shared_ssl_domain_suffix])
def migrate_domain(self, project_id, service_id, domain_name, new_cert):
dns_controller = self.dns_controller
storage_controller = self.storage_controller
try:
# Update CNAME records and provider_details in cassandra
provider_details = storage_controller.get_provider_details(
project_id, service_id)
except ValueError as e:
# If service is not found
LOG.warning('Migrating domain failed: Service {0} could not '
'be found.. Error message: {1}'.format(service_id, e))
raise errors.ServiceNotFound(e)
for provider in provider_details:
domain_found = False
for url in provider_details[provider].access_urls:
if url['domain'] == domain_name:
if 'operator_url' in url:
access_url = url['operator_url']
dns_controller.modify_cname(access_url, new_cert)
url['provider_url'] = new_cert
storage_controller.update_provider_details(
project_id,
service_id,
provider_details
)
domain_found = True
break
if not domain_found:
LOG.warning('Migrating domain failed: Domain {0} could not '
'be found.'.format(domain_name))
raise LookupError('Domain {0} could not be found.'.format(
domain_name))

View File

@ -512,6 +512,10 @@ class ServicesController(base.ServicesController):
consistency_level=self._driver.consistency_level)
exec_results = self.session.execute(stmt, args)
if len(exec_results) != 1:
raise ValueError('No service found: %s'
% service_id)
provider_details_result = exec_results[0]['provider_details'] or {}
results = {}
for provider_name in provider_details_result:

View File

@ -18,15 +18,71 @@ import json
import pecan
from pecan import hooks
from poppy.common import errors
from poppy.transport.pecan.controllers import base
from poppy.transport.pecan import hooks as poppy_hooks
from poppy.transport.validators import helpers
from poppy.transport.validators.schemas import domain_migration
from poppy.transport.validators.schemas import service_action
from poppy.transport.validators.stoplight import decorators
from poppy.transport.validators.stoplight import helpers as stoplight_helpers
from poppy.transport.validators.stoplight import rule
class DomainMigrationController(base.Controller, hooks.HookController):
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
def __init__(self, driver):
super(DomainMigrationController, self).__init__(driver)
@pecan.expose('json')
@decorators.validate(
request=rule.Rule(
helpers.json_matches_service_schema(
domain_migration.DomainMigrationServiceSchema.get_schema(
"domain_migration", "POST")),
helpers.abort_with_message,
stoplight_helpers.pecan_getter))
def post(self):
request_json = json.loads(pecan.request.body.decode('utf-8'))
project_id = request_json.get('project_id', None)
service_id = request_json.get('service_id', None)
domain_name = request_json.get('domain_name', None)
new_cert = request_json.get('new_cert', None)
if not helpers.is_valid_domain_name(domain_name):
pecan.abort(400, detail='Domain {0} is not valid'.format(
domain_name))
# Akamai specific suffix:
if not new_cert.endswith("edgekey.net"):
new_cert = new_cert + ".edgekey.net"
try:
self._driver.manager.services_controller.migrate_domain(
project_id, service_id, domain_name, new_cert)
except errors.ServiceNotFound:
pecan.abort(404, detail='Service {0} could not be found'.format(
service_id))
except LookupError:
pecan.abort(404, detail='Domain {0} could not be found'.format(
domain_name))
return pecan.Response(None, 202)
class AkamaiController(base.Controller, hooks.HookController):
def __init__(self, driver):
super(AkamaiController, self).__init__(driver)
self.__class__.service = DomainMigrationController(driver)
class ProviderController(base.Controller, hooks.HookController):
def __init__(self, driver):
super(ProviderController, self).__init__(driver)
self.__class__.akamai = AkamaiController(driver)
class OperatorServiceActionController(base.Controller, hooks.HookController):
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
@ -71,3 +127,4 @@ class AdminController(base.Controller, hooks.HookController):
def __init__(self, driver):
super(AdminController, self).__init__(driver)
self.__class__.services = AdminServiceController(driver)
self.__class__.provider = ProviderController(driver)

View File

@ -0,0 +1,50 @@
# 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 DomainMigrationServiceSchema(schema_base.SchemaBase):
'''JSON Schmema validation for /admin/provider/akamai/service'''
schema = {
'domain_migration': {
'POST': {
'type': 'object',
'additionalProperties': False,
'properties': {
'project_id': {
'type': 'string',
'required': True
},
'service_id': {
'type': 'string',
'required': True
},
'domain_name': {
'type': 'string',
'required': True,
'minLength': 3,
'maxLength': 253
},
'new_cert': {
'type': 'string',
'required': True
}
}
}
}
}

View File

@ -0,0 +1,291 @@
# coding= utf-8
# 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.
import ddt
import random
from tests.api import base
@ddt.ddt
class TestSanCertService(base.TestBase):
def setUp(self):
super(TestSanCertService, self).setUp()
self.service_name = self.generate_random_string(prefix='API-Test-')
self.flavor_id = self.test_flavor
domain = self.generate_random_string(
prefix='api-test-domain') + '.com'
self.domain_list = [
{"domain": domain, "protocol": "https", "certificate": "san"}
]
origin = self.generate_random_string(
prefix='api-test-origin') + u'.com'
self.origin_list = [
{
u"origin": origin,
u"port": 443,
u"ssl": True,
u"rules": [{
u"name": u"default",
u"request_url": u"/*"
}]
}
]
self.caching_list = [
{
u"name": u"default",
u"ttl": 3600,
u"rules": [{
u"name": "default",
u"request_url": "/*"
}]
},
{
u"name": u"home",
u"ttl": 1200,
u"rules": [{
u"name": u"index",
u"request_url": u"/index.htm"
}]
}
]
self.restrictions_list = [
{
u"name": u"website only",
u"rules": [
{
u"name": domain,
u"referrer": domain,
u"request_url": "/*"
}
]
}
]
resp = self.setup_service(
service_name=self.service_name,
domain_list=self.domain_list,
origin_list=self.origin_list,
caching_list=self.caching_list,
restrictions_list=self.restrictions_list,
flavor_id=self.flavor_id)
self.service_url = resp.headers["location"]
def test_migrate(self):
new_certs = self.akamai_config.san_certs
new_certs_list = new_certs.split(',')
index = random.randint(0, len(new_certs_list)-1)
new_cert = new_certs_list[index]
get_resp = self.client.get_service(location=self.service_url)
get_resp_body = get_resp.json()
domain = get_resp_body['domains'][0]['domain']
resp = self.client.admin_migrate_domain(
project_id=self.user_project_id, service_id=get_resp_body['id'],
domain=domain, new_cert=new_cert)
self.assertEqual(resp.status_code, 202)
new_resp = self.client.get_service(location=self.service_url)
new_resp_body = new_resp.json()
for link in new_resp_body['links']:
if link['rel'] == 'access_url':
access_url = link['href']
dns_suffix = str(self.dns_config.dns_url_suffix)
akamai_access_url_suffix = str(self.akamai_config.access_url_suffix)
data = self.dns_client.verify_domain_migration(access_url=access_url,
suffix=dns_suffix)
# Akamai specific suffix
if not new_cert.endswith(akamai_access_url_suffix):
new_cert = new_cert + "." + akamai_access_url_suffix
self.assertEqual(data, new_cert)
def test_migrate_negative_invalid_projectid(self):
new_certs = self.akamai_config.san_certs
new_certs_list = new_certs.split(',')
index = random.randint(0, len(new_certs_list)-1)
new_cert = new_certs_list[index]
get_resp = self.client.get_service(location=self.service_url)
get_resp_body = get_resp.json()
domain = get_resp_body['domains'][0]['domain']
project_id = self.generate_random_string(prefix=self.user_project_id)
resp = self.client.admin_migrate_domain(
project_id=project_id, service_id=get_resp_body['id'],
domain=domain, new_cert=new_cert)
self.assertEqual(resp.status_code, 404)
def test_migrate_negative_invalid_serviceid(self):
new_certs = self.akamai_config.san_certs
new_certs_list = new_certs.split(',')
index = random.randint(0, len(new_certs_list)-1)
new_cert = new_certs_list[index]
get_resp = self.client.get_service(location=self.service_url)
get_resp_body = get_resp.json()
domain = get_resp_body['domains'][0]['domain']
service_id = self.generate_random_string(prefix=get_resp_body['id'])
resp = self.client.admin_migrate_domain(
project_id=self.user_project_id, service_id=service_id,
domain=domain, new_cert=new_cert)
self.assertEqual(resp.status_code, 404)
def test_migrate_negative_invalid_domain(self):
new_certs = self.akamai_config.san_certs
new_certs_list = new_certs.split(',')
index = random.randint(0, len(new_certs_list)-1)
new_cert = new_certs_list[index]
get_resp = self.client.get_service(location=self.service_url)
get_resp_body = get_resp.json()
domain = "1234"
service_id = self.generate_random_string(prefix=get_resp_body['id'])
resp = self.client.admin_migrate_domain(
project_id=self.user_project_id, service_id=service_id,
domain=domain, new_cert=new_cert)
self.assertEqual(resp.status_code, 400)
def tearDown(self):
self.client.delete_service(location=self.service_url)
if self.test_config.generate_flavors:
self.client.delete_flavor(flavor_id=self.flavor_id)
super(TestSanCertService, self).tearDown()
@ddt.ddt
class TestSanCertServiceWithLogDelivery(base.TestBase):
def setUp(self):
super(TestSanCertServiceWithLogDelivery, self).setUp()
self.service_name = self.generate_random_string(prefix='API-Test-')
self.flavor_id = self.test_flavor
domain = self.generate_random_string(
prefix='api-test-domain') + '.com'
self.domain_list = [
{"domain": domain, "protocol": "https", "certificate": "san"}
]
origin = self.generate_random_string(
prefix='api-test-origin') + u'.com'
self.origin_list = [
{
u"origin": origin,
u"port": 443,
u"ssl": True,
u"rules": [{
u"name": u"default",
u"request_url": u"/*"
}]
}
]
self.caching_list = [
{
u"name": u"default",
u"ttl": 3600,
u"rules": [{
u"name": "default",
u"request_url": "/*"
}]
},
{
u"name": u"home",
u"ttl": 1200,
u"rules": [{
u"name": u"index",
u"request_url": u"/index.htm"
}]
}
]
self.restrictions_list = [
{
u"name": u"website only",
u"rules": [
{
u"name": domain,
u"referrer": domain,
u"request_url": "/*"
}
]
}
]
self.log_delivery = {"enabled": True}
resp = self.setup_service(
service_name=self.service_name,
domain_list=self.domain_list,
origin_list=self.origin_list,
caching_list=self.caching_list,
restrictions_list=self.restrictions_list,
flavor_id=self.flavor_id,
log_delivery=self.log_delivery)
self.service_url = resp.headers["location"]
def test_migrate(self):
new_certs = self.akamai_config.san_certs
new_certs_list = new_certs.split(',')
index = random.randint(0, len(new_certs_list)-1)
new_cert = new_certs_list[index]
get_resp = self.client.get_service(location=self.service_url)
get_resp_body = get_resp.json()
domain = get_resp_body['domains'][0]['domain']
resp = self.client.admin_migrate_domain(
project_id=self.user_project_id, service_id=get_resp_body['id'],
domain=domain, new_cert=new_cert)
self.assertEqual(resp.status_code, 202)
new_resp = self.client.get_service(location=self.service_url)
new_resp_body = new_resp.json()
for link in new_resp_body['links']:
if link['rel'] == 'access_url':
access_url = link['href']
dns_suffix = str(self.dns_config.dns_url_suffix)
akamai_access_url_suffix = str(self.akamai_config.access_url_suffix)
data = self.dns_client.verify_domain_migration(access_url=access_url,
suffix=dns_suffix)
# Akamai specific suffix
if not new_cert.endswith(akamai_access_url_suffix):
new_cert = new_cert + "." + akamai_access_url_suffix
self.assertEqual(str(data), str(new_cert))
def tearDown(self):
self.client.delete_service(location=self.service_url)
if self.test_config.generate_flavors:
self.client.delete_flavor(flavor_id=self.flavor_id)
super(TestSanCertServiceWithLogDelivery, self).tearDown()

View File

@ -97,6 +97,12 @@ class TestBase(fixtures.BaseTestFixture):
serialize_format='json',
deserialize_format='json')
cls.dns_config = config.DNSConfig()
cls.dns_client = client.DNSClient(cls.dns_config.dns_username,
cls.dns_config.dns_api_key)
cls.akamai_config = config.AkamaiConfig()
def generate_random_string(self, prefix='API-Tests', length=12):
"""Generates a random string of given prefix & length"""
random_string = ''.join(random.choice(

View File

@ -16,6 +16,7 @@
# limitations under the License.
import json
import pyrax
import time
from cafe.engine.http import client
@ -56,6 +57,31 @@ class AuthClient(client.HTTPClient):
return token, project_id
class DNSClient(client.HTTPClient):
def __init__(self, username, api_key):
super(DNSClient, self).__init__()
self.username = username
self.api_key = api_key
pyrax.set_setting('identity_type', 'rackspace')
pyrax.set_credentials(self.username,
self.api_key)
def verify_domain_migration(self, access_url, suffix):
# 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])
# get subdomain
subdomain = pyrax.cloud_dns.find(name=subdomain_name)
# search and find the CNAME record
name = access_url
record_type = 'CNAME'
records = pyrax.cloud_dns.search_records(subdomain, record_type, name)
return records[0].data
class PoppyClient(client.AutoMarshallingHTTPClient):
"""Client objects for all the Poppy api calls."""
@ -172,6 +198,22 @@ class PoppyClient(client.AutoMarshallingHTTPClient):
return self.request('POST', url, request_entity=request_object,
requestslib_kwargs=requestslib_kwargs)
def admin_migrate_domain(self, project_id, service_id, domain, new_cert,
requestslib_kwargs=None):
"""Update SAN domain
:return: Response Object containing response code 202
POST
/admin/provider/akamai/service
"""
url = '{0}/admin/provider/akamai/service'.format(self.url)
request_object = requests.MigrateDomain(
project_id=project_id, service_id=service_id, domain=domain,
new_cert=new_cert)
return self.request('POST', url, request_entity=request_object,
requestslib_kwargs=requestslib_kwargs)
def check_health(self):
"""Check Health of the application

View File

@ -88,6 +88,26 @@ class TestConfig(data_interfaces.ConfigSectionInterface):
return int(self.get('cassandra_consistency_wait_time'))
class DNSConfig(data_interfaces.ConfigSectionInterface):
"""Defines the values for DNS configuration."""
SECTION_NAME = 'dns'
@property
def dns_username(self):
"""The user name for the Cloud DNS service"""
return self.get('dns_username')
@property
def dns_api_key(self):
"""The API Key for the Cloud DNS service"""
return self.get('dns_api_key')
@property
def dns_url_suffix(self):
"""The url for customers to CNAME to."""
return self.get('dns_url_suffix')
class AuthConfig(data_interfaces.ConfigSectionInterface):
"""Defines the auth config values."""
SECTION_NAME = 'auth'
@ -138,6 +158,21 @@ class AuthConfig(data_interfaces.ConfigSectionInterface):
return self.get_raw('operator_api_key')
class AkamaiConfig(data_interfaces.ConfigSectionInterface):
"""Defines the Akamai config values."""
SECTION_NAME = 'provider_akamai'
@property
def access_url_suffix(self):
"""The access URL suffix for Akamai"""
return self.get('access_url_suffix')
@property
def san_certs(self):
"""A list of SAN certificates from Akamai"""
return self.get('san_certs')
class FastlyConfig(data_interfaces.ConfigSectionInterface):
"""Defines the fastly config values."""
SECTION_NAME = 'fastly'

View File

@ -73,6 +73,27 @@ class ServiceAction(base.AutoMarshallingModel):
return json.dumps(service_action_request)
class MigrateDomain(base.AutoMarshallingModel):
"""Marshalling for Action on Services requests."""
def __init__(self, project_id, service_id, domain, new_cert):
super(MigrateDomain, self).__init__()
self.project_id = project_id
self.service_id = service_id
self.domain = domain
self.new_cert = new_cert
def _obj_to_json(self):
service_action_request = {
"project_id": self.project_id,
"service_id": self.service_id,
"domain_name": self.domain,
"new_cert": self.new_cert
}
return json.dumps(service_action_request)
class CreateFlavor(base.AutoMarshallingModel):
"""Marshalling for Create Flavor requests."""

View File

@ -31,6 +31,16 @@ project_id_in_url=False
run_ssl_tests=False
cassandra_consistency_wait_time=5
[dns]
dns_username={user name of the cloud account}
dns_api_key={api key for this user name}
dns_url_suffix={the suffix for the DNS shards}
[provider_akamai]
access_url_suffix = edgekey.net
san_certs = secure1.san2.xyzcdn.com,domain2.san3.xyz.com,cert5.san9.abc.com
[provider_1]
api_key=INSERT_YOUR_API_KEY
email_id=account_email_id