Merge lp:~mdragon/nova/system-usages into lp:~hudson-openstack/nova/trunk

Proposed by Monsyne Dragon
Status: Merged
Approved by: Matt Dietz
Approved revision: 1086
Merged at revision: 1222
Proposed branch: lp:~mdragon/nova/system-usages
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 436 lines (+296/-0)
7 files modified
bin/instance-usage-audit (+116/-0)
nova/compute/manager.py (+36/-0)
nova/db/api.py (+5/-0)
nova/db/sqlalchemy/api.py (+18/-0)
nova/notifier/test_notifier.py (+28/-0)
nova/tests/test_compute.py (+77/-0)
nova/utils.py (+16/-0)
To merge this branch: bzr merge lp:~mdragon/nova/system-usages
Reviewer Review Type Date Requested Status
Matt Dietz (community) Approve
Ed Leafe (community) Approve
Trey Morris (community) Approve
Review via email: mp+66178@code.launchpad.net

Description of the change

This adds system usage notifications using the notifications framework.
These are designed to feed an external billing or similar system that subscribes to the nova feed and does the analysis.

To post a comment you must log in.
Revision history for this message
Trey Morris (tr3buchet) wrote :

all my comments were discussed verbally. looks good.

review: Approve
Revision history for this message
Ed Leafe (ed-leafe) wrote :

lgtm

review: Approve
Revision history for this message
Matt Dietz (cerberus) wrote :

Good work on this! Tests look good.

However, looks like there's some room for cleanup

Couldn't we just take lines:

152 + usage_info = dict(
153 + tenant_id=instance_ref['project_id'],
154 + user_id=instance_ref['user_id'],
155 + instance_id=instance_ref['id'],
156 + instance_type=instance_ref['instance_type']['name'],
157 + instance_type_id=instance_ref['instance_type_id'],
158 + display_name=instance_ref['display_name'],
159 + created_at=str(instance_ref['created_at']),
160 + launched_at=str(instance_ref['launched_at']) \
161 + if instance_ref['launched_at'] else '',
162 + image_ref=instance_ref['image_ref'])

And make a utility method, like "usage_from_instance" ? You repeat the pattern 3 times.

review: Needs Fixing
lp:~mdragon/nova/system-usages updated
1086. By Monsyne Dragon

Refactored usage generation

Revision history for this message
Monsyne Dragon (mdragon) wrote :

ok, I refactored that method. Was debating that anyway.

Revision history for this message
Matt Dietz (cerberus) wrote :

Cool beans

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'bin/instance-usage-audit'
2--- bin/instance-usage-audit 1970-01-01 00:00:00 +0000
3+++ bin/instance-usage-audit 2011-06-28 20:37:32 +0000
4@@ -0,0 +1,116 @@
5+#!/usr/bin/env python
6+# vim: tabstop=4 shiftwidth=4 softtabstop=4
7+
8+# Copyright (c) 2011 Openstack, LLC.
9+# All Rights Reserved.
10+#
11+# Licensed under the Apache License, Version 2.0 (the "License"); you may
12+# not use this file except in compliance with the License. You may obtain
13+# a copy of the License at
14+#
15+# http://www.apache.org/licenses/LICENSE-2.0
16+#
17+# Unless required by applicable law or agreed to in writing, software
18+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
19+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
20+# License for the specific language governing permissions and limitations
21+# under the License.
22+
23+"""Cron script to generate usage notifications for instances neither created
24+ nor destroyed in a given time period.
25+
26+ Together with the notifications generated by compute on instance
27+ create/delete/resize, over that ime period, this allows an external
28+ system consuming usage notification feeds to calculate instance usage
29+ for each tenant.
30+
31+ Time periods are specified like so:
32+ <number>[mdy]
33+
34+ 1m = previous month. If the script is run April 1, it will generate usages
35+ for March 1 thry March 31.
36+ 3m = 3 previous months.
37+ 90d = previous 90 days.
38+ 1y = previous year. If run on Jan 1, it generates usages for
39+ Jan 1 thru Dec 31 of the previous year.
40+"""
41+
42+import datetime
43+import gettext
44+import os
45+import sys
46+import time
47+
48+# If ../nova/__init__.py exists, add ../ to Python search path, so that
49+# it will override what happens to be installed in /usr/(local/)lib/python...
50+POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
51+ os.pardir,
52+ os.pardir))
53+if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')):
54+ sys.path.insert(0, POSSIBLE_TOPDIR)
55+
56+gettext.install('nova', unicode=1)
57+
58+
59+from nova import context
60+from nova import db
61+from nova import exception
62+from nova import flags
63+from nova import log as logging
64+from nova import utils
65+
66+from nova.notifier import api as notifier_api
67+
68+FLAGS = flags.FLAGS
69+flags.DEFINE_string('instance_usage_audit_period', '1m',
70+ 'time period to generate instance usages for.')
71+
72+
73+def time_period(period):
74+ today = datetime.date.today()
75+ unit = period[-1]
76+ if unit not in 'mdy':
77+ raise ValueError('Time period must be m, d, or y')
78+ n = int(period[:-1])
79+ if unit == 'm':
80+ year = today.year - (n // 12)
81+ n = n % 12
82+ if n >= today.month:
83+ year -= 1
84+ month = 12 + (today.month - n)
85+ else:
86+ month = today.month - n
87+ begin = datetime.datetime(day=1, month=month, year=year)
88+ end = datetime.datetime(day=1, month=today.month, year=today.year)
89+
90+ elif unit == 'y':
91+ begin = datetime.datetime(day=1, month=1, year=today.year - n)
92+ end = datetime.datetime(day=1, month=1, year=today.year)
93+
94+ elif unit == 'd':
95+ b = today - datetime.timedelta(days=n)
96+ begin = datetime.datetime(day=b.day, month=b.month, year=b.year)
97+ end = datetime.datetime(day=today.day,
98+ month=today.month,
99+ year=today.year)
100+
101+ return (begin, end)
102+
103+if __name__ == '__main__':
104+ utils.default_flagfile()
105+ flags.FLAGS(sys.argv)
106+ logging.setup()
107+ begin, end = time_period(FLAGS.instance_usage_audit_period)
108+ print "Creating usages for %s until %s" % (str(begin), str(end))
109+ instances = db.instance_get_active_by_window(context.get_admin_context(),
110+ begin,
111+ end)
112+ print "%s instances" % len(instances)
113+ for instance_ref in instances:
114+ usage_info = utils.usage_from_instance(instance_ref,
115+ audit_period_begining=str(begin),
116+ audit_period_ending=str(end))
117+ notifier_api.notify('compute.%s' % FLAGS.host,
118+ 'compute.instance.exists',
119+ notifier_api.INFO,
120+ usage_info)
121
122=== modified file 'nova/compute/manager.py'
123--- nova/compute/manager.py 2011-06-24 12:01:51 +0000
124+++ nova/compute/manager.py 2011-06-28 20:37:32 +0000
125@@ -49,10 +49,12 @@
126 from nova import log as logging
127 from nova import manager
128 from nova import network
129+from nova import notifier
130 from nova import rpc
131 from nova import utils
132 from nova import volume
133 from nova.compute import power_state
134+from nova.notifier import api as notifier_api
135 from nova.compute.utils import terminate_volumes
136 from nova.virt import driver
137
138@@ -343,6 +345,11 @@
139
140 self._update_launched_at(context, instance_id)
141 self._update_state(context, instance_id)
142+ usage_info = utils.usage_from_instance(instance_ref)
143+ notifier_api.notify('compute.%s' % self.host,
144+ 'compute.instance.create',
145+ notifier_api.INFO,
146+ usage_info)
147 except exception.InstanceNotFound:
148 # FIXME(wwolf): We are just ignoring InstanceNotFound
149 # exceptions here in case the instance was immediately
150@@ -421,9 +428,15 @@
151 def terminate_instance(self, context, instance_id):
152 """Terminate an instance on this host."""
153 self._shutdown_instance(context, instance_id, 'Terminating')
154+ instance_ref = self.db.instance_get(context.elevated(), instance_id)
155
156 # TODO(ja): should we keep it in a terminated state for a bit?
157 self.db.instance_destroy(context, instance_id)
158+ usage_info = utils.usage_from_instance(instance_ref)
159+ notifier_api.notify('compute.%s' % self.host,
160+ 'compute.instance.delete',
161+ notifier_api.INFO,
162+ usage_info)
163
164 @exception.wrap_exception
165 @checks_instance_lock
166@@ -460,6 +473,12 @@
167 self._update_image_ref(context, instance_id, image_ref)
168 self._update_launched_at(context, instance_id)
169 self._update_state(context, instance_id)
170+ usage_info = utils.usage_from_instance(instance_ref,
171+ image_ref=image_ref)
172+ notifier_api.notify('compute.%s' % self.host,
173+ 'compute.instance.rebuild',
174+ notifier_api.INFO,
175+ usage_info)
176
177 @exception.wrap_exception
178 @checks_instance_lock
179@@ -637,6 +656,11 @@
180 context = context.elevated()
181 instance_ref = self.db.instance_get(context, instance_id)
182 self.driver.destroy(instance_ref)
183+ usage_info = utils.usage_from_instance(instance_ref)
184+ notifier_api.notify('compute.%s' % self.host,
185+ 'compute.instance.resize.confirm',
186+ notifier_api.INFO,
187+ usage_info)
188
189 @exception.wrap_exception
190 @checks_instance_lock
191@@ -684,6 +708,11 @@
192 self.driver.revert_resize(instance_ref)
193 self.db.migration_update(context, migration_id,
194 {'status': 'reverted'})
195+ usage_info = utils.usage_from_instance(instance_ref)
196+ notifier_api.notify('compute.%s' % self.host,
197+ 'compute.instance.resize.revert',
198+ notifier_api.INFO,
199+ usage_info)
200
201 @exception.wrap_exception
202 @checks_instance_lock
203@@ -720,6 +749,13 @@
204 'migration_id': migration_ref['id'],
205 'instance_id': instance_id, },
206 })
207+ usage_info = utils.usage_from_instance(instance_ref,
208+ new_instance_type=instance_type['name'],
209+ new_instance_type_id=instance_type['id'])
210+ notifier_api.notify('compute.%s' % self.host,
211+ 'compute.instance.resize.prep',
212+ notifier_api.INFO,
213+ usage_info)
214
215 @exception.wrap_exception
216 @checks_instance_lock
217
218=== modified file 'nova/db/api.py'
219--- nova/db/api.py 2011-06-28 13:04:19 +0000
220+++ nova/db/api.py 2011-06-28 20:37:32 +0000
221@@ -442,6 +442,11 @@
222 return IMPL.instance_get_all(context)
223
224
225+def instance_get_active_by_window(context, begin, end=None):
226+ """Get instances active during a certain time window."""
227+ return IMPL.instance_get_active_by_window(context, begin, end)
228+
229+
230 def instance_get_all_by_user(context, user_id):
231 """Get all instances."""
232 return IMPL.instance_get_all_by_user(context, user_id)
233
234=== modified file 'nova/db/sqlalchemy/api.py'
235--- nova/db/sqlalchemy/api.py 2011-06-28 13:04:19 +0000
236+++ nova/db/sqlalchemy/api.py 2011-06-28 20:37:32 +0000
237@@ -956,6 +956,24 @@
238
239
240 @require_admin_context
241+def instance_get_active_by_window(context, begin, end=None):
242+ """Return instances that were continuously active over the given window"""
243+ session = get_session()
244+ query = session.query(models.Instance).\
245+ options(joinedload_all('fixed_ip.floating_ips')).\
246+ options(joinedload('security_groups')).\
247+ options(joinedload_all('fixed_ip.network')).\
248+ options(joinedload('instance_type')).\
249+ filter(models.Instance.launched_at < begin)
250+ if end:
251+ query = query.filter(or_(models.Instance.terminated_at == None,
252+ models.Instance.terminated_at > end))
253+ else:
254+ query = query.filter(models.Instance.terminated_at == None)
255+ return query.all()
256+
257+
258+@require_admin_context
259 def instance_get_all_by_user(context, user_id):
260 session = get_session()
261 return session.query(models.Instance).\
262
263=== added file 'nova/notifier/test_notifier.py'
264--- nova/notifier/test_notifier.py 1970-01-01 00:00:00 +0000
265+++ nova/notifier/test_notifier.py 2011-06-28 20:37:32 +0000
266@@ -0,0 +1,28 @@
267+# Copyright 2011 OpenStack LLC.
268+# All Rights Reserved.
269+#
270+# Licensed under the Apache License, Version 2.0 (the "License"); you may
271+# not use this file except in compliance with the License. You may obtain
272+# a copy of the License at
273+#
274+# http://www.apache.org/licenses/LICENSE-2.0
275+#
276+# Unless required by applicable law or agreed to in writing, software
277+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
278+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
279+# License for the specific language governing permissions and limitations
280+# under the License.
281+
282+import json
283+
284+from nova import flags
285+from nova import log as logging
286+
287+FLAGS = flags.FLAGS
288+
289+NOTIFICATIONS = []
290+
291+
292+def notify(message):
293+ """Test notifier, stores notifications in memory for unittests."""
294+ NOTIFICATIONS.append(message)
295
296=== modified file 'nova/tests/test_compute.py'
297--- nova/tests/test_compute.py 2011-06-24 12:01:51 +0000
298+++ nova/tests/test_compute.py 2011-06-28 20:37:32 +0000
299@@ -37,6 +37,7 @@
300 from nova import rpc
301 from nova import test
302 from nova import utils
303+from nova.notifier import test_notifier
304
305 LOG = logging.getLogger('nova.tests.compute')
306 FLAGS = flags.FLAGS
307@@ -62,6 +63,7 @@
308 super(ComputeTestCase, self).setUp()
309 self.flags(connection_type='fake',
310 stub_network=True,
311+ notification_driver='nova.notifier.test_notifier',
312 network_manager='nova.network.manager.FlatManager')
313 self.compute = utils.import_object(FLAGS.compute_manager)
314 self.compute_api = compute.API()
315@@ -69,6 +71,7 @@
316 self.user = self.manager.create_user('fake', 'fake', 'fake')
317 self.project = self.manager.create_project('fake', 'fake', 'fake')
318 self.context = context.RequestContext('fake', 'fake', False)
319+ test_notifier.NOTIFICATIONS = []
320
321 def fake_show(meh, context, id):
322 return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}}
323@@ -327,6 +330,50 @@
324 self.assert_(console)
325 self.compute.terminate_instance(self.context, instance_id)
326
327+ def test_run_instance_usage_notification(self):
328+ """Ensure run instance generates apropriate usage notification"""
329+ instance_id = self._create_instance()
330+ self.compute.run_instance(self.context, instance_id)
331+ self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
332+ msg = test_notifier.NOTIFICATIONS[0]
333+ self.assertEquals(msg['priority'], 'INFO')
334+ self.assertEquals(msg['event_type'], 'compute.instance.create')
335+ payload = msg['payload']
336+ self.assertEquals(payload['tenant_id'], self.project.id)
337+ self.assertEquals(payload['user_id'], self.user.id)
338+ self.assertEquals(payload['instance_id'], instance_id)
339+ self.assertEquals(payload['instance_type'], 'm1.tiny')
340+ type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
341+ self.assertEquals(str(payload['instance_type_id']), str(type_id))
342+ self.assertTrue('display_name' in payload)
343+ self.assertTrue('created_at' in payload)
344+ self.assertTrue('launched_at' in payload)
345+ self.assertEquals(payload['image_ref'], '1')
346+ self.compute.terminate_instance(self.context, instance_id)
347+
348+ def test_terminate_usage_notification(self):
349+ """Ensure terminate_instance generates apropriate usage notification"""
350+ instance_id = self._create_instance()
351+ self.compute.run_instance(self.context, instance_id)
352+ test_notifier.NOTIFICATIONS = []
353+ self.compute.terminate_instance(self.context, instance_id)
354+
355+ self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
356+ msg = test_notifier.NOTIFICATIONS[0]
357+ self.assertEquals(msg['priority'], 'INFO')
358+ self.assertEquals(msg['event_type'], 'compute.instance.delete')
359+ payload = msg['payload']
360+ self.assertEquals(payload['tenant_id'], self.project.id)
361+ self.assertEquals(payload['user_id'], self.user.id)
362+ self.assertEquals(payload['instance_id'], instance_id)
363+ self.assertEquals(payload['instance_type'], 'm1.tiny')
364+ type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
365+ self.assertEquals(str(payload['instance_type_id']), str(type_id))
366+ self.assertTrue('display_name' in payload)
367+ self.assertTrue('created_at' in payload)
368+ self.assertTrue('launched_at' in payload)
369+ self.assertEquals(payload['image_ref'], '1')
370+
371 def test_run_instance_existing(self):
372 """Ensure failure when running an instance that already exists"""
373 instance_id = self._create_instance()
374@@ -378,6 +425,36 @@
375
376 self.compute.terminate_instance(self.context, instance_id)
377
378+ def test_resize_instance_notification(self):
379+ """Ensure notifications on instance migrate/resize"""
380+ instance_id = self._create_instance()
381+ context = self.context.elevated()
382+
383+ self.compute.run_instance(self.context, instance_id)
384+ test_notifier.NOTIFICATIONS = []
385+
386+ db.instance_update(self.context, instance_id, {'host': 'foo'})
387+ self.compute.prep_resize(context, instance_id, 1)
388+ migration_ref = db.migration_get_by_instance_and_status(context,
389+ instance_id, 'pre-migrating')
390+
391+ self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
392+ msg = test_notifier.NOTIFICATIONS[0]
393+ self.assertEquals(msg['priority'], 'INFO')
394+ self.assertEquals(msg['event_type'], 'compute.instance.resize.prep')
395+ payload = msg['payload']
396+ self.assertEquals(payload['tenant_id'], self.project.id)
397+ self.assertEquals(payload['user_id'], self.user.id)
398+ self.assertEquals(payload['instance_id'], instance_id)
399+ self.assertEquals(payload['instance_type'], 'm1.tiny')
400+ type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
401+ self.assertEquals(str(payload['instance_type_id']), str(type_id))
402+ self.assertTrue('display_name' in payload)
403+ self.assertTrue('created_at' in payload)
404+ self.assertTrue('launched_at' in payload)
405+ self.assertEquals(payload['image_ref'], '1')
406+ self.compute.terminate_instance(context, instance_id)
407+
408 def test_resize_instance(self):
409 """Ensure instance can be migrated/resized"""
410 instance_id = self._create_instance()
411
412=== modified file 'nova/utils.py'
413--- nova/utils.py 2011-06-28 14:43:25 +0000
414+++ nova/utils.py 2011-06-28 20:37:32 +0000
415@@ -282,6 +282,22 @@
416 'ABCDEFGHJKLMNPQRSTUVWXYZ') # Removed: I, O
417
418
419+def usage_from_instance(instance_ref, **kw):
420+ usage_info = dict(
421+ tenant_id=instance_ref['project_id'],
422+ user_id=instance_ref['user_id'],
423+ instance_id=instance_ref['id'],
424+ instance_type=instance_ref['instance_type']['name'],
425+ instance_type_id=instance_ref['instance_type_id'],
426+ display_name=instance_ref['display_name'],
427+ created_at=str(instance_ref['created_at']),
428+ launched_at=str(instance_ref['launched_at']) \
429+ if instance_ref['launched_at'] else '',
430+ image_ref=instance_ref['image_ref'])
431+ usage_info.update(kw)
432+ return usage_info
433+
434+
435 def generate_password(length=20, symbols=DEFAULT_PASSWORD_SYMBOLS):
436 """Generate a random password from the supplied symbols.
437