Send usage notifications on major stack events

These notifications can then be used for usage/billing/alarming purposes.

Some of the base fields are based on the existing nova fields:
https://wiki.openstack.org/wiki/SystemUsageData

This patch just has the minimum fields needed we can add more as users
ask.

We can also add more notifications as we need:
- automated actions (autoscaling, restart)
- time based .exists notifications

implements blueprint send-notification
Change-Id: I63ad5402037799842643eb642af15b54ef8d7483
This commit is contained in:
Angus Salkeld 2013-10-30 09:22:52 +11:00
parent a1301baaa4
commit 4a42070fee
6 changed files with 246 additions and 0 deletions

View File

@ -141,6 +141,27 @@ def format_event(event):
return result
def format_notification_body(stack):
# some other posibilities here are:
# - template name
# - template size
# - resource count
if stack.status is not None and stack.action is not None:
state = '_'.join(stack.state)
else:
state = 'Unknown'
result = {
api.NOTIFY_TENANT_ID: stack.context.tenant_id,
api.NOTIFY_USER_ID: stack.context.user,
api.NOTIFY_STACK_ID: stack.identifier().arn(),
api.NOTIFY_STACK_NAME: stack.name,
api.NOTIFY_STATE: state,
api.NOTIFY_STATE_REASON: stack.status_reason,
api.NOTIFY_CREATE_AT: timeutils.isotime(stack.created_time),
}
return result
def format_watch(watch):
result = {

View File

@ -0,0 +1,53 @@
#
# 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 oslo.config import cfg
from heat.openstack.common import log
from heat.openstack.common.notifier import api as notifier_api
from heat.engine import api as engine_api
LOG = log.getLogger(__name__)
SERVICE = 'orchestration'
CONF = cfg.CONF
CONF.import_opt('default_notification_level',
'heat.openstack.common.notifier.api')
CONF.import_opt('default_publisher_id',
'heat.openstack.common.notifier.api')
def send(stack):
"""Send usage notifications to the configured notification driver."""
publisher_id = CONF.default_publisher_id
if publisher_id is None:
publisher_id = notifier_api.publisher_id(SERVICE)
# The current notifications have a start/end:
# see: https://wiki.openstack.org/wiki/SystemUsageData
# so to be consistant we translate our status into a known start/end/error
# suffix.
level = CONF.default_notification_level.upper()
if stack.status == stack.IN_PROGRESS:
suffix = 'start'
elif stack.status == stack.COMPLETE:
suffix = 'end'
else:
suffix = 'error'
level = notifier_api.ERROR
event_type = '%s.%s.%s' % (SERVICE, stack.action.lower(), suffix)
notifier_api.notify(stack.context, publisher_id,
event_type, level,
engine_api.format_notification_body(stack))

View File

@ -23,6 +23,7 @@ from heat.engine import environment
from heat.common import exception
from heat.engine import dependencies
from heat.common import identifier
from heat.engine import notification
from heat.engine import resource
from heat.engine import resources
from heat.engine import scheduler
@ -348,6 +349,7 @@ class Stack(collections.Mapping):
stack.update_and_save({'action': action,
'status': status,
'status_reason': reason})
notification.send(self)
@property
def state(self):

View File

@ -82,6 +82,24 @@ EVENT_KEYS = (
'resource_properties',
)
NOTIFY_KEYS = (
NOTIFY_TENANT_ID,
NOTIFY_USER_ID,
NOTIFY_STACK_ID,
NOTIFY_STACK_NAME,
NOTIFY_STATE,
NOTIFY_STATE_REASON,
NOTIFY_CREATE_AT,
) = (
'tenant_id',
'user_id',
STACK_ID,
STACK_NAME,
'state',
'state_reason',
'create_at',
)
# This is the representation of a watch we expose to the API via RPC
WATCH_KEYS = (
WATCH_ACTIONS_ENABLED, WATCH_ALARM_ACTIONS, WATCH_TOPIC,

View File

@ -20,6 +20,7 @@ from oslo.config import cfg
from heat.common import exception
from heat.common import template_format
from heat.engine import notification
from heat.engine import parser
from heat.engine.resources import user
from heat.engine.resources import instance
@ -242,6 +243,9 @@ class AutoScalingGroupTest(HeatTestCase):
self.m.StubOutWithMock(instance.Instance, 'handle_create')
self.m.StubOutWithMock(instance.Instance, 'check_create_complete')
self.m.StubOutWithMock(notification, 'send')
notification.send(mox.IgnoreArg()).MultipleTimes().AndReturn(None)
cookie = object()
# for load balancer setup
@ -266,6 +270,9 @@ class AutoScalingGroupTest(HeatTestCase):
# for load balancer setup
self._stub_lb_reload(num_reloads_expected_on_updt)
self.m.StubOutWithMock(notification, 'send')
notification.send(mox.IgnoreArg()).MultipleTimes().AndReturn(None)
# for instances in the group
self.m.StubOutWithMock(instance.Instance, 'handle_create')
self.m.StubOutWithMock(instance.Instance, 'check_create_complete')

View File

@ -0,0 +1,145 @@
# 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 heat.openstack.common import timeutils
from heat.engine import environment
from heat.engine import parser
from heat.engine import resource
from heat.tests import generic_resource
from heat.tests import utils
from heat.tests import common
class NotificationTest(common.HeatTestCase):
def setUp(self):
super(NotificationTest, self).setUp()
utils.setup_dummy_db()
cfg.CONF.import_opt('notification_driver',
'heat.openstack.common.notifier.api')
cfg.CONF.set_default('notification_driver',
['heat.openstack.common.notifier.test_notifier'])
cfg.CONF.set_default('host', 'test_host')
resource._register_class('GenericResource',
generic_resource.ResourceWithProps)
def create_test_stack(self):
test_template = {'Parameters': {'Foo': {'Type': 'String'},
'Pass': {'Type': 'String',
'NoEcho': True}},
'Resources':
{'TestResource': {'Type': 'GenericResource',
'Properties': {'Foo': 'abc'}}},
'Outputs': {'food':
{'Value':
{'Fn::GetAtt': ['TestResource',
'foo']}}}}
template = parser.Template(test_template)
self.ctx = utils.dummy_context()
self.ctx.tenant_id = 'test_tenant'
env = environment.Environment()
env.load({u'parameters':
{u'Foo': 'user_data', u'Pass': 'secret'}})
self.stack_name = utils.random_name()
stack = parser.Stack(self.ctx, self.stack_name, template,
env=env, disable_rollback=True)
self.stack = stack
stack.store()
self.created_time = stack.created_time
self.create_at = timeutils.isotime(self.created_time)
stack.create()
self.expected = {}
for action in ('create', 'suspend', 'delete'):
self.make_mocks(action)
def make_mocks(self, action):
stack_arn = self.stack.identifier().arn()
self.expected[action] = [
mock.call(self.ctx,
'orchestration.test_host',
'orchestration.%s.start' % action,
'INFO',
{'state_reason': 'Stack %s started' % action.upper(),
'user_id': 'test_username',
'stack_identity': stack_arn,
'tenant_id': 'test_tenant',
'create_at': self.create_at,
'stack_name': self.stack_name,
'state': '%s_IN_PROGRESS' % action.upper()}),
mock.call(self.ctx, 'orchestration.test_host',
'orchestration.%s.end' % action,
'INFO',
{'state_reason':
'Stack %s completed successfully' % action,
'user_id': 'test_username',
'stack_identity': stack_arn,
'tenant_id': 'test_tenant',
'create_at': self.create_at,
'stack_name': self.stack_name,
'state': '%s_COMPLETE' % action.upper()})]
@utils.stack_delete_after
def test_create_stack(self):
with mock.patch('heat.openstack.common.notifier.api.notify') \
as mock_notify:
self.create_test_stack()
self.assertEqual(self.stack.state, (self.stack.CREATE,
self.stack.COMPLETE))
self.assertEqual(self.expected['create'],
mock_notify.call_args_list)
@utils.stack_delete_after
def test_create_and_suspend_stack(self):
with mock.patch('heat.openstack.common.notifier.api.notify') \
as mock_notify:
self.create_test_stack()
self.assertEqual(self.stack.state, (self.stack.CREATE,
self.stack.COMPLETE))
self.assertEqual(self.expected['create'],
mock_notify.call_args_list)
self.stack.suspend()
self.assertEqual(self.stack.state, (self.stack.SUSPEND,
self.stack.COMPLETE))
expected = self.expected['create'] + self.expected['suspend']
self.assertEqual(expected,
mock_notify.call_args_list)
@utils.stack_delete_after
def test_create_and_delete_stack(self):
with mock.patch('heat.openstack.common.notifier.api.notify') \
as mock_notify:
self.create_test_stack()
self.assertEqual(self.stack.state, (self.stack.CREATE,
self.stack.COMPLETE))
self.assertEqual(self.expected['create'],
mock_notify.call_args_list)
self.stack.delete()
self.assertEqual(self.stack.state, (self.stack.DELETE,
self.stack.COMPLETE))
expected = self.expected['create'] + self.expected['delete']
self.assertEqual(expected,
mock_notify.call_args_list)