Notification driver implementation

Implements blueprint: notification-driver

Change-Id: I959b7b6e30ddef5e2591d4ad43177213d53ab0cd
This commit is contained in:
tonytan4ever 2015-07-22 15:11:43 -04:00
parent d8d69a79d7
commit 4954298a60
24 changed files with 597 additions and 8 deletions

View File

@ -43,6 +43,9 @@ storage = cassandra
# Provider modules list (a list of comma separated provider module list)
providers = fastly
# Notification modules list (a list of comma separated notification module list)
notifications = mailgun
# DNS driver module (e.g. default, designate, rackspace)
dns = default
@ -111,6 +114,7 @@ auth_endpoint = ""
timeout = 30
delay = 1
[drivers:provider]
default_cache_ttl = 86400
@ -148,6 +152,13 @@ akamai_https_custom_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_CUSTOM_NUMBER'
san_cert_cnames = "MY_SAN_CERT_LIST"
san_cert_hostname_limit = "MY_SAN_HOSTNAMES_LMIT"
[drivers:notification:mailgun]
mailgun_api_key = "<operator_api_key>"
mailgun_request_url = "https://api.mailgun.net/v2/{0}/events"
sand_box = "<your_sand_box_domain>"
from_address = "<your_send_from_email_address>"
recipients="<a_list_of_email_recipient>"
retry_send=5
[log_delivery]
identity_url = OPENSTACK_IDENTITY_URL

View File

@ -51,6 +51,8 @@ _DRIVER_OPTIONS = [
help='Storage driver to use'),
cfg.ListOpt('providers', default=['mock'],
help='Provider driver(s) to use'),
cfg.ListOpt('notifications', default=['mail'],
help='Notification driver(s) to use'),
cfg.StrOpt('dns', default='default',
help='DNS driver to use'),
cfg.StrOpt('distributed_task', default='taskflow',
@ -119,6 +121,26 @@ class Bootstrap(object):
invoke_args=args)
return mgr
@decorators.lazy_property(write=False)
def notification(self):
"""notification.
:returns mgr
"""
LOG.debug((u'Loading notification extension(s)'))
# create the driver manager to load the appropriate drivers
notification_type = 'poppy.notification'
args = [self.conf]
notification_type_names = self.driver_conf.notifications
mgr = named.NamedExtensionManager(namespace=notification_type,
names=notification_type_names,
invoke_on_load=True,
invoke_args=args)
return mgr
@decorators.lazy_property(write=False)
def storage(self):
"""storage.
@ -156,7 +178,7 @@ class Bootstrap(object):
manager_name = self.driver_conf.manager
args = [self.conf, self.storage, self.provider, self.dns,
self.distributed_task]
self.distributed_task, self.notification]
try:
mgr = driver.DriverManager(namespace=manager_type,

View File

@ -1,3 +1,18 @@
# Copyright (c) 2013 Red Hat, 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.
"""Rackspace Cloud DNS driver"""
from poppy.dns.rackspace import driver

View File

@ -21,12 +21,14 @@ import six
@six.add_metaclass(abc.ABCMeta)
class ManagerDriverBase(object):
"""Base class for driver manager."""
def __init__(self, conf, storage, providers, dns, distributed_task):
def __init__(self, conf, storage, providers, dns, distributed_task,
notification):
self._conf = conf
self._storage = storage
self._providers = providers
self._dns = dns
self._distributed_task = distributed_task
self._notification = notification
@property
def conf(self):
@ -60,6 +62,10 @@ class ManagerDriverBase(object):
def distributed_task(self):
return self._distributed_task
@property
def notification(self):
return self._notification
@abc.abstractproperty
def services_controller(self):
"""Returns the driver's services controller

View File

@ -0,0 +1,29 @@
# 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.
class NotificationWrapper(object):
""""ProviderWrapper class."""
def send(self, ext, subject, mail_content):
"""Send notfications with subject and mail_content
:param ext
:param subject
:param mail_content
:returns: ext.obj.services_controller.send(subject, mail_content)
"""
return ext.obj.services_controller.send(subject, mail_content)

View File

@ -18,6 +18,7 @@ import abc
import six
from poppy.manager.base import controller
from poppy.manager.base import notifications
from poppy.manager.base import providers
@ -29,6 +30,7 @@ class ServicesControllerBase(controller.ManagerControllerBase):
super(ServicesControllerBase, self).__init__(manager)
self.provider_wrapper = providers.ProviderWrapper()
self.notification_wrapper = notifications.NotificationWrapper()
@abc.abstractmethod
def list(self, project_id, marker=None, limit=None):

View File

@ -23,9 +23,10 @@ from poppy.manager.default import controllers
class DefaultManagerDriver(base.Driver):
"""Default Manager Driver."""
def __init__(self, conf, storage, providers, dns, distributed_task):
def __init__(self, conf, storage, providers, dns, distributed_task,
notification):
super(DefaultManagerDriver, self).__init__(
conf, storage, providers, dns, distributed_task)
conf, storage, providers, dns, distributed_task, notification)
@decorators.lazy_property(write=True)
def services_controller(self):

View File

View File

@ -0,0 +1,21 @@
# 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.notification.base import driver
from poppy.notification.base import services
Driver = driver.NotificationDriverBase
ServicesBase = services.ServicesControllerBase

View File

@ -0,0 +1,35 @@
# 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 abc
import six
@six.add_metaclass(abc.ABCMeta)
class NotificationControllerBase(object):
"""Top-level class for controllers.
:param driver: Instance of the driver
instantiating this controller.
"""
def __init__(self, driver):
self._driver = driver
@property
def driver(self):
return self._driver

View File

@ -0,0 +1,38 @@
# 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 abc
import six
@six.add_metaclass(abc.ABCMeta)
class NotificationDriverBase(object):
"""Interface definition for notification drivers.
:param conf: Configuration containing options for this driver.
:type conf: `oslo_config.ConfigOpts`
"""
def __init__(self, conf):
self._conf = conf
@abc.abstractproperty
def services_controller(self):
"""Returns the driver's hostname controller.
:raises NotImplementedError
"""
raise NotImplementedError

View File

@ -0,0 +1,38 @@
# 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 abc
import six
from poppy.notification.base import controller
@six.add_metaclass(abc.ABCMeta)
class ServicesControllerBase(controller.NotificationControllerBase):
"""Services Controller Base class."""
def __init__(self, driver):
super(ServicesControllerBase, self).__init__(driver)
def send(self):
"""delete.
:param provider_details
:raises NotImplementedError
"""
raise NotImplementedError

View File

@ -0,0 +1,21 @@
# 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.notification.mailgun import driver
'''Mail notification driver'''
# Hoist classes into package namespace
Driver = driver.MailNotificationDriver

View File

@ -0,0 +1,18 @@
# 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.notification.mailgun import services
ServicesController = services.ServicesController

View File

@ -0,0 +1,83 @@
# 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.
"""Mail Notification Driver implementation."""
import re
from oslo_config import cfg
from poppy.notification import base
from poppy.notification.mailgun import controller
MAIL_NOTIFICATION_OPTIONS = [
cfg.StrOpt('mailgun_api_key',
help='Mail gun secret API Key'),
cfg.IntOpt('retry_send', default=5,
help='Mailgun send retry'),
cfg.StrOpt('mailgun_request_url',
help='Mail gun request url'),
cfg.StrOpt('sand_box',
help='Mail gun sand box domain'),
cfg.StrOpt('from_address', default='noreply@poppycdn.org',
help='Sent from email address'),
cfg.ListOpt('recipients',
help='A list of emails addresses to receive notification ')
]
MAIL_NOTIFICATION_GROUP = 'drivers:notification:mailgun'
def validate_email_address(email_address):
email_re = re.compile('(\w+[.|\w])*@(\w+[.])*\w+')
return email_re.match(email_address) is not None
class MailNotificationDriver(base.Driver):
"""Mail Notification Driver."""
def __init__(self, conf):
super(MailNotificationDriver, self).__init__(conf)
conf.register_opts(MAIL_NOTIFICATION_OPTIONS,
group=MAIL_NOTIFICATION_GROUP)
self.mail_notification_conf = conf[MAIL_NOTIFICATION_GROUP]
self.retry_send = self.mail_notification_conf.retry_send
self.mailgun_api_key = self.mail_notification_conf.mailgun_api_key
self.mailgun_request_url = (
self.mail_notification_conf.mailgun_request_url)
self.sand_box = self.mail_notification_conf.sand_box
self.from_address = self.mail_notification_conf.from_address
self.recipients = self.mail_notification_conf.recipients
# validate email addresses
if not validate_email_address(self.from_address):
raise validate_email_address("Notification config error:"
"%s is not a valid email address"
% self.from_address)
for address in self.recipients:
if not validate_email_address(address):
raise validate_email_address("Notification config error:"
"%s is not a valid email address"
% address)
@property
def services_controller(self):
"""Hook for service controller.
:return service_controller
"""
return controller.ServicesController(self)

View File

@ -0,0 +1,87 @@
# 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 requests
from poppy.notification import base
from poppy.openstack.common import log
LOG = log.getLogger(__name__)
class ServicesController(base.ServicesBase):
"""Services Controller Base class."""
def __init__(self, driver):
super(ServicesController, self).__init__(driver)
self.mailgun_api_key = self.driver.mailgun_api_key
self.retry_send = self.driver.retry_send
self.mailgun_request_url = self.driver.mailgun_request_url
self.sand_box = self.driver.sand_box
self.from_address = self.driver.from_address
self.recipients = self.driver.recipients
def send(self, subject, mail_content):
"""send notification to a (list) of recipients.
:param subject
:param mail_content
:raises NotImplementedError
"""
res = self._send_mail_notification_via_mailgun(subject, mail_content)
if res:
LOG.info("Send email notification successful."
"Subject: %s"
"Content: %s" % (subject,
mail_content))
return
def _send_mail_notification_via_mailgun(self, subject, mail_content):
'''Send a mail via mail gun'''
request_url = self.mailgun_request_url.format(self.sand_box)
response_status_code = None
attempt = 1
while response_status_code != 200 and self.retry_send != attempt:
LOG.info("Sending email notification attempt: %s" % str(attempt))
response = requests.post(
request_url,
auth=('api', self.mailgun_api_key),
data={
'from': self.from_address,
'to': self.recipients,
'subject': subject,
'text': mail_content
}
)
response_status_code = response.status_code
attempt += 1
if response_status_code != 200:
LOG.warning("Send email notification failed. Details:"
"From: %s"
"To: %s"
"Subject: %s"
"Content: %s" % (self.from_address,
self.recipients,
subject,
mail_content))
return False
else:
return True

View File

@ -60,6 +60,9 @@ poppy.provider =
poppy.distributed_task =
taskflow = poppy.distributed_task.taskflow:Driver
poppy.notification =
mailgun = poppy.notification.mailgun:Driver
[wheel]
universal = 1

View File

@ -30,8 +30,9 @@ class DefaultManagerFlavorTests(base.TestCase):
@mock.patch('poppy.provider.base.driver.ProviderDriverBase')
@mock.patch('poppy.dns.base.driver.DNSDriverBase')
@mock.patch('poppy.distributed_task.base.driver.DistributedTaskDriverBase')
@mock.patch('poppy.notification.base.driver.NotificationDriverBase')
def setUp(self, mock_driver, mock_provider, mock_dns,
mock_distributed_task):
mock_distributed_task, mock_notification):
super(DefaultManagerFlavorTests, self).setUp()
# create mocked config and driver
@ -45,7 +46,8 @@ class DefaultManagerFlavorTests(base.TestCase):
mock_driver,
mock_provider,
mock_dns,
mock_distributed_task)
mock_distributed_task,
mock_notification)
# stubbed driver
self.fc = flavors.DefaultFlavorsController(manager_driver)

View File

@ -0,0 +1,35 @@
# 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 mock
from poppy.manager.base import notifications
from tests.unit import base
class TestProviderWrapper(base.TestCase):
def setUp(self):
super(TestProviderWrapper, self).setUp()
self.notifications_wrapper_obj = notifications.NotificationWrapper()
def test_create(self):
mock_obj = mock.Mock()
mock_ext = mock.Mock(obj=mock_obj)
self.notifications_wrapper_obj.send(mock_ext,
"test_subject",
"test_mail_content")
mock_ext.obj.services_controller.send.assert_called_once_with(
"test_subject", "test_mail_content")

View File

@ -98,11 +98,12 @@ class MonkeyPatchControllers(object):
class DefaultManagerServiceTests(base.TestCase):
@mock.patch('poppy.bootstrap.Bootstrap')
@mock.patch('poppy.notification.base.driver.NotificationDriverBase')
@mock.patch('poppy.dns.base.driver.DNSDriverBase')
@mock.patch('poppy.storage.base.driver.StorageDriverBase')
@mock.patch('poppy.distributed_task.base.driver.DistributedTaskDriverBase')
def setUp(self, mock_distributed_task, mock_storage,
mock_dns, mock_bootstrap):
mock_dns, mock_notification, mock_bootstrap):
# NOTE(TheSriram): the mock.patch decorator applies mocks
# in the reverse order of the arguments present
super(DefaultManagerServiceTests, self).setUp()
@ -127,7 +128,8 @@ class DefaultManagerServiceTests(base.TestCase):
mock_storage,
mock_providers,
mock_dns,
mock_distributed_task)
mock_distributed_task,
mock_notification)
# stubbed driver
self.sc = services.DefaultServicesController(manager_driver)

View File

View File

@ -0,0 +1,60 @@
# 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.
"""Unittests for mail notification driver implementation."""
import mock
from oslo_config import cfg
from poppy.notification.mailgun import driver
from tests.unit import base
MAIL_NOTIFICATION_OPTIONS = [
cfg.StrOpt('mailgun_api_key', default='123',
help='Mail gun secret API Key'),
cfg.IntOpt('retry_send', default=5,
help='Mailgun send retry'),
cfg.StrOpt('mailgun_request_url', default='http://123.com/{0}',
help='Mail gun request url'),
cfg.StrOpt('sand_box', default='123.com',
help='Mail gun sand box domain'),
cfg.StrOpt('from_address', default='noreply@poppycdn.org',
help='Sent from email address'),
cfg.ListOpt('recipients', default=['recipient@gmail.com'],
help='A list of emails addresses to receive notification ')
]
MAIL_NOTIFICATION_GROUP = 'drivers:notification:mailgun'
class TestDriver(base.TestCase):
@mock.patch.object(driver, 'MAIL_NOTIFICATION_OPTIONS',
new=MAIL_NOTIFICATION_OPTIONS)
def setUp(self):
super(TestDriver, self).setUp()
self.conf = cfg.ConfigOpts()
self.mailgun_notification_driver = (
driver.MailNotificationDriver(self.conf))
def test_init(self):
self.assertTrue(self.mailgun_notification_driver is not None)
def test_service_contoller(self):
self.assertTrue(self.mailgun_notification_driver.services_controller
is not None)
self.assertTrue(self.mailgun_notification_driver.retry_send == 5)

View File

@ -0,0 +1,60 @@
# 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.
"""Unittests for TaskFlow distributed_task service_controller."""
import mock
import requests
from oslo_config import cfg
from poppy.notification.mailgun import driver
from tests.unit import base
MAIL_NOTIFICATION_OPTIONS = [
cfg.StrOpt('mailgun_api_key', default='123',
help='Mail gun secret API Key'),
cfg.IntOpt('retry_send', default=5,
help='Mailgun send retry'),
cfg.StrOpt('mailgun_request_url', default='http://123.com/{0}',
help='Mail gun request url'),
cfg.StrOpt('sand_box', default='123.com',
help='Mail gun sand box domain'),
cfg.StrOpt('from_address', default='noreply@poppycdn.org',
help='Sent from email address'),
cfg.ListOpt('recipients', default=['recipient@gmail.com'],
help='A list of emails addresses to receive notification ')
]
MAIL_NOTIFICATION_GROUP = 'drivers:notification:mail'
class TestServiceController(base.TestCase):
@mock.patch.object(driver, 'MAIL_NOTIFICATION_OPTIONS',
new=MAIL_NOTIFICATION_OPTIONS)
def setUp(self):
super(TestServiceController, self).setUp()
self.conf = cfg.ConfigOpts()
self.mail_notification_driver = (
driver.MailNotificationDriver(self.conf))
self.controller = self.mail_notification_driver.services_controller
@mock.patch.object(requests, 'post', new=mock.Mock())
def test_send_mail_notification(self):
self.assertTrue(self.controller.retry_send == 5)
self.controller.send("A Subject", "Some random text")
requests.post.assert_called_once()