Merge lp:~mdragon/nova/system-usages into lp:~hudson-openstack/nova/trunk
- system-usages
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
System Usages for billing
(Medium)
|
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 |
Commit message
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.
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_
154 + user_id=
155 + instance_
156 + instance_
157 + instance_
158 + display_
159 + created_
160 + launched_
161 + if instance_
162 + image_ref=
And make a utility method, like "usage_
- 1086. By Monsyne Dragon
-
Refactored usage generation
Monsyne Dragon (mdragon) wrote : | # |
ok, I refactored that method. Was debating that anyway.
Preview Diff
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 |
all my comments were discussed verbally. looks good.