Implementing Akamai Driver

Change-Id: I822c0248995be0af3a880445b113225d26435698
This commit is contained in:
tonytan4ever 2014-10-22 12:13:48 -04:00
parent 6c938bc8e7
commit 89552af743
21 changed files with 1110 additions and 56 deletions

View File

@ -92,3 +92,14 @@ consumer_key = "MYCONSUMERKEY"
[drivers:provider:cloudfront]
aws_access_key_id = "MY_AWS_ACCESS_KEY_ID"
aws_secret_access_key = "MY_AWS_SECRET_ACCESS_KEY"
[drivers:provider:akamai]
policy_api_client_token = "MY_POLICY_API_CLIENT_TOKEN"
policy_api_client_secret = "MY_POLICY_API_CLIENT_SECRET"
policy_api_access_token = "MY_POLICY_API_ACCESS_TOKEN"
policy_api_base_url = "MY_POLICY_API_BASE_URL"
ccu_api_client_token = "MY_CCU_API_CLIENT_TOKEN"
ccu_api_client_secret = "MY_CCU_API_CLIENT_SECRET"
ccu_api_access_token = "MY_CCU_API_ACCESS_TOKEN"
ccu_api_base_url = "MY_CCU_API_BASE_URL"
akamai_access_url_link = "MY_ACCESS_URL_LINK"

View File

@ -19,11 +19,11 @@ from poppy.model import common
class Origin(common.DictSerializableModel):
"""Origin."""
def __init__(self, origin, port=80, ssl=False):
def __init__(self, origin, port=80, ssl=False, rules=[]):
self._origin = origin
self._port = port
self._ssl = ssl
self._rules = []
self._rules = rules
@property
def origin(self):
@ -90,3 +90,10 @@ class Origin(common.DictSerializableModel):
o.port = dict_obj.get("port", 80)
o.ssl = dict_obj.get("ssl", False)
return o
def to_dict(self):
result = common.DictSerializableModel.to_dict(self)
# need to deserialize the nested rules object
rules_obj_list = result['rules']
result['rules'] = [r.to_dict() for r in rules_obj_list]
return result

View File

@ -0,0 +1,19 @@
# Copyright (c) 2014 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.provider.akamai import driver
# Hoist classes into package namespace
Driver = driver.CDNProvider

View File

@ -0,0 +1,27 @@
# Copyright (c) 2013 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.
"""Exports Akamai poppy controllers.
Field Mappings:
In order to reduce the disk / memory space used,
fields name will be, most of the time, the first
letter of their long name. Fields mapping will be
updated and documented in each controller class.
"""
from poppy.provider.akamai import services
ServiceController = services.ServiceController

View File

@ -0,0 +1,107 @@
# Copyright (c) 2014 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.
"""Akamai CDN Provider implementation."""
from akamai import edgegrid
from oslo.config import cfg
import requests
from poppy.provider.akamai import controllers
from poppy.provider import base
AKAMAI_OPTIONS = [
# credentials && base URL for policy API
cfg.StrOpt(
'policy_api_client_token',
help='Akamai client token for policy API'),
cfg.StrOpt(
'policy_api_client_secret',
help='Akamai client secret for policy API'),
cfg.StrOpt(
'policy_api_access_token',
help='Akamai access token for policy API'),
cfg.StrOpt(
'policy_api_base_url',
help='Akamai policy API base URL'),
# credentials && base URL for CCU API
# for purging
cfg.StrOpt(
'ccu_api_client_token',
help='Akamai client token for CCU API'),
cfg.StrOpt(
'ccu_api_client_secret',
help='Akamai client secret for CCU API'),
cfg.StrOpt(
'ccu_api_access_token',
help='Akamai access token for CCU API'),
cfg.StrOpt(
'ccu_api_base_url',
help='Akamai CCU Purge API base URL'),
# Access URL in Akamai chain
cfg.StrOpt(
'akamai_access_url_link',
help='Akamai domain access_url link'),
]
AKAMAI_GROUP = 'drivers:provider:akamai'
class CDNProvider(base.Driver):
def __init__(self, conf):
super(CDNProvider, self).__init__(conf)
self._conf.register_opts(AKAMAI_OPTIONS,
group=AKAMAI_GROUP)
self.akamai_conf = self._conf[AKAMAI_GROUP]
self.akamai_policy_api_base_url = self.akamai_conf.policy_api_base_url
self.akamai_ccu_api_base_url = (
self.akamai_conf.ccu_api_base_url)
self.akamai_access_url_link = self.akamai_conf.akamai_access_url_link
self.akamai_policy_api_client = requests.Session()
self.akamai_policy_api_client.auth = edgegrid.EdgeGridAuth(
client_token=self.akamai_conf.policy_api_client_token,
client_secret=self.akamai_conf.policy_api_client_secret,
access_token=self.akamai_conf.policy_api_access_token
)
self.akamai_ccu_api_client = requests.Session()
self.akamai_ccu_api_client.auth = edgegrid.EdgeGridAuth(
client_token=self.akamai_conf.ccu_api_client_token,
client_secret=self.akamai_conf.ccu_api_client_secret,
access_token=self.akamai_conf.ccu_api_access_token
)
def is_alive(self):
return True
@property
def provider_name(self):
return "Akamai"
@property
def policy_api_client(self):
return self.akamai_policy_api_client
@property
def ccu_api_client(self):
return self.akamai_ccu_api_client
@property
def service_controller(self):
"""Returns the driver's hostname controller."""
return controllers.ServiceController(self)

View File

@ -0,0 +1,340 @@
# Copyright (c) 2014 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 json
from poppy.common import decorators
from poppy.openstack.common import log
from poppy.provider import base
LOG = log.getLogger(__name__)
class ServiceController(base.ServiceBase):
@property
def policy_api_client(self):
return self.driver.policy_api_client
@property
def ccu_api_client(self):
return self.driver.ccu_api_client
def __init__(self, driver):
super(ServiceController, self).__init__(driver)
self.driver = driver
self.policy_api_base_url = self.driver.akamai_policy_api_base_url
self.ccu_api_base_url = self.driver.akamai_ccu_api_base_url
self.request_header = {'Content-type': 'application/json',
'Accept': 'text/plain'}
def create(self, service_obj):
post_data = {
'rules': []
}
# for now global level akamai only supports 1 origin match global
# * url. At this point we are guaranteed there is at least 1 origin
# in incoming service_obj
# form all origin rules for this service
for origin in service_obj.origins:
self._process_new_origin(origin, post_data['rules'])
classified_domains = self._classify_domains(service_obj.domains)
try:
# NOTE(tonytan4ever): for akamai it might be possible to have
# multiple policies associated with one poppy service, so we use
# a list to represent provide_detail id
ids = []
links = []
for classified_domain in classified_domains:
# assign the content realm to be the digital property field
# of each group
dp = self._process_new_domain(classified_domain,
post_data['rules'])
resp = self.policy_api_client.put(
self.policy_api_base_url.format(
policy_name=dp),
data=json.dumps(post_data),
headers=self.request_header)
LOG.info('akamai response code: %s' % resp.status_code)
LOG.info('akamai response text: %s' % resp.text)
if resp.status_code != 200:
raise RuntimeError(resp.text)
ids.append(dp)
# TODO(tonytan4ever): leave empty links for now
# may need to work with dns integration
LOG.info('Creating policy %s on domain %s complete' %
(dp, ','.join(classified_domain)))
links.append({'href': self.driver.akamai_access_url_link,
"rel": 'access_url'
})
except Exception:
return self.responder.failed("failed to create service")
else:
return self.responder.created(json.dumps(ids), links)
def _classify_domains(self, domains_list):
# classify domains into different categories based on first two level
# of domains, group them together
result_dict = {}
for domain in domains_list:
# get the content_realm (1st and 2nd level domain of each domains)
content_realm = '.'.join(domain.domain.split('.')[-2:])
if content_realm not in result_dict:
result_dict[content_realm] = [domain.domain]
else:
result_dict[content_realm].append(domain.domain)
return [domain_mapping[1] for domain_mapping in result_dict.items()]
def _process_new_origin(self, origin, rules_list):
rule_dict_template = {
'matches': [],
'behaviors': []
}
origin_behavior_dict = {
'name': 'origin',
'value': '-',
'params': {
# missing digitalProperty(domain) for now
'originDomain': '',
'hostHeaderType': 'digital_property',
'cacheKeyType': 'origin',
'hostHeaderValue': '-',
'cacheKeyValue': '-'
}
}
# this is the global 'url-wildcard' rule
if origin.rules == []:
match_rule = {
'name': 'url-wildcard',
'value': '/*'
}
rule_dict_template['matches'].append(match_rule)
else:
for rule in origin.rules:
match_rule = {
'name': 'url-path',
'value': rule.request_url
}
rule_dict_template['matches'].append(
match_rule)
if origin.ssl:
rule_dict_template['matches'].append({
'name': 'url-scheme',
'value': 'HTTPS'
})
origin_behavior_dict['params']['originDomain'] = (
origin.origin
)
rule_dict_template['behaviors'].append(
origin_behavior_dict
)
# Append the new generated rules
rules_list.append(rule_dict_template)
def _process_new_domain(self, domain, rules_list):
dp = '.'.join(domain[0].split('.')[-2:])
for rule in rules_list:
for behavior in rule['behaviors']:
behavior['params']['digitalProperty'] = dp
return dp
def get(self, service_name):
pass
def update(self, provider_service_id,
service_old,
service_updates,
service_obj):
# depending on domains field presented or not, do PUT/POST
# and depending on origins field presented or not, set behavior on
# the data or not
try:
# get a list of policies
policies = json.loads(provider_service_id)
except Exception:
# raise a more meaningful error for debugging info
try:
raise RuntimeError('Mal-formed Akaimai policy ids: %s' %
provider_service_id)
except Exception:
return self.responder.failed("failed to update service")
ids = []
links = []
if len(service_obj.domains) > 0:
# in this case we need to copy
# and tweak the content of one old policy
# and creates new policy for the new domains,
# old policies ought to be deleted.
try:
resp = self.policy_api_client.get(
self.policy_api_base_url.format(
policy_name=policies[0]),
headers=self.request_header)
if resp.status_code != 200:
raise RuntimeError(resp.text)
except Exception:
return self.responder.failed("failed to update service")
else:
policy_content = json.loads(resp.text)
# Update origin if necessary
if len(service_obj.origins) > 0:
policy_content['rules'] = []
for origin in service_obj.origins:
self._process_new_origin(origin, policy_content['rules'])
else:
# to incorporate caching rule or referrer restriciton
# if necessary
pass
# Update domain if necessary ( by adjust digital property)
classified_domains = self._classify_domains(service_obj.domains)
try:
for classified_domain in classified_domains:
# assign the content realm to be the digital property field
# of each group
dp = self._process_new_domain(classified_domain,
policy_content['rules'])
if dp in policies:
# in this case we should update existing policy
# instead of create a new policy
LOG.info('Start to update policy %s' % dp)
resp = self.policy_api_client.put(
self.policy_api_base_url.format(
policy_name=dp),
data=json.dumps(policy_content),
headers=self.request_header)
policies.remove(dp)
else:
LOG.info('Start to create new policy %s' % dp)
resp = self.policy_api_client.put(
self.policy_api_base_url.format(
policy_name=dp),
data=json.dumps(policy_content),
headers=self.request_header)
LOG.info('akamai response code: %s' % resp.status_code)
LOG.info('akamai response text: %s' % resp.text)
if resp.status_code != 200:
raise RuntimeError(resp.text)
ids.append(classified_domain[0])
# TODO(tonytan4ever): leave empty links for now
# may need to work with dns integration
LOG.info('Creating/Updateing policy %s on domain %s '
'complete' % (dp, ','.join(classified_domain)))
links.append({'href': self.driver.akamai_access_url_link,
'rel': 'access_url'
})
except Exception:
return self.responder.failed("failed to update service")
try:
for policy in policies:
LOG.info('Starting to delete old policy %s' % policy)
resp = self.policy_api_client.delete(
self.policy_api_base_url.format(
policy_name=policy))
LOG.info('akamai response code: %s' % resp.status_code)
LOG.info('akamai response text: %s' % resp.text)
if resp.status_code != 200:
raise RuntimeError(resp.text)
LOG.info('Delete old policy %s complete' % policy)
except Exception:
return self.responder.failed("failed to update service")
else:
# in this case we only need to adjust the existing policies
for policy in policies:
try:
resp = self.policy_api_client.get(
self.policy_api_base_url.format(policy_name=policy),
headers=self.request_header)
if resp.status_code != 200:
raise RuntimeError(resp.text)
except Exception:
return self.responder.failed("failed to update service")
else:
policy_content = json.loads(resp.text)
if len(service_obj.origins) > 0:
policy_content['rules'] = []
for origin in service_obj.origins:
self._process_new_origin(origin,
policy_content['rules'])
else:
# TODO(tonytan4ever):
# to incorporate caching rule and
# referrer restriciton if necessary
pass
# post new policies back with Akamai Policy API
try:
LOG.info('Start to update policy %s ' % policy)
resp = self.policy_api_client.put(
self.policy_api_base_url.format(
policy_name=policy),
data=json.dumps(policy_content),
headers=self.request_header)
LOG.info('akamai response code: %s' % resp.status_code)
LOG.info('akamai response text: %s' % resp.text)
LOG.info('Update policy %s complete' % policy)
except Exception:
return self.responder.failed("failed to update service")
ids = policies
links.append({'href': self.driver.akamai_access_url_link,
'rel': 'access_url'})
return self.responder.updated(json.dumps(ids), links)
def delete(self, provider_service_id):
# delete needs to provide a list of policy id/domains
# then delete them accordingly
try:
policies = json.loads(provider_service_id)
except Exception:
# raise a more meaningful error for debugging info
try:
raise RuntimeError('Mal-formed Akaimai policy ids: %s' %
provider_service_id)
except Exception:
return self.responder.failed("failed to delete service")
try:
for policy in policies:
LOG.info('Starting to delete policy %s' % policy)
resp = self.policy_api_client.delete(
self.policy_api_base_url.format(policy_name=policy))
LOG.info('akamai response code: %s' % resp.status_code)
LOG.info('akamai response text: %s' % resp.text)
if resp.status_code != 200:
raise RuntimeError(resp.text)
except Exception:
return self.responder.failed("failed to delete service")
else:
return self.responder.deleted(provider_service_id)
def purge(self, service_id, purge_urls=None):
pass
@decorators.lazy_property(write=False)
def current_customer(self):
return None

View File

@ -1,3 +1,18 @@
# Copyright (c) 2013 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.
"""Fastly CDN Extension for CDN"""
from poppy.provider.fastly import driver

View File

@ -130,6 +130,7 @@ CQL_UPDATE_PROVIDER_DETAILS = '''
class ServicesController(base.ServicesController):
"""Services Controller."""
@property
@ -342,10 +343,16 @@ class ServicesController(base.ServicesController):
name = result.get('service_name')
origins = [json.loads(o) for o in result.get('origins', [])]
domains = [json.loads(d) for d in result.get('domains', [])]
origins = [origin.Origin(o['origin'],
o.get('port', 80),
o.get('ssl', False))
for o in origins]
origins = [
origin.Origin(
o['origin'], o.get(
'port', 80), o.get(
'ssl', False), [
rule.Rule(
rule_i.get('name'),
request_url=rule_i.get('request_url'))
for rule_i in o.get(
'rules', [])]) for o in origins]
domains = [domain.Domain(d['domain']) for d in domains]
flavor_ref = result.get('flavor_id')

View File

@ -14,10 +14,15 @@
# limitations under the License.
from poppy.model.helpers import origin
from poppy.transport.pecan.models.request import rule
def load_from_json(json_data):
origin_name = json_data.get("origin")
port = json_data.get("port", 80)
ssl = json_data.get("ssl", False)
return origin.Origin(origin_name, port, ssl)
rules = json_data.get("rules", [])
rules = [rule.load_from_json(r) for r in rules]
result = origin.Origin(origin_name, port, ssl)
result.rules = rules
return result

View File

@ -18,6 +18,8 @@ try:
except ImportError: # pragma: no cover
import collections # pragma: no cover
from poppy.transport.pecan.models.response import rule
class Model(collections.OrderedDict):
@ -28,3 +30,4 @@ class Model(collections.OrderedDict):
self['origin'] = origin.origin
self['port'] = origin.port
self['ssl'] = origin.ssl
self['rules'] = [rule.Model(r) for r in origin.rules]

View File

@ -54,7 +54,10 @@ class ServiceSchema(schema_base.SchemaBase):
'minItems': 1},
'origins': {
'type': 'array',
'items': {
# the first origin does not have to
# have rules field, it will be defaulted
# to global url matching
'items': [{
'type': 'object',
'properties': {
'origin': {
@ -73,10 +76,69 @@ class ServiceSchema(schema_base.SchemaBase):
80,
443]},
'ssl': {
'type': 'boolean'}},
},
'required': True,
'minItems': 1},
'type': 'boolean'},
'rules': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'required': True
},
'request_url': {
'type': 'string',
'required': True
}
}
}
}
}
}],
'minItems': 1,
# the 2nd and successive items must have
# 'rules' field which has at least one rule
"additionalItems": {
'type': 'object',
'properties': {
'origin': {
'type': 'string',
'pattern': re.compile(
'^(([^:/?#]+):)?'
'(//([^/?#]*))?'
'([^?#]*)(\?([^#]*))?'
'(#(.*))?$',
re.UNICODE
),
'required': True},
'port': {
'type': 'integer',
'enum': [
80,
443]},
'ssl': {
'type': 'boolean'},
'rules': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'required': True
},
'request_url': {
'type': 'string',
'required': True
}
}
},
'required': True,
'minItems': 1,
},
}
}
},
'caching': {
'type': 'array',
'required': False,
@ -199,8 +261,10 @@ class ServiceSchema(schema_base.SchemaBase):
'origins': {
'type': 'array',
'required': False,
'minItems': 1,
'items': {
# the first origin does not have to
# have rules field, it will be defaulted
# to global url matching
'items': [{
'type': 'object',
'properties': {
'origin': {
@ -219,8 +283,68 @@ class ServiceSchema(schema_base.SchemaBase):
80,
443]},
'ssl': {
'type': 'boolean'}},
},
'type': 'boolean'},
'rules': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'required': True
},
'request_url': {
'type': 'string',
'required': True
}
}
}
}
}
}],
'minItems': 1,
# the 2nd and successive items must have
# 'rules' field which has at least one rule
"additionalItems": {
'type': 'object',
'properties': {
'origin': {
'type': 'string',
'pattern': re.compile(
'^(([^:/?#]+):)?'
'(//([^/?#]*))?'
'([^?#]*)(\?([^#]*))?'
'(#(.*))?$',
re.UNICODE
),
'required': True},
'port': {
'type': 'integer',
'enum': [
80,
443]},
'ssl': {
'type': 'boolean'},
'rules': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'required': True
},
'request_url': {
'type': 'string',
'required': True
}
}
},
'required': True,
'minItems': 1,
},
}
}
},
'caching': {
'type': 'array',

View File

@ -0,0 +1 @@
edgegrid-python

View File

@ -2,6 +2,7 @@
-r docs.txt
-r transport/pecan.txt
-r storage/cassandra.txt
-r provider/akamai.txt
-r provider/cloudfront.txt
-r provider/fastly.txt
-r provider/maxcdn.txt

View File

@ -54,6 +54,7 @@ poppy.provider =
mock = poppy.provider.mock:Driver
cloudfront = poppy.provider.cloudfront:Driver
maxcdn = poppy.provider.maxcdn:Driver
akamai = poppy.provider.akamai:Driver
[wheel]
universal = 1

View File

@ -31,7 +31,7 @@ def abort(code):
@decorators.validation_function
def is_valid_json(r):
"""Test for a valid JSON string."""
'''Test for a valid JSON string.'''
if len(r.body) == 0:
return
else:
@ -48,29 +48,33 @@ class DummyRequest(object):
def __init__(self):
self.headers = dict(header1='headervalue1')
self.method = "POST"
self.method = 'POST'
self.body = json.dumps({
"name": "fake_service_name",
"domains": [
{"domain": "www.mywebsite.com"},
{"domain": "blog.mywebsite.com"},
'name': 'fake_service_name',
'domains': [
{'domain': 'www.mywebsite.com'},
{'domain': 'blog.mywebsite.com'},
],
"origins": [
'origins': [
{
"origin": "mywebsite.com",
"port": 80,
"ssl": False
'origin': 'mywebsite.com',
'port': 80,
'ssl': False
},
{
"origin": "mywebsite.com",
'origin': 'mywebsite.com',
'rules': [{
'name': 'img',
'request_url': '/img'
}]
}
],
"caching": [
{"name": "default", "ttl": 3600},
{"name": "home",
"ttl": 17200,
"rules": [
{"name": "index", "request_url": "/index.htm"}
'caching': [
{'name': 'default', 'ttl': 3600},
{'name': 'home',
'ttl': 17200,
'rules': [
{'name': 'index', 'request_url': '/index.htm'}
]
},
{"name": "images",
@ -80,7 +84,7 @@ class DummyRequest(object):
]
}
],
"flavor_ref": "https://www.poppycdn.io/v1.0/flavors/standard"
'flavor_ref': 'https://www.poppycdn.io/v1.0/flavors/standard'
})
@ -96,38 +100,38 @@ class DummyRequestWithInvalidHeader(DummyRequest):
fake_request_good = DummyRequest()
fake_request_bad_missing_domain = DummyRequest()
fake_request_bad_missing_domain.body = json.dumps({
"name": "fake_service_name",
"origins": [
'name': 'fake_service_name',
'origins': [
{
"origin": "mywebsite.com",
"port": 80,
"ssl": False
'origin': 'mywebsite.com',
'port': 80,
'ssl': False
}
],
"caching": [
{"name": "default", "ttl": 3600},
{"name": "home",
"ttl": 17200,
"rules": [
{"name": "index", "request_url": "/index.htm"}
'caching': [
{'name': 'default', 'ttl': 3600},
{'name': 'home',
'ttl': 17200,
'rules': [
{'name': 'index', 'request_url': '/index.htm'}
]
},
{"name": "images",
"ttl": 12800,
"rules": [
{"name": "images", "request_url": "*.png"}
{'name': 'images',
'ttl': 12800,
'rules': [
{'name': 'images', 'request_url': '*.png'}
]
}
],
"flavor_ref": "https://www.poppycdn.io/v1.0/flavors/standard"
'flavor_ref': 'https://www.poppycdn.io/v1.0/flavors/standard'
})
fake_request_bad_invalid_json_body = DummyRequest()
fake_request_bad_invalid_json_body.body = "{"
fake_request_bad_invalid_json_body.body = '{'
class _AssertRaisesContext(object):
"""A context manager used to implement TestCase.assertRaises* methods."""
'''A context manager used to implement TestCase.assertRaises* methods.'''
def __init__(self, expected, test_case, expected_regexp=None):
self.expected = expected
@ -144,7 +148,7 @@ class _AssertRaisesContext(object):
except AttributeError:
exc_name = str(self.expected)
raise self.failureException(
"{0} not raised".format(exc_name))
'{0} not raised'.format(exc_name))
if not issubclass(exc_type, self.expected):
return False # let unexpected exceptions pass through
self.exception = exc_value # store for later retrieval
@ -170,7 +174,7 @@ class _AssertRaisesContext(object):
class BaseTestCase(base.TestCase):
def assertRaises(self, excClass, callableObj=None, *args, **kwargs):
"""Check the expected Exception is raised.
'''Check the expected Exception is raised.
Fail unless an exception of class excClass is raised
by callableObj when invoked with arguments args and keyword
@ -193,7 +197,7 @@ class BaseTestCase(base.TestCase):
do_something()
the_exception = cm.exception
self.assertEqual(the_exception.error_code, 3)
"""
'''
context = _AssertRaisesContext(excClass, self)
if callableObj is None:
return context
@ -202,7 +206,7 @@ class BaseTestCase(base.TestCase):
def assertRaisesRegexp(self, expected_exception, expected_regexp,
callable_obj=None, *args, **kwargs):
"""Asserts that the message in a raised exception matches a regexp."""
'''Asserts that the message in a raised exception matches a regexp.'''
context = _AssertRaisesContext(expected_exception, self,
expected_regexp)
if callable_obj is None:

View File

View File

@ -0,0 +1,44 @@
{
"single_one_origin": {
"name" : "mysite.com",
"domains": [
{"domain": "parsely.sage.com"},
{"domain": "densely.sage.com"},
{"domain": "rosemary.thyme.net"}
],
"origins": [
{"origin": "mockdomain.com", "ssl": false, "port": 80}
],
"flavor_ref" : "standard"
},
"multiple_origins": {
"name" : "mysite.com",
"domains": [
{"domain": "parsely.sage.com"},
{"domain": "densely.sage.com"},
{"domain": "rosemary.thyme.net"}
],
"origins": [
{"origin": "mockdomain.com", "ssl": false, "port": 80},
{"origin": "mockdomain-image.com",
"rules": [{"name": "img", "request_url": "/img"}] }
],
"flavor_ref" : "standard"
},
"multiple_origins_complicated": {
"name" : "mysite.com",
"domains": [
{"domain": "parsely.sage.com"},
{"domain": "densely.sage.com"},
{"domain": "rosemary.thyme.net"}
],
"origins": [
{"origin": "mockdomain-text.com", "ssl": false, "port": 80,
"rules": [{"name": "global", "request_url": "/*"},
{"name": "text", "request_url": "/text"}]},
{"origin": "mockdomain-image.com",
"rules": [{"name": "img", "request_url": "/img"}] }
],
"flavor_ref" : "standard"
}
}

View File

@ -0,0 +1,57 @@
{
"single_one_origin_with_domains": {
"name" : "mysite.com",
"domains": [
{"domain": "parsely.sage.com"},
{"domain": "densely.sage.com"},
{"domain": "rosemary.thyme.net"}
],
"origins": [
{"origin": "mockdomain.com", "ssl": false, "port": 80}
],
"flavor_ref" : "standard"
},
"multiple_origins_with_domains": {
"name" : "mysite.com",
"domains": [
{"domain": "parsely.sage.com"},
{"domain": "densely.sage.com"},
{"domain": "rosemary.thyme.net"}
],
"origins": [
{"origin": "mockdomain.com", "ssl": false, "port": 80},
{"origin": "mockdomain-image.com",
"rules": [{"name": "img", "request_url": "/img"}] }
],
"flavor_ref" : "standard"
},
"single_one_origin_without_domains": {
"name" : "mysite.com",
"origins": [
{"origin": "mockdomain.com", "ssl": false, "port": 80}
],
"flavor_ref" : "standard"
},
"multiple_origins_without_domains": {
"name" : "mysite.com",
"origins": [
{"origin": "mockdomain.com", "ssl": false, "port": 80},
{"origin": "mockdomain-image.com",
"rules": [{"name": "img", "request_url": "/img"}] }
],
"flavor_ref" : "standard"
},
"no_origin_with_domains": {
"name" : "mysite.com",
"domains": [
{"domain": "parsely.sage.com"},
{"domain": "densely.sage.com"},
{"domain": "rosemary.thyme.net"}
],
"flavor_ref" : "standard"
},
"no_origin_without_domains": {
"name" : "mysite.com",
"flavor_ref" : "standard"
}
}

View File

@ -0,0 +1,14 @@
{
"single_domain_list": [
"www.mysite.com",
"www.myawsomesite.com",
"www.myawsomesite2.com"
],
"multiple_domain_list": [
"www.mysite.com",
"blog.mysite.com",
"www.myawsomesite.com",
"test.myawsomesite.com",
"www.myawsomesite2.com"
]
}

View File

@ -0,0 +1,98 @@
# Copyright (c) 2014 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 mock
from oslo.config import cfg
from poppy.provider.akamai import driver
from tests.unit import base
AKAMAI_OPTIONS = [
# credentials && base URL for policy API
cfg.StrOpt(
'policy_api_client_token', default='ccc',
help='Akamai client token for policy API'),
cfg.StrOpt(
'policy_api_client_secret', default='sss',
help='Akamai client secret for policy API'),
cfg.StrOpt(
'policy_api_access_token', default='aaa',
help='Akamai access token for policy API'),
cfg.StrOpt(
'policy_api_base_url', default='/abc',
help='Akamai policy API base URL'),
# credentials && base URL for CCU API
# for purging
cfg.StrOpt(
'ccu_api_client_token', default='ccc',
help='Akamai client token for CCU API'),
cfg.StrOpt(
'ccu_api_client_secret', default='sss',
help='Akamai client secret for CCU API'),
cfg.StrOpt(
'ccu_api_access_token', default='aaa',
help='Akamai access token for CCU API'),
cfg.StrOpt(
'ccu_api_base_url', default='/abc',
help='Akamai CCU Purge API base URL'),
# Access URL in Akamai chain
cfg.StrOpt(
'akamai_access_url_link', default='abc.def.org',
help='Akamai domain access_url link'),
]
class TestDriver(base.TestCase):
def setUp(self):
super(TestDriver, self).setUp()
self.conf = cfg.ConfigOpts()
@mock.patch('akamai.edgegrid.EdgeGridAuth')
@mock.patch.object(driver, 'AKAMAI_OPTIONS', new=AKAMAI_OPTIONS)
def test_init(self, mock_connect):
provider = driver.CDNProvider(self.conf)
akamai_conf = provider._conf['drivers:provider:akamai']
mock_connect.assert_called_with(
client_token=(
akamai_conf.policy_api_client_token),
client_secret=(
akamai_conf.policy_api_client_secret),
access_token=(
akamai_conf.policy_api_access_token)
)
mock_connect.assert_called_with(
client_token=(
akamai_conf.ccu_api_client_token),
client_secret=(
akamai_conf.ccu_api_client_secret),
access_token=(
akamai_conf.ccu_api_access_token)
)
self.assertEqual('Akamai', provider.provider_name)
def test_is_alive(self):
provider = driver.CDNProvider(self.conf)
self.assertEqual(True, provider.is_alive())
@mock.patch('akamai.edgegrid.EdgeGridAuth')
@mock.patch.object(driver, 'AKAMAI_OPTIONS', new=AKAMAI_OPTIONS)
def test_get_client(self, mock_connect):
mock_connect.return_value = mock.Mock()
provider = driver.CDNProvider(self.conf)
self.assertNotEqual(None, provider.policy_api_client)
self.assertNotEqual(None, provider.ccu_api_client)

View File

@ -0,0 +1,169 @@
# Copyright (c) 2014 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 json
import uuid
import ddt
import mock
from poppy.model.helpers import domain
from poppy.provider.akamai import services
from poppy.transport.pecan.models.request import service
from tests.unit import base
@ddt.ddt
class TestServices(base.TestCase):
@mock.patch(
'poppy.provider.akamai.services.ServiceController.policy_api_client')
@mock.patch(
'poppy.provider.akamai.services.ServiceController.ccu_api_client')
@mock.patch('poppy.provider.akamai.driver.CDNProvider')
def setUp(self, mock_controller_policy_api_client,
mock_controller_ccu_api_client,
mock_driver):
super(TestServices, self).setUp()
self.driver = mock_driver()
self.controller = services.ServiceController(self.driver)
@ddt.file_data('domains_list.json')
def test_classify_domains(self, domains_list):
domains_list = [domain.Domain(domain_s) for domain_s in domains_list]
c_domains_list = self.controller._classify_domains(domains_list)
prev_content_realm = ''
for c_domains in c_domains_list:
self.assertTrue(len(c_domains) >= 1)
content_realm = '.'.join(c_domains[0].split('.')[-2:])
if len(c_domains) > 1:
# inside a group the content realm should be
# the same
for c_domain in c_domains:
self.assertEqual(content_realm,
'.'.join(c_domain.split('.')[-2:]))
next_content_realm = content_realm
# assert different group's content real is not eaual
self.assertNotEqual(prev_content_realm, next_content_realm,
'classified domains\'s content realm'
' should not equal')
prev_content_realm = next_content_realm
@ddt.file_data('data_service.json')
def test_create_with_exception(self, service_json):
# ASSERTIONS
# create_service
service_obj = service.load_from_json(service_json)
self.controller.policy_api_client.put.side_effect = (
RuntimeError('Creating service failed.'))
resp = self.controller.create(service_obj)
self.assertIn('error', resp[self.driver.provider_name])
@ddt.file_data('data_service.json')
def test_create_with_4xx_return(self, service_json):
service_obj = service.load_from_json(service_json)
# test exception
self.controller.policy_api_client.delete.return_value = mock.Mock(
status_code=400,
text='Some create service error happened'
)
resp = self.controller.create(service_obj)
self.assertIn('error', resp[self.driver.provider_name])
@ddt.file_data('data_service.json')
def test_create(self, service_json):
service_obj = service.load_from_json(service_json)
self.controller.create(service_obj)
self.controller.policy_api_client.put.assert_called_once()
def test_delete_with_exception(self):
provider_service_id = json.dumps([str(uuid.uuid1())])
# test exception
exception = RuntimeError('ding')
self.controller.policy_api_client.delete.side_effect = exception
resp = self.controller.delete(provider_service_id)
self.assertIn('error', resp[self.driver.provider_name])
def test_delete_with_service_id_json_load_error(self):
# This should trigger a json.loads error
provider_service_id = None
resp = self.controller.delete(provider_service_id)
self.assertIn('error', resp[self.driver.provider_name])
def test_delete_with_4xx_return(self):
provider_service_id = json.dumps([str(uuid.uuid1())])
# test exception
self.controller.policy_api_client.delete.return_value = mock.Mock(
status_code=400,
text='Some error happened'
)
resp = self.controller.delete(provider_service_id)
self.assertIn('error', resp[self.driver.provider_name])
def test_delete(self):
provider_service_id = json.dumps([str(uuid.uuid1())])
self.controller.delete(provider_service_id)
self.controller.policy_api_client.delete.assert_called_once()
@ddt.file_data('data_update_service.json')
def test_update_with_get_error(self, service_json):
provider_service_id = json.dumps([str(uuid.uuid1())])
controller = services.ServiceController(self.driver)
controller.policy_api_client.get.return_value = mock.Mock(
status_code=400,
text='Some get error happened'
)
controller.policy_api_client.put.return_value = mock.Mock(
status_code=200,
text='Put successful'
)
controller.policy_api_client.delete.return_value = mock.Mock(
status_code=200,
text='Delete successful'
)
service_obj = service.load_from_json(service_json)
resp = controller.update(
provider_service_id, service_obj, service_obj, service_obj)
self.assertIn('error', resp[self.driver.provider_name])
@ddt.file_data('data_update_service.json')
def test_update(self, service_json):
provider_service_id = json.dumps([str(uuid.uuid1())])
controller = services.ServiceController(self.driver)
controller.policy_api_client.get.return_value = mock.Mock(
status_code=200,
text=json.dumps(dict(rules=[]))
)
controller.policy_api_client.put.return_value = mock.Mock(
status_code=200,
text='Put successful'
)
controller.policy_api_client.delete.return_value = mock.Mock(
status_code=200,
text='Delete successful'
)
service_obj = service.load_from_json(service_json)
resp = controller.update(
provider_service_id, service_obj, service_obj, service_obj)
self.assertIn('id', resp[self.driver.provider_name])