Add MaxCDN driver && unittest

Change-Id: I4b1b51c994cf918b3de898c702869dae475eda95
This commit is contained in:
tonytan4ever 2014-09-01 23:26:40 -04:00
parent 1f2960cf2f
commit 2ea3f39221
14 changed files with 557 additions and 3 deletions

View File

@ -0,0 +1,21 @@
# 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.
"""MaxCDN Provider Extension for CDN"""
from poppy.provider.maxcdn 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 MaxCDN 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.maxcdn import services
ServiceController = services.ServiceController

View File

@ -0,0 +1,68 @@
# 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.
"""Max CDN Provider implementation."""
import maxcdn
from oslo.config import cfg
from poppy.openstack.common import log as logging
from poppy.provider import base
from poppy.provider.maxcdn import controllers
LOG = logging.getLogger(__name__)
MAXCDN_OPTIONS = [
cfg.StrOpt('alias', help='MAXCDN API account alias'),
cfg.StrOpt('consumer_key', help='MAXCDN API consumer key'),
cfg.StrOpt('consumer_secret', help='MAXCDN API consumer secret'),
]
MAXCDN_GROUP = 'drivers:provider:maxcdn'
class CDNProvider(base.Driver):
def __init__(self, conf):
"""Init constructor."""
super(CDNProvider, self).__init__(conf)
self._conf.register_opts(MAXCDN_OPTIONS,
group=MAXCDN_GROUP)
self.maxcdn_conf = self._conf[MAXCDN_GROUP]
self.maxcdn_client = maxcdn.MaxCDN(self.maxcdn_conf.alias,
self.maxcdn_conf.consumer_key,
self.maxcdn_conf.consumer_secret)
def is_alive(self):
"""For health state."""
return True
@property
def provider_name(self):
"""For name."""
return "MaxCDN"
@property
def client(self):
"""client to this provider."""
return self.maxcdn_client
@property
def service_controller(self):
"""Hook for service controller."""
return controllers.ServiceController(self)

View File

@ -0,0 +1,110 @@
# 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.
from poppy.provider import base
class ServiceController(base.ServiceBase):
'''MaxCDN Service Controller.
'''
@property
def client(self):
return self.driver.client
def __init__(self, driver):
'''Initialize a service controller object.'''
super(ServiceController, self).__init__(driver)
self.driver = driver
# This returns the current customer account info
account_info_return = self.client.get('/account.json')
if account_info_return['code'] != 200:
raise RuntimeError(account_info_return['error'])
self.current_customer = account_info_return['data']['account']
def update(self, pullzone_id, service_json):
'''MaxCDN update.
manager needs to pass in pullzone id to delete.
'''
try:
update_response = self.client.put('/zones/pull.json/%s'
% pullzone_id,
params=service_json)
if update_response['code'] != 200:
return self.responder.failed('failed to update service')
return self.responder.updated(
update_response['data']['pullzone']['id'])
except Exception:
# this exception branch will most likely for a network failure
return self.responder.failed('failed to update service')
def create(self, service_name, service_json):
'''MaxCDN create.
manager needs to pass in a service name to create.
'''
try:
# Create a new pull zone: maxcdn only supports 1 origin
origin = service_json['origins'][0]
create_response = self.client.post('/zones/pull.json', data={
'name': service_name,
'url': origin['origin'],
'port': origin.get('port', 80),
'sslshared': 1 if origin['ssl'] else 0,
})
if create_response['code'] != 201:
return self.responder.failed('failed to create service')
created_zone_info = create_response['data']['pullzone']
# Add custom domains to this service
links = []
for domain in service_json['domains']:
custom_domain_response = self.client.post(
'/zones/pull/%s/customdomains.json'
% created_zone_info['id'],
{'custom_domain': domain['domain']})
links.append(custom_domain_response)
# TODO(tonytan4ever): What if it fails during add domains ?
return self.responder.created(created_zone_info['id'], links)
except Exception:
# this exception branch will most likely for a network failure
return self.responder.failed('failed to create service')
def delete(self, pullzone_id):
'''MaxCDN create.
manager needs to pass in a service name to delete.
'''
try:
delete_response = self.client.delete('/zones/pull.json/%s'
% pullzone_id)
if delete_response['code'] != 200:
return self.responder.failed('failed to delete service')
return self.responder.deleted(pullzone_id)
except Exception:
# this exception branch will most likely for a network failure
return self.responder.failed('failed to delete service')
# TODO(tonytan4ever): get service
def get(self, service_name):
'''Get details of the service, as stored by the provider.'''
return {'domains': [], 'origins': [], 'caching': []}

View File

@ -0,0 +1 @@
# official max-cdn is not working for python33 yet. add it later

View File

@ -3,3 +3,4 @@
-r storage/cassandra.txt
-r transport/pecan.txt
-r provider/fastly.txt
-r provider/maxcdn.txt

View File

@ -5,4 +5,9 @@ manager = default
storage = mockdb
[drivers:provider:fastly]
apikey = "MYAPIKEY"
apikey = "MYAPIKEY"
[drivers:provider:maxcdn]
alias = "MYALIAS"
consumer_secret = "MYCONSUMER_SECRET"
consumer_key = "MYCONSUMERKEY"

View File

@ -25,7 +25,7 @@ log_file = poppy.log
[drivers]
# Transport driver module (e.g., falcon, pecan)
transport = falcon
transport = pecan
# Manager driver module (e.g. default)
manager = default

View File

@ -27,7 +27,7 @@ log_file = poppy.log
[drivers]
# Transport driver module (e.g., falcon, pecan)
transport = falcon
transport = pecan
# Manager driver module (e.g. default)
manager = default

View File

View File

@ -0,0 +1,11 @@
{
"service_json": {
"domains": [
{"domain": "parsely.sage.com"},
{"domain": "rosemary.thyme.net"}
],
"origins": [
{"origin": "mockdomain.com", "ssl": false, "port": 80}
]
}
}

View File

@ -0,0 +1,70 @@
# 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 os
import mock
from oslo.config import cfg
from poppy.provider.maxcdn import driver
from tests.unit import base
MAXCDN_OPTIONS = [
cfg.StrOpt('alias', help='MAXCDN API account alias'),
cfg.StrOpt('consumer_key', help='MAXCDN API consumer key'),
cfg.StrOpt('consumer_secret', help='MAXCDN API consumer secret'),
]
class TestDriver(base.TestCase):
def setUp(self):
super(TestDriver, self).setUp()
tests_path = os.path.abspath(os.path.dirname(
os.path.dirname(
os.path.dirname(os.path.dirname(__file__)
))))
conf_path = os.path.join(tests_path, 'etc', 'default_functional.conf')
cfg.CONF(args=[], default_config_files=[conf_path])
self.conf = cfg.CONF
@mock.patch('maxcdn.MaxCDN')
@mock.patch.object(driver, 'MAXCDN_OPTIONS', new=MAXCDN_OPTIONS)
def test_init(self, mock_connect):
provider = driver.CDNProvider(self.conf)
mock_connect.assert_called_once_with(
provider._conf['drivers:provider:maxcdn'].alias,
provider._conf['drivers:provider:maxcdn'].consumer_key,
provider._conf['drivers:provider:maxcdn'].consumer_secret)
@mock.patch.object(driver, 'MAXCDN_OPTIONS', new=MAXCDN_OPTIONS)
def test_is_alive(self):
provider = driver.CDNProvider(self.conf)
self.assertEqual(provider.is_alive(), True)
@mock.patch.object(driver, 'MAXCDN_OPTIONS', new=MAXCDN_OPTIONS)
def test_get_client(self):
provider = driver.CDNProvider(self.conf)
client = provider.client
self.assertNotEqual(client, None)
@mock.patch('poppy.provider.maxcdn.controllers.ServiceController')
@mock.patch.object(driver, 'MAXCDN_OPTIONS', new=MAXCDN_OPTIONS)
def test_service_controller(self, MockController):
provider = driver.CDNProvider(self.conf)
self.assertNotEqual(provider.service_controller, None)

View File

@ -0,0 +1,239 @@
# 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 ddt
import mock
from oslo.config import cfg
from poppy.provider.maxcdn import driver
from poppy.provider.maxcdn import services
from tests.unit import base
MAXCDN_OPTIONS = [
cfg.StrOpt('alias',
default='no_good_alias',
help='MAXCDN API account alias'),
cfg.StrOpt('consumer_key',
default='a_consumer_key',
help='MAXCDN API consumer key'),
cfg.StrOpt('consumer_secret',
default='a_consumer_secret',
help='MAXCDN API consumer secret'),
]
fake_maxcdn_client_get_return_value = {u'code': 200,
u'data':
{u'account':
{u'status': u'2',
u'name': u'<My_fake_company_alias>',
u'id': u'32811'
}}}
fake_maxcdn_client_400_return_value = {
u'code': 400,
u'message': "operation PUT/GET/POST failed due to technical difficulties.."
}
class fake_maxcdn_api_client:
def get(self, url='/account.json'):
return {u'code': 200,
u'data':
{u'account':
{u'status': u'2',
u'name': u'<My_fake_company_alias>',
u'id': u'32811'
}}}
def post(self, url=None, data=None):
return {u'code': 201,
u'data': {
u"pullzone": {
u"cdn_url": u"newpullzone1.alias.netdna-cdn.com",
u'name': u'newpullzone1',
u'id': u'97312'
}}}
def put(self, url=None, params=None):
return {u'code': 200,
u'data': {
u"pullzone": {
u"cdn_url": u"newpullzone1.alias.netdna-cdn.com",
u'name': u'newpullzone1',
u'id': u'97312'
}}}
def delete(self, url=None):
return {u'code': 200,
}
@ddt.ddt
class TestServices(base.TestCase):
def setUp(self):
super(TestServices, self).setUp()
self.conf = cfg.ConfigOpts()
@mock.patch.object(driver, 'MAXCDN_OPTIONS', new=MAXCDN_OPTIONS)
def test_init(self):
provider = driver.CDNProvider(self.conf)
# instantiate will get
self.assertRaises(RuntimeError, services.ServiceController, provider)
@mock.patch.object(driver.CDNProvider, 'client',
new=fake_maxcdn_api_client())
def test_get(self):
new_driver = driver.CDNProvider(self.conf)
# instantiate
controller = services.ServiceController(new_driver)
service_name = "test_service_name"
self.assertTrue(controller.get(service_name) is not None)
@ddt.file_data('data_service.json')
@mock.patch.object(driver.CDNProvider, 'client',
new=fake_maxcdn_api_client())
def test_create(self, service_json):
new_driver = driver.CDNProvider(self.conf)
# instantiate
controller = services.ServiceController(new_driver)
# test create, everything goes through successfully
service_name = "test_service_name"
resp = controller.create(service_name, service_json)
self.assertIn('id', resp[new_driver.provider_name])
self.assertIn('links', resp[new_driver.provider_name])
@ddt.file_data('data_service.json')
@mock.patch('poppy.provider.maxcdn.driver.CDNProvider.client')
@mock.patch('poppy.provider.maxcdn.driver.CDNProvider')
def test_create_with_exception(self, service_json, mock_controllerclient,
mock_driver):
# test create with exceptions
driver = mock_driver()
driver.attach_mock(mock_controllerclient, 'client')
driver.client.configure_mock(**{'get.return_value':
fake_maxcdn_client_get_return_value
})
service_name = "test_service_name"
controller_with_create_exception = services.ServiceController(driver)
controller_with_create_exception.client.configure_mock(**{
"post.side_effect":
RuntimeError('Creating service mysteriously failed.')})
resp = controller_with_create_exception.create(
service_name,
service_json)
self.assertIn('error', resp[driver.provider_name])
controller_with_create_exception.client.reset_mock()
controller_with_create_exception.client.configure_mock(**{
'post.side_effect': None,
"post.return_value": fake_maxcdn_client_400_return_value
})
resp = controller_with_create_exception.create(
service_name,
service_json)
self.assertIn('error', resp[driver.provider_name])
@ddt.file_data('data_service.json')
@mock.patch.object(driver.CDNProvider, 'client',
new=fake_maxcdn_api_client())
def test_update(self, service_json):
new_driver = driver.CDNProvider(self.conf)
# instantiate
controller = services.ServiceController(new_driver)
# test create, everything goes through successfully
service_name = "test_service_name"
resp = controller.update(service_name, service_json)
self.assertIn('id', resp[new_driver.provider_name])
@ddt.file_data('data_service.json')
@mock.patch('poppy.provider.maxcdn.driver.CDNProvider.client')
@mock.patch('poppy.provider.maxcdn.driver.CDNProvider')
def test_update_with_exception(self, service_json, mock_controllerclient,
mock_driver):
# test create with exceptions
driver = mock_driver()
driver.attach_mock(mock_controllerclient, 'client')
driver.client.configure_mock(**{'get.return_value':
fake_maxcdn_client_get_return_value
})
service_name = "test_service_name"
controller_with_update_exception = services.ServiceController(driver)
controller_with_update_exception.client.configure_mock(**{
"put.side_effect":
RuntimeError('Updating service mysteriously failed.')})
resp = controller_with_update_exception.update(
service_name,
service_json)
self.assertIn('error', resp[driver.provider_name])
controller_with_update_exception.client.reset_mock()
controller_with_update_exception.client.configure_mock(**{
"put.side_effect": None,
"put.return_value": fake_maxcdn_client_400_return_value
})
resp = controller_with_update_exception.update(
service_name,
service_json)
self.assertIn('error', resp[driver.provider_name])
@mock.patch.object(driver.CDNProvider, 'client',
new=fake_maxcdn_api_client())
def test_delete(self):
new_driver = driver.CDNProvider(self.conf)
# instantiate
controller = services.ServiceController(new_driver)
# test create, everything goes through successfully
service_name = "test_service_name"
resp = controller.delete(service_name)
self.assertIn('id', resp[new_driver.provider_name])
@mock.patch('poppy.provider.maxcdn.driver.CDNProvider.client')
@mock.patch('poppy.provider.maxcdn.driver.CDNProvider')
def test_delete_with_exception(self, mock_controllerclient,
mock_driver):
# test create with exceptions
driver = mock_driver()
driver.attach_mock(mock_controllerclient, 'client')
driver.client.configure_mock(**{'get.return_value':
fake_maxcdn_client_get_return_value
})
service_name = "test_service_name"
controller_with_delete_exception = services.ServiceController(driver)
controller_with_delete_exception.client.configure_mock(**{
"delete.side_effect":
RuntimeError('Deleting service mysteriously failed.')})
resp = controller_with_delete_exception.delete(service_name)
self.assertEqual(resp[driver.provider_name]['error'],
'failed to delete service')
controller_with_delete_exception.client.reset_mock()
controller_with_delete_exception.client.configure_mock(**{
"delete.side_effect": None,
"delete.return_value": fake_maxcdn_client_400_return_value
})
resp = controller_with_delete_exception.delete(service_name)
self.assertEqual(resp[driver.provider_name]['error'],
'failed to delete service')

View File

@ -17,6 +17,7 @@ setenv = VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements/requirements.txt
-r{toxinidir}/tests/test-requirements.txt
commands = pip install git+https://github.com/malini-kamalambal/opencafe.git#egg=cafe
pip install git+https://github.com/tonytan4ever/python-maxcdn.git#egg=maxcdn
nosetests {posargs}
[tox:jenkins]