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:
parent
a1301baaa4
commit
4a42070fee
@ -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 = {
|
||||
|
53
heat/engine/notification.py
Normal file
53
heat/engine/notification.py
Normal 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))
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
145
heat/tests/test_notifications.py
Normal file
145
heat/tests/test_notifications.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user