From 75a87df739effe840e6cb39c976002e99b49c796 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Wed, 22 Jun 2011 13:31:28 -0500 Subject: [PATCH 01/39] Round 1 of backup with rotation. --- nova/compute/api.py | 32 ++++++++++++++++++++++++++++++-- nova/compute/manager.py | 20 ++++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index a7ea88d51a63..365aa1c5d645 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -701,18 +701,46 @@ class API(base.Base): raise exception.Error(_("Unable to find host for Instance %s") % instance_id) + def backup(self, context, instance_id, backup_type, rotation): + """Backup the given instance + + instance_id - int - id representing the instance + backup_type - str - whether it's 'daily' or 'weekly' + rotation - int - number of backups to keep around + """ + name = backup_type # daily backups are called 'daily' + recv_meta = self._snapshot(context, instance_id, name, backup_type, + rotation=rotation) + return recv_meta + def snapshot(self, context, instance_id, name): """Snapshot the given instance. :returns: A dict containing image metadata """ - properties = {'instance_id': str(instance_id), + return self._snapshot(context, instance_id, name, 'snapshot') + + def _snapshot(self, context, instance_id, name, image_type, rotation=None): + """Snapshot an instance on this host. + + :param context: security context + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param name: string for name of the snapshot + :param image_type: snapshot | daily | weekly + :param rotation: int representing how many backups to keep around; + None if rotation shouldn't be used (as in the case of snapshots) + """ + instance = db.api.instance_get(context, instance_id) + properties = {'instance_uuid': instance['uuid'], 'user_id': str(context.user_id), 'image_state': 'creating'} + if image_type != 'snapshot': + properties['backup_type'] = image_type sent_meta = {'name': name, 'is_public': False, 'status': 'creating', 'properties': properties} recv_meta = self.image_service.create(context, sent_meta) - params = {'image_id': recv_meta['id']} + params = {'image_id': recv_meta['id'], 'image_type': image_type, + 'rotation': rotation} self._cast_compute_message('snapshot_instance', context, instance_id, params=params) return recv_meta diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 4e006e677fd9..bc6981c58787 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -473,8 +473,17 @@ class ComputeManager(manager.SchedulerDependentManager): self._update_state(context, instance_id) @exception.wrap_exception - def snapshot_instance(self, context, instance_id, image_id): - """Snapshot an instance on this host.""" + def snapshot_instance(self, context, instance_id, image_id, + image_type='snapshot', rotation=None): + """Snapshot an instance on this host. + + :param context: security context + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param image_id: glance.db.sqlalchemy.models.Image.Id + :param image_type: snapshot | daily | weekly + :param rotation: int representing how many backups to keep around; + None if rotation shouldn't be used (as in the case of snapshots) + """ context = context.elevated() instance_ref = self.db.instance_get(context, instance_id) @@ -493,6 +502,13 @@ class ComputeManager(manager.SchedulerDependentManager): 'expected: %(running)s)') % locals()) self.driver.snapshot(instance_ref, image_id) + if rotation: + self.rotate_backups(context, instance_id, image_type, rotation) + + def rotate_backups(self, context, instance_id, image_type, rotation): + """ + """ + pass @exception.wrap_exception @checks_instance_lock From ab2a77d0c6f738fe70b5d5a77fa7f97bf1f1f88b Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Wed, 22 Jun 2011 16:14:01 -0500 Subject: [PATCH 02/39] Adding backup rotation --- nova/compute/api.py | 7 +++---- nova/compute/manager.py | 40 ++++++++++++++++++++++++++++++++++------ nova/exception.py | 4 ++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 365aa1c5d645..c0cb2e18ac2c 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -722,7 +722,7 @@ class API(base.Base): def _snapshot(self, context, instance_id, name, image_type, rotation=None): """Snapshot an instance on this host. - + :param context: security context :param instance_id: nova.db.sqlalchemy.models.Instance.Id :param name: string for name of the snapshot @@ -733,9 +733,8 @@ class API(base.Base): instance = db.api.instance_get(context, instance_id) properties = {'instance_uuid': instance['uuid'], 'user_id': str(context.user_id), - 'image_state': 'creating'} - if image_type != 'snapshot': - properties['backup_type'] = image_type + 'image_state': 'creating', + 'image_type': image_type} sent_meta = {'name': name, 'is_public': False, 'status': 'creating', 'properties': properties} recv_meta = self.image_service.create(context, sent_meta) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index bc6981c58787..44abd5d8932a 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -43,6 +43,7 @@ import time import functools from eventlet import greenthread +from operator import itemgetter from nova import exception from nova import flags @@ -476,7 +477,7 @@ class ComputeManager(manager.SchedulerDependentManager): def snapshot_instance(self, context, instance_id, image_id, image_type='snapshot', rotation=None): """Snapshot an instance on this host. - + :param context: security context :param instance_id: nova.db.sqlalchemy.models.Instance.Id :param image_id: glance.db.sqlalchemy.models.Image.Id @@ -502,13 +503,40 @@ class ComputeManager(manager.SchedulerDependentManager): 'expected: %(running)s)') % locals()) self.driver.snapshot(instance_ref, image_id) - if rotation: - self.rotate_backups(context, instance_id, image_type, rotation) + if rotation and image_type == 'snapshot': + raise exception.ImageRotationNotAllowed + elif rotation: + instance_uuid = instance_ref['uuid'] + self.rotate_backups(context, instance_uuid, image_type, rotation) - def rotate_backups(self, context, instance_id, image_type, rotation): + def rotate_backups(self, context, instance_uuid, image_type, rotation): + """Delete excess backups associated to an instance. + + Instances are allowed a fixed number of backups (the rotation number); + this method deletes the oldest backups that exceed the rotation + threshold. + + :param context: security context + :param instance_uuid: string representing uuid of instance + :param image_type: snapshot | daily | weekly + :param rotation: int representing how many backups to keep around; + None if rotation shouldn't be used (as in the case of snapshots) """ - """ - pass + image_service = nova.image.get_default_image_service() + filters = {'property-image-type': image_type, + 'property-instance-uuid': instance_uuid} + images = image_service.detail(context, filters=filters) + if len(images) > rotation: + # Sort oldest (by created_at) to end of list + images.sort(key=itemgetter('created_at'), reverse=True) + + # NOTE(sirp): this deletes all backups that exceed the rotation + # limit + excess = len(images) - rotation + for i in xrange(excess): + image = images.pop() + image_id = image['id'] + image_service.delete(context, image_id) @exception.wrap_exception @checks_instance_lock diff --git a/nova/exception.py b/nova/exception.py index f3a452228cf4..a548a638ca93 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -549,6 +549,10 @@ class GlobalRoleNotAllowed(NotAllowed): message = _("Unable to use global role %(role_id)s") +class ImageRotationNotAllowed(NovaException): + message = _("Rotation is not allowed for snapshots") + + #TODO(bcwaldon): EOL this exception! class Duplicate(NovaException): pass From f6964aadc5b073152d221bb0a4e899c2b17d174c Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Thu, 23 Jun 2011 14:27:13 -0500 Subject: [PATCH 03/39] Small refactoring around getting params --- nova/api/openstack/images.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 5ffd8e96afd3..54f8e05a9986 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -88,28 +88,53 @@ class Controller(object): return webob.exc.HTTPNoContent() def create(self, req, body): - """Snapshot a server instance and save the image. + """Snapshot or backup a server instance and save the image. + + Images now have an `image_type` associated with them, which can be + 'snapshot' or the backup type, like 'daily' or 'weekly'. + + If the image_type is backup-like, then the rotation factor can be + included and that will cause the oldest backups that exceed the + rotation factor to be deleted. :param req: `wsgi.Request` object """ + def get_param(param): + try: + return body["image"][param] + except KeyError: + raise webob.exc.HTTPBadRequest() + context = req.environ['nova.context'] content_type = req.get_content_type() if not body: raise webob.exc.HTTPBadRequest() + image_type = body["image"].get("image_type", "snapshot") + try: server_id = self._server_id_from_req_data(body) - image_name = body["image"]["name"] except KeyError: raise webob.exc.HTTPBadRequest() - image = self._compute_service.snapshot(context, server_id, image_name) + if image_type == "snapshot": + image_name = get_param("name") + image = self._compute_service.snapshot(context, server_id, + image_name) + else: + if not FLAGS.allow_admin_api: + raise webob.exc.HTTPBadRequest() + + rotation = get_param("rotation") + image = self._compute_service.backup(context, server_id, + image_type, rotation) + return dict(image=self.get_builder(req).build(image, detail=True)) def get_builder(self, request): """Indicates that you must use a Controller subclass.""" - raise NotImplementedError + raise NotImplementedError() def _server_id_from_req_data(self, data): raise NotImplementedError() From 63a9216ecbaab20fc7dfb82afb9fe0e2f3fbded4 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 23 Jun 2011 15:35:26 -0500 Subject: [PATCH 04/39] Adding missing import. --- nova/compute/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 44abd5d8932a..3c849286ec79 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -47,6 +47,7 @@ from operator import itemgetter from nova import exception from nova import flags +import nova.image from nova import log as logging from nova import manager from nova import network From 2028222a5ed47dc82b49f51969d237c4eece50e7 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 23 Jun 2011 16:17:54 -0500 Subject: [PATCH 05/39] Fixed filter property and added logging. --- nova/compute/manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 3c849286ec79..d0ca1ff0d68a 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -524,9 +524,10 @@ class ComputeManager(manager.SchedulerDependentManager): None if rotation shouldn't be used (as in the case of snapshots) """ image_service = nova.image.get_default_image_service() - filters = {'property-image-type': image_type, - 'property-instance-uuid': instance_uuid} + filters = {'property-image_type': image_type, + 'property-instance_uuid': instance_uuid} images = image_service.detail(context, filters=filters) + LOG.debug(_("Found %d images (rotation: %d)" % (len(images), rotation))) if len(images) > rotation: # Sort oldest (by created_at) to end of list images.sort(key=itemgetter('created_at'), reverse=True) @@ -534,9 +535,11 @@ class ComputeManager(manager.SchedulerDependentManager): # NOTE(sirp): this deletes all backups that exceed the rotation # limit excess = len(images) - rotation + LOG.debug(_("Rotating out %d backups" % excess)) for i in xrange(excess): image = images.pop() image_id = image['id'] + LOG.debug(_("Deleting image %d" % image_id)) image_service.delete(context, image_id) @exception.wrap_exception From e3c1a6742b16add04d76631b9dbd4f2ef016e0b3 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 23 Jun 2011 16:19:08 -0500 Subject: [PATCH 06/39] PEP8 cleanup. --- nova/compute/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d0ca1ff0d68a..4bd7d434ec07 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -527,7 +527,8 @@ class ComputeManager(manager.SchedulerDependentManager): filters = {'property-image_type': image_type, 'property-instance_uuid': instance_uuid} images = image_service.detail(context, filters=filters) - LOG.debug(_("Found %d images (rotation: %d)" % (len(images), rotation))) + LOG.debug(_("Found %d images (rotation: %d)" % + (len(images), rotation))) if len(images) > rotation: # Sort oldest (by created_at) to end of list images.sort(key=itemgetter('created_at'), reverse=True) From 2d0d1e179dd8870967ebf00a82fbc7d21bed6116 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 23 Jun 2011 16:28:59 -0500 Subject: [PATCH 07/39] Cast rotation to int. --- nova/api/openstack/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 54f8e05a9986..d8dbd2360a6c 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -126,7 +126,7 @@ class Controller(object): if not FLAGS.allow_admin_api: raise webob.exc.HTTPBadRequest() - rotation = get_param("rotation") + rotation = int(get_param("rotation")) image = self._compute_service.backup(context, server_id, image_type, rotation) From a045cd5fdd00b3e52f46181017077146abe8df9f Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 23 Jun 2011 16:54:28 -0500 Subject: [PATCH 08/39] Fixed syntax errors. --- nova/compute/manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 4bd7d434ec07..ca66d03874ec 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -527,9 +527,10 @@ class ComputeManager(manager.SchedulerDependentManager): filters = {'property-image_type': image_type, 'property-instance_uuid': instance_uuid} images = image_service.detail(context, filters=filters) - LOG.debug(_("Found %d images (rotation: %d)" % - (len(images), rotation))) - if len(images) > rotation: + num_images = len(images) + LOG.debug(_("Found %(num_images)d images (rotation: %(rotation)d)" + % locals())) + if num_images > rotation: # Sort oldest (by created_at) to end of list images.sort(key=itemgetter('created_at'), reverse=True) From c941234c86fc02cf652f2e91ee958260d83fc4d7 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 24 Jun 2011 10:50:09 -0500 Subject: [PATCH 09/39] Adding tests for snapshot no-name and backup no-name --- nova/tests/api/openstack/fakes.py | 9 +++ nova/tests/api/openstack/test_images.py | 88 ++++++++++++++++++------- 2 files changed, 72 insertions(+), 25 deletions(-) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index f8d158ddd750..0a2584910623 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -146,6 +146,15 @@ def stub_out_compute_api_snapshot(stubs): stubs.Set(nova.compute.API, 'snapshot', snapshot) +def stub_out_compute_api_backup(stubs): + def backup(self, context, instance_id, backup_type, rotation): + return dict(id='123', status='ACTIVE', + properties=dict(instance_id='123', + image_type=backup_type, + rotation=rotation)) + stubs.Set(nova.compute.API, 'backup', backup) + + def stub_out_glance_add_image(stubs, sent_to_glance): """ We return the metadata sent to glance by modifying the sent_to_glance dict diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index e4204809fef8..9fabfeae187a 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -340,6 +340,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.fixtures = self._make_image_fixtures() fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures) fakes.stub_out_compute_api_snapshot(self.stubs) + fakes.stub_out_compute_api_backup(self.stubs) def tearDown(self): """Run after each test.""" @@ -364,10 +365,10 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response_list = response_dict["images"] expected = [{'id': 123, 'name': 'public image'}, - {'id': 124, 'name': 'queued backup'}, - {'id': 125, 'name': 'saving backup'}, - {'id': 126, 'name': 'active backup'}, - {'id': 127, 'name': 'killed backup'}, + {'id': 124, 'name': 'queued snapshot'}, + {'id': 125, 'name': 'saving snapshot'}, + {'id': 126, 'name': 'active snapshot'}, + {'id': 127, 'name': 'killed snapshot'}, {'id': 129, 'name': None}] self.assertDictListMatch(response_list, expected) @@ -617,7 +618,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 124, - 'name': 'queued backup', + 'name': 'queued snapshot', 'serverId': 42, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, @@ -625,7 +626,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 125, - 'name': 'saving backup', + 'name': 'saving snapshot', 'serverId': 42, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, @@ -634,7 +635,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 126, - 'name': 'active backup', + 'name': 'active snapshot', 'serverId': 42, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, @@ -642,7 +643,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 127, - 'name': 'killed backup', + 'name': 'killed snapshot', 'serverId': 42, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, @@ -688,7 +689,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 124, - 'name': 'queued backup', + 'name': 'queued snapshot', 'serverRef': "http://localhost/v1.1/servers/42", 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, @@ -710,7 +711,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 125, - 'name': 'saving backup', + 'name': 'saving snapshot', 'serverRef': "http://localhost/v1.1/servers/42", 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, @@ -733,7 +734,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 126, - 'name': 'active backup', + 'name': 'active snapshot', 'serverRef': "http://localhost/v1.1/servers/42", 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, @@ -755,7 +756,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 127, - 'name': 'killed backup', + 'name': 'killed snapshot', 'serverRef': "http://localhost/v1.1/servers/42", 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, @@ -973,8 +974,43 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(res.status_int, 404) def test_create_image(self): + body = dict(image=dict(serverId='123', name='Snapshot 1')) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(200, response.status_int) - body = dict(image=dict(serverId='123', name='Backup 1')) + def test_create_snapshot_no_name(self): + """Name is required for snapshots + + If an image_type isn't passed, we default to image_type=snapshot, + thus `name` is required + """ + body = dict(image=dict(serverId='123')) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + + def test_create_backup_no_name_with_rotation(self): + """Name isn't required for backups, but rotation is. + + The reason name isn't required is because it defaults to the + image_type. + + Creating a backup is an admin-only operation, as opposed to snapshots + which are available to anybody. + """ + # FIXME(sirp): teardown needed? + FLAGS.allow_admin_api = True + + # FIXME(sirp): should the fact that backups are admin_only be a FLAG + body = dict(image=dict(serverId='123', image_type='daily', + rotation=1)) req = webob.Request.blank('/v1.0/images') req.method = 'POST' req.body = json.dumps(body) @@ -984,7 +1020,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_create_image_no_server_id(self): - body = dict(image=dict(name='Backup 1')) + body = dict(image=dict(name='Snapshot 1')) req = webob.Request.blank('/v1.0/images') req.method = 'POST' req.body = json.dumps(body) @@ -994,7 +1030,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_create_image_v1_1(self): - body = dict(image=dict(serverRef='123', name='Backup 1')) + body = dict(image=dict(serverRef='123', name='Snapshot 1')) req = webob.Request.blank('/v1.1/images') req.method = 'POST' req.body = json.dumps(body) @@ -1004,7 +1040,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_create_image_v1_1_xml_serialization(self): - body = dict(image=dict(serverRef='123', name='Backup 1')) + body = dict(image=dict(serverRef='123', name='Snapshot 1')) req = webob.Request.blank('/v1.1/images') req.method = 'POST' req.body = json.dumps(body) @@ -1037,7 +1073,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_create_image_v1_1_no_server_ref(self): - body = dict(image=dict(name='Backup 1')) + body = dict(image=dict(name='Snapshot 1')) req = webob.Request.blank('/v1.1/images') req.method = 'POST' req.body = json.dumps(body) @@ -1064,18 +1100,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): status='active', properties={}) image_id += 1 - # Backup for User 1 - backup_properties = {'instance_id': '42', 'user_id': '1'} + # Snapshot for User 1 + snapshot_properties = {'instance_id': '42', 'user_id': '1'} for status in ('queued', 'saving', 'active', 'killed'): - add_fixture(id=image_id, name='%s backup' % status, + add_fixture(id=image_id, name='%s snapshot' % status, is_public=False, status=status, - properties=backup_properties) + properties=snapshot_properties) image_id += 1 - # Backup for User 2 - other_backup_properties = {'instance_id': '43', 'user_id': '2'} - add_fixture(id=image_id, name='someone elses backup', is_public=False, - status='active', properties=other_backup_properties) + # Snapshot for User 2 + other_snapshot_properties = {'instance_id': '43', 'user_id': '2'} + add_fixture(id=image_id, name='someone elses snapshot', + is_public=False, status='active', + properties=other_snapshot_properties) + image_id += 1 # Image without a name From 4a32c971893a22a6451eed7e618291ad86c24510 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 24 Jun 2011 10:50:48 -0500 Subject: [PATCH 10/39] Trailing whitespace --- nova/tests/api/openstack/test_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 9fabfeae187a..036e510c96fb 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1008,7 +1008,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): # FIXME(sirp): teardown needed? FLAGS.allow_admin_api = True - # FIXME(sirp): should the fact that backups are admin_only be a FLAG + # FIXME(sirp): should the fact that backups are admin_only be a FLAG body = dict(image=dict(serverId='123', image_type='daily', rotation=1)) req = webob.Request.blank('/v1.0/images') From cbf9f1bef113d54be57e2bb9a79990226afcd90f Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 24 Jun 2011 11:55:43 -0500 Subject: [PATCH 11/39] Adding tests for backup no rotation, invalid image type --- nova/api/openstack/images.py | 6 ++++- nova/tests/api/openstack/test_images.py | 29 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index d8dbd2360a6c..2287ca0f7dcd 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -122,13 +122,17 @@ class Controller(object): image_name = get_param("name") image = self._compute_service.snapshot(context, server_id, image_name) - else: + elif image_type in ("daily", "weekly"): if not FLAGS.allow_admin_api: raise webob.exc.HTTPBadRequest() rotation = int(get_param("rotation")) image = self._compute_service.backup(context, server_id, image_type, rotation) + else: + LOG.error(_("Invalid image_type '%s' passed" % image_type)) + raise webob.exc.HTTPBadRequest() + return dict(image=self.get_builder(req).build(image, detail=True)) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 036e510c96fb..0fad044f117e 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1018,6 +1018,35 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(200, response.status_int) + def test_create_backup_no_rotation(self): + """Rotation is required for backup requests""" + # FIXME(sirp): teardown needed? + FLAGS.allow_admin_api = True + + # FIXME(sirp): should the fact that backups are admin_only be a FLAG + body = dict(image=dict(serverId='123', image_type='daily')) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + + def test_create_image_with_invalid_image_type(self): + """Valid image_types are snapshot | daily | weekly""" + # FIXME(sirp): teardown needed? + FLAGS.allow_admin_api = True + + # FIXME(sirp): should the fact that backups are admin_only be a FLAG + body = dict(image=dict(serverId='123', image_type='monthly', + rotation=1)) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + def test_create_image_no_server_id(self): body = dict(image=dict(name='Snapshot 1')) From 1d3960e3b76e3f75c68f919278a2a227e1f96e48 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 24 Jun 2011 11:56:15 -0500 Subject: [PATCH 12/39] Pep8 fix --- nova/api/openstack/images.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 2287ca0f7dcd..5f88ede96834 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -133,7 +133,6 @@ class Controller(object): LOG.error(_("Invalid image_type '%s' passed" % image_type)) raise webob.exc.HTTPBadRequest() - return dict(image=self.get_builder(req).build(image, detail=True)) def get_builder(self, request): From 594d5c7a98f2b4e6ea2d866f10c67cbdaa88ce0c Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 24 Jun 2011 15:03:01 -0500 Subject: [PATCH 13/39] Refactored backup rotate. --- nova/api/openstack/images.py | 20 +++++++---- nova/compute/api.py | 33 +++++++++++------- nova/compute/manager.py | 29 +++++++++++----- nova/exception.py | 4 +++ nova/tests/api/openstack/fakes.py | 5 +-- nova/tests/api/openstack/test_images.py | 45 ++++++++++++++++++------- 6 files changed, 93 insertions(+), 43 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 5f88ede96834..c535e4e264b8 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -118,19 +118,25 @@ class Controller(object): except KeyError: raise webob.exc.HTTPBadRequest() + image_name = get_param("name") + if image_type == "snapshot": - image_name = get_param("name") - image = self._compute_service.snapshot(context, server_id, - image_name) - elif image_type in ("daily", "weekly"): + image = self._compute_service.snapshot( + context, server_id, image_name) + elif image_type == "backup": + # NOTE(sirp): Unlike snapshot, backup is not a customer facing + # API call; rather, it's used by the internal backup scheduler if not FLAGS.allow_admin_api: raise webob.exc.HTTPBadRequest() + backup_type = get_param("backup_type") rotation = int(get_param("rotation")) - image = self._compute_service.backup(context, server_id, - image_type, rotation) + + image = self._compute_service.backup( + context, server_id, image_name, + backup_type, rotation) else: - LOG.error(_("Invalid image_type '%s' passed" % image_type)) + LOG.error(_("Invalid image_type '%s' passed") % image_type) raise webob.exc.HTTPBadRequest() return dict(image=self.get_builder(req).build(image, detail=True)) diff --git a/nova/compute/api.py b/nova/compute/api.py index c0cb2e18ac2c..9c6f0ef9dc83 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -701,32 +701,38 @@ class API(base.Base): raise exception.Error(_("Unable to find host for Instance %s") % instance_id) - def backup(self, context, instance_id, backup_type, rotation): + def backup(self, context, instance_id, name, backup_type, rotation): """Backup the given instance - instance_id - int - id representing the instance - backup_type - str - whether it's 'daily' or 'weekly' - rotation - int - number of backups to keep around - """ + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param name: name of the backup or snapshot name = backup_type # daily backups are called 'daily' - recv_meta = self._snapshot(context, instance_id, name, backup_type, - rotation=rotation) + :param rotation: int representing how many backups to keep around; + None if rotation shouldn't be used (as in the case of snapshots) + """ + recv_meta = self._create_image(context, instance_id, name, 'backup', + backup_type=backup_type, rotation=rotation) return recv_meta def snapshot(self, context, instance_id, name): """Snapshot the given instance. + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param name: name of the backup or snapshot + :returns: A dict containing image metadata """ - return self._snapshot(context, instance_id, name, 'snapshot') + return self._create_image(context, instance_id, name, 'snapshot') - def _snapshot(self, context, instance_id, name, image_type, rotation=None): - """Snapshot an instance on this host. + def _create_image(self, context, instance_id, name, image_type, + backup_type=None, rotation=None): + """Create snapshot or backup for an instance on this host. :param context: security context :param instance_id: nova.db.sqlalchemy.models.Instance.Id :param name: string for name of the snapshot - :param image_type: snapshot | daily | weekly + :param image_type: snapshot | backup + :param backup_type: daily | weekly :param rotation: int representing how many backups to keep around; None if rotation shouldn't be used (as in the case of snapshots) """ @@ -734,12 +740,13 @@ class API(base.Base): properties = {'instance_uuid': instance['uuid'], 'user_id': str(context.user_id), 'image_state': 'creating', - 'image_type': image_type} + 'image_type': image_type, + 'backup_type': backup_type} sent_meta = {'name': name, 'is_public': False, 'status': 'creating', 'properties': properties} recv_meta = self.image_service.create(context, sent_meta) params = {'image_id': recv_meta['id'], 'image_type': image_type, - 'rotation': rotation} + 'backup_type': backup_type, 'rotation': rotation} self._cast_compute_message('snapshot_instance', context, instance_id, params=params) return recv_meta diff --git a/nova/compute/manager.py b/nova/compute/manager.py index ca66d03874ec..1458ea41f5a0 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -476,13 +476,15 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception def snapshot_instance(self, context, instance_id, image_id, - image_type='snapshot', rotation=None): + image_type='snapshot', backup_type=None, + rotation=None): """Snapshot an instance on this host. :param context: security context :param instance_id: nova.db.sqlalchemy.models.Instance.Id :param image_id: glance.db.sqlalchemy.models.Image.Id - :param image_type: snapshot | daily | weekly + :param image_type: snapshot | backup + :param backup_type: daily | weekly :param rotation: int representing how many backups to keep around; None if rotation shouldn't be used (as in the case of snapshots) """ @@ -504,13 +506,21 @@ class ComputeManager(manager.SchedulerDependentManager): 'expected: %(running)s)') % locals()) self.driver.snapshot(instance_ref, image_id) - if rotation and image_type == 'snapshot': + + if image_type == 'snapshot' and rotation: raise exception.ImageRotationNotAllowed - elif rotation: - instance_uuid = instance_ref['uuid'] - self.rotate_backups(context, instance_uuid, image_type, rotation) + elif image_type == 'backup': + if rotation: + instance_uuid = instance_ref['uuid'] + self.rotate_backups(context, instance_uuid, backup_type, + rotation) + else: + raise exception.RotationRequiredForBackup + else: + raise Exception(_('Image type not recognized %s') % image_type) - def rotate_backups(self, context, instance_uuid, image_type, rotation): + + def rotate_backups(self, context, instance_uuid, backup_type, rotation): """Delete excess backups associated to an instance. Instances are allowed a fixed number of backups (the rotation number); @@ -519,12 +529,13 @@ class ComputeManager(manager.SchedulerDependentManager): :param context: security context :param instance_uuid: string representing uuid of instance - :param image_type: snapshot | daily | weekly + :param backup_type: daily | weekly :param rotation: int representing how many backups to keep around; None if rotation shouldn't be used (as in the case of snapshots) """ image_service = nova.image.get_default_image_service() - filters = {'property-image_type': image_type, + filters = {'property-image_type': 'backup', + 'property-backup_type': backup_type, 'property-instance_uuid': instance_uuid} images = image_service.detail(context, filters=filters) num_images = len(images) diff --git a/nova/exception.py b/nova/exception.py index a548a638ca93..f3893d239d84 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -553,6 +553,10 @@ class ImageRotationNotAllowed(NovaException): message = _("Rotation is not allowed for snapshots") +class RotationRequiredForBackup(NovaException): + message = _("Rotation param is required for backup image_type") + + #TODO(bcwaldon): EOL this exception! class Duplicate(NovaException): pass diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 0a2584910623..ad9c5067c3ac 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -147,10 +147,11 @@ def stub_out_compute_api_snapshot(stubs): def stub_out_compute_api_backup(stubs): - def backup(self, context, instance_id, backup_type, rotation): + def backup(self, context, instance_id, name, backup_type, rotation): return dict(id='123', status='ACTIVE', properties=dict(instance_id='123', - image_type=backup_type, + name=name, + backup_type=backup_type, rotation=rotation)) stubs.Set(nova.compute.API, 'backup', backup) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 0fad044f117e..8ad08080a6ec 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -983,11 +983,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(200, response.status_int) def test_create_snapshot_no_name(self): - """Name is required for snapshots - - If an image_type isn't passed, we default to image_type=snapshot, - thus `name` is required - """ + """Name is required for snapshots""" body = dict(image=dict(serverId='123')) req = webob.Request.blank('/v1.0/images') req.method = 'POST' @@ -996,11 +992,19 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(400, response.status_int) - def test_create_backup_no_name_with_rotation(self): - """Name isn't required for backups, but rotation is. + def test_create_backup_no_name(self): + """Name is also required for backups""" + body = dict(image=dict(serverId='123', image_type='backup', + backup_type='daily', rotation=1)) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) - The reason name isn't required is because it defaults to the - image_type. + def test_create_backup_with_rotation_and_backup_type(self): + """The happy path for creating backups Creating a backup is an admin-only operation, as opposed to snapshots which are available to anybody. @@ -1009,8 +1013,9 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): FLAGS.allow_admin_api = True # FIXME(sirp): should the fact that backups are admin_only be a FLAG - body = dict(image=dict(serverId='123', image_type='daily', - rotation=1)) + body = dict(image=dict(serverId='123', image_type='backup', + name='Backup 1', + backup_type='daily', rotation=1)) req = webob.Request.blank('/v1.0/images') req.method = 'POST' req.body = json.dumps(body) @@ -1024,7 +1029,23 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): FLAGS.allow_admin_api = True # FIXME(sirp): should the fact that backups are admin_only be a FLAG - body = dict(image=dict(serverId='123', image_type='daily')) + body = dict(image=dict(serverId='123', name='daily', + image_type='backup', backup_type='daily')) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + + def test_create_backup_no_backup_type(self): + """Backup Type (daily or weekly) is required for backup requests""" + # FIXME(sirp): teardown needed? + FLAGS.allow_admin_api = True + + # FIXME(sirp): should the fact that backups are admin_only be a FLAG + body = dict(image=dict(serverId='123', name='daily', + image_type='backup', rotation=1)) req = webob.Request.blank('/v1.0/images') req.method = 'POST' req.body = json.dumps(body) From a1b9aea9d12eaa32f869e5a4a59b01788e6c836d Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 24 Jun 2011 15:04:34 -0500 Subject: [PATCH 14/39] PEP8 cleanup. --- nova/compute/api.py | 2 +- nova/compute/manager.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 9c6f0ef9dc83..efd6d166b5fb 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -719,7 +719,7 @@ class API(base.Base): :param instance_id: nova.db.sqlalchemy.models.Instance.Id :param name: name of the backup or snapshot - + :returns: A dict containing image metadata """ return self._create_image(context, instance_id, name, 'snapshot') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 1458ea41f5a0..d4e1d3a1efbd 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -506,7 +506,7 @@ class ComputeManager(manager.SchedulerDependentManager): 'expected: %(running)s)') % locals()) self.driver.snapshot(instance_ref, image_id) - + if image_type == 'snapshot' and rotation: raise exception.ImageRotationNotAllowed elif image_type == 'backup': @@ -519,7 +519,6 @@ class ComputeManager(manager.SchedulerDependentManager): else: raise Exception(_('Image type not recognized %s') % image_type) - def rotate_backups(self, context, instance_uuid, backup_type, rotation): """Delete excess backups associated to an instance. From 3b85d8080ee06436873bd2e4d8f358e4686da1bf Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 24 Jun 2011 15:18:05 -0500 Subject: [PATCH 15/39] Fixed snapshot logic. --- nova/compute/manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d4e1d3a1efbd..8708768fb44e 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -507,8 +507,9 @@ class ComputeManager(manager.SchedulerDependentManager): self.driver.snapshot(instance_ref, image_id) - if image_type == 'snapshot' and rotation: - raise exception.ImageRotationNotAllowed + if image_type == 'snapshot': + if rotation: + raise exception.ImageRotationNotAllowed elif image_type == 'backup': if rotation: instance_uuid = instance_ref['uuid'] From 707c64ba5cb86ae3fc72d7bdc64070d9e562d96b Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 24 Jun 2011 17:19:32 -0500 Subject: [PATCH 16/39] PEP8 cleanup. --- .../migrate_repo/versions/027_add_provider_firewall_rules.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py index 5aa30f7a8b88..7e51d93b7a51 100644 --- a/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py +++ b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py @@ -58,8 +58,7 @@ provider_fw_rules = Table('provider_fw_rules', meta, Column('to_port', Integer()), Column('cidr', String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False)) - ) + unicode_error=None, _warn_on_bytestring=False))) def upgrade(migrate_engine): From 21cbac8334a37f15595088bc0c99f8f04451f1a7 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Mon, 27 Jun 2011 00:20:53 -0400 Subject: [PATCH 17/39] Adding files for building an rpm for xenserver xenapi plugins. --- plugins/xenserver/xenapi/contrib/build.sh | 20 +++++++++++ .../rpmbuild/SPECS/nova-xenapi-plugins.spec | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100755 plugins/xenserver/xenapi/contrib/build.sh create mode 100644 plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/nova-xenapi-plugins.spec diff --git a/plugins/xenserver/xenapi/contrib/build.sh b/plugins/xenserver/xenapi/contrib/build.sh new file mode 100755 index 000000000000..b194ffacec0e --- /dev/null +++ b/plugins/xenserver/xenapi/contrib/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash +PACKAGE=nova-xenapi-plugins +RPMBUILD_DIR=$PWD/rpmbuild +if [ ! -d $RPMBUILD_DIR ]; then + echo $RPMBUILD_DIR is missing + exit 1 +fi + +for dir in BUILD BUILDROOT SRPMS RPMS SOURCES; do + rm -rf $RPMBUILD_DIR/$dir + mkdir -p $RPMBUILD_DIR/$dir +done + +rm -rf /tmp/$PACKAGE +mkdir /tmp/$PACKAGE +cp -r ../etc/xapi.d /tmp/$PACKAGE +tar czf $RPMBUILD_DIR/SOURCES/$PACKAGE.tar.gz -C /tmp $PACKAGE + +rpmbuild -ba --nodeps --define "_topdir $RPMBUILD_DIR" \ + $RPMBUILD_DIR/SPECS/$PACKAGE.spec diff --git a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/nova-xenapi-plugins.spec b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/nova-xenapi-plugins.spec new file mode 100644 index 000000000000..1a61dbbad9a7 --- /dev/null +++ b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/nova-xenapi-plugins.spec @@ -0,0 +1,36 @@ +Name: nova-xenapi-plugins +Version: 1.0 +Release: 1 +Summary: Files for XenAPI support. +License: Apache +Group: Applications/Utilities +Source0: nova-xenapi-plugins.tar.gz +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) + +%define debug_package %{nil} + +%description +This package contains files that are required for XenAPI support for OpenStack. + +%prep +%setup -q -n nova-xenapi-plugins + +%install +rm -rf $RPM_BUILD_ROOT +mkdir -p $RPM_BUILD_ROOT/etc +cp -r xapi.d $RPM_BUILD_ROOT/etc +chmod u+x $RPM_BUILD_ROOT/etc/xapi.d/plugins/objectstore +#%{_fixperms} $RPM_BUILD_ROOT/* + +%clean +rm -rf $RPM_BUILD_ROOT + +%files +%defattr(-,root,root,-) +/etc/xapi.d/plugins/agent +/etc/xapi.d/plugins/glance +/etc/xapi.d/plugins/migration +/etc/xapi.d/plugins/objectstore +/etc/xapi.d/plugins/pluginlib_nova.py +/etc/xapi.d/plugins/xenhost +/etc/xapi.d/plugins/xenstore.py From 998b5ba2c709054c535583195ba489454e384f41 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Mon, 27 Jun 2011 12:04:44 -0400 Subject: [PATCH 18/39] Changed package name to openstack-xen-plugins per dprince's suggestion. All the files in /etc/xapi.d/plugins must be executable. Added dependency on parted. Renamed build.sh to build-rpm.sh. --- .../xenapi/contrib/{build.sh => build-rpm.sh} | 2 +- ...enapi-plugins.spec => openstack-xen-plugins.spec} | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) rename plugins/xenserver/xenapi/contrib/{build.sh => build-rpm.sh} (94%) rename plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/{nova-xenapi-plugins.spec => openstack-xen-plugins.spec} (76%) diff --git a/plugins/xenserver/xenapi/contrib/build.sh b/plugins/xenserver/xenapi/contrib/build-rpm.sh similarity index 94% rename from plugins/xenserver/xenapi/contrib/build.sh rename to plugins/xenserver/xenapi/contrib/build-rpm.sh index b194ffacec0e..f7bed4d84bdc 100755 --- a/plugins/xenserver/xenapi/contrib/build.sh +++ b/plugins/xenserver/xenapi/contrib/build-rpm.sh @@ -1,5 +1,5 @@ #!/bin/bash -PACKAGE=nova-xenapi-plugins +PACKAGE=openstack-xen-plugins RPMBUILD_DIR=$PWD/rpmbuild if [ ! -d $RPMBUILD_DIR ]; then echo $RPMBUILD_DIR is missing diff --git a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/nova-xenapi-plugins.spec b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec similarity index 76% rename from plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/nova-xenapi-plugins.spec rename to plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec index 1a61dbbad9a7..864ca7ff7ea5 100644 --- a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/nova-xenapi-plugins.spec +++ b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec @@ -1,11 +1,12 @@ -Name: nova-xenapi-plugins -Version: 1.0 +Name: openstack-xen-plugins +Version: 2011.3 Release: 1 Summary: Files for XenAPI support. License: Apache Group: Applications/Utilities -Source0: nova-xenapi-plugins.tar.gz +Source0: openstack-xen-plugins.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) +Requires: parted %define debug_package %{nil} @@ -13,14 +14,13 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) This package contains files that are required for XenAPI support for OpenStack. %prep -%setup -q -n nova-xenapi-plugins +%setup -q -n openstack-xen-plugins %install rm -rf $RPM_BUILD_ROOT mkdir -p $RPM_BUILD_ROOT/etc cp -r xapi.d $RPM_BUILD_ROOT/etc -chmod u+x $RPM_BUILD_ROOT/etc/xapi.d/plugins/objectstore -#%{_fixperms} $RPM_BUILD_ROOT/* +chmod a+x $RPM_BUILD_ROOT/etc/xapi.d/plugins/* %clean rm -rf $RPM_BUILD_ROOT From 7b6ded922adc26f26dd208d5de1763b708866cea Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Mon, 27 Jun 2011 12:10:47 -0400 Subject: [PATCH 19/39] Updating license to ASL 2.0 --- .../xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec index 864ca7ff7ea5..91ff20e5fcab 100644 --- a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec +++ b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec @@ -2,7 +2,7 @@ Name: openstack-xen-plugins Version: 2011.3 Release: 1 Summary: Files for XenAPI support. -License: Apache +License: ASL 2.0 Group: Applications/Utilities Source0: openstack-xen-plugins.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) From 883992df19441544deb9aa5f60f2a77ab1f46567 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Mon, 27 Jun 2011 16:50:17 -0500 Subject: [PATCH 20/39] Review feedback. --- nova/compute/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 9d71ff92296e..156b197e18df 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -523,14 +523,14 @@ class ComputeManager(manager.SchedulerDependentManager): if image_type == 'snapshot': if rotation: - raise exception.ImageRotationNotAllowed + raise exception.ImageRotationNotAllowed() elif image_type == 'backup': if rotation: instance_uuid = instance_ref['uuid'] self.rotate_backups(context, instance_uuid, backup_type, rotation) else: - raise exception.RotationRequiredForBackup + raise exception.RotationRequiredForBackup() else: raise Exception(_('Image type not recognized %s') % image_type) From 4b8bcf30f934ea91290b7fe41536ba06ee832b3f Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Tue, 28 Jun 2011 08:57:05 +0000 Subject: [PATCH 21/39] Re-merging code for generating system-usages to get around bzr merge braindeadness. --- bin/instance-usage-audit | 127 +++++++++++++++++++++++++++++++++ nova/compute/manager.py | 94 ++++++++++++++++++++++++ nova/db/api.py | 5 ++ nova/db/sqlalchemy/api.py | 18 +++++ nova/notifier/test_notifier.py | 28 ++++++++ nova/tests/test_compute.py | 77 ++++++++++++++++++++ 6 files changed, 349 insertions(+) create mode 100755 bin/instance-usage-audit create mode 100644 nova/notifier/test_notifier.py diff --git a/bin/instance-usage-audit b/bin/instance-usage-audit new file mode 100755 index 000000000000..1124cf550719 --- /dev/null +++ b/bin/instance-usage-audit @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# 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. + +"""Cron script to generate usage notifications for instances neither created + nor destroyed in a given time period. + + Together with the notifications generated by compute on instance + create/delete/resize, over that ime period, this allows an external + system consuming usage notification feeds to calculate instance usage + for each tenant. + + Time periods are specified like so: + [mdy] + + 1m = previous month. If the script is run April 1, it will generate usages + for March 1 thry March 31. + 3m = 3 previous months. + 90d = previous 90 days. + 1y = previous year. If run on Jan 1, it generates usages for + Jan 1 thru Dec 31 of the previous year. +""" + +import datetime +import gettext +import os +import sys +import time + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')): + sys.path.insert(0, POSSIBLE_TOPDIR) + +gettext.install('nova', unicode=1) + + +from nova import context +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils + +from nova.notifier import api as notifier_api + +FLAGS = flags.FLAGS +flags.DEFINE_string('instance_usage_audit_period', '1m', + 'time period to generate instance usages for.') + +def time_period(period): + today = datetime.date.today() + unit = period[-1] + if unit not in 'mdy': + raise ValueError('Time period must be m, d, or y') + n = int(period[:-1]) + if unit == 'm': + year = today.year - (n//12) + n = n % 12 + if n >= today.month: + year -= 1 + month = 12 + (today.month - n) + else: + month = today.month - n + begin = datetime.datetime(day=1, month=month, year=year) + end = datetime.datetime(day=1, month=today.month, year=today.year) + + elif unit == 'y': + begin = datetime.datetime(day=1, month=1, year=today.year - n) + end = datetime.datetime(day=1, month=1, year=today.year) + + elif unit == 'd': + b = today - datetime.timedelta(days=n) + begin = datetime.datetime(day=b.day, month=b.month, year=b.year) + end = datetime.datetime(day=today.day, + month=today.month, + year=today.year) + + return (begin, end) + +if __name__ == '__main__': + utils.default_flagfile() + flags.FLAGS(sys.argv) + logging.setup() + begin, end = time_period(FLAGS.instance_usage_audit_period) + print "Creating usages for %s until %s" % (str(begin), str(end)) + instances = db.instance_get_active_by_window(context.get_admin_context(), + begin, + end) + print "%s instances" % len(instances) + for instance_ref in instances: + usage_info = dict( + tenant_id=instance_ref['project_id'], + user_id=instance_ref['user_id'], + instance_id=instance_ref['id'], + instance_type=instance_ref['instance_type']['name'], + instance_type_id=instance_ref['instance_type_id'], + display_name=instance_ref['display_name'], + created_at=str(instance_ref['created_at']), + launched_at=str(instance_ref['launched_at']) \ + if instance_ref['launched_at'] else '', + image_id=instance_ref['image_id'], + audit_period_begining=str(begin), + audit_period_ending=str(end)) + notifier_api.notify('compute.%s' % FLAGS.host, + 'compute.instance.exists', + notifier_api.INFO, + usage_info) + + diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 923feaa593d3..d0e9cdf95327 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -50,10 +50,12 @@ from nova import flags from nova import log as logging from nova import manager from nova import network +from nova import notifier from nova import rpc from nova import utils from nova import volume from nova.compute import power_state +from nova.notifier import api as notifier_api from nova.virt import driver @@ -275,6 +277,21 @@ class ComputeManager(manager.SchedulerDependentManager): self._update_launched_at(context, instance_id) self._update_state(context, instance_id) + usage_info = dict( + tenant_id=instance_ref['project_id'], + user_id=instance_ref['user_id'], + instance_id=instance_ref['id'], + instance_type=instance_ref['instance_type']['name'], + instance_type_id=instance_ref['instance_type_id'], + display_name=instance_ref['display_name'], + created_at=str(instance_ref['created_at']), + launched_at=str(instance_ref['launched_at']) \ + if instance_ref['launched_at'] else '', + image_id=instance_ref['image_id']) + notifier_api.notify('compute.%s' % self.host, + 'compute.instance.create', + notifier_api.INFO, + usage_info) @exception.wrap_exception @checks_instance_lock @@ -327,6 +344,21 @@ class ComputeManager(manager.SchedulerDependentManager): # TODO(ja): should we keep it in a terminated state for a bit? self.db.instance_destroy(context, instance_id) + usage_info = dict( + tenant_id=instance_ref['project_id'], + user_id=instance_ref['user_id'], + instance_id=instance_ref['id'], + instance_type=instance_ref['instance_type']['name'], + instance_type_id=instance_ref['instance_type_id'], + display_name=instance_ref['display_name'], + created_at=str(instance_ref['created_at']), + launched_at=str(instance_ref['launched_at']) \ + if instance_ref['launched_at'] else '', + image_id=instance_ref['image_id']) + notifier_api.notify('compute.%s' % self.host, + 'compute.instance.delete', + notifier_api.INFO, + usage_info) @exception.wrap_exception @checks_instance_lock @@ -354,6 +386,21 @@ class ComputeManager(manager.SchedulerDependentManager): self._update_image_id(context, instance_id, image_id) self._update_launched_at(context, instance_id) self._update_state(context, instance_id) + usage_info = dict( + tenant_id=instance_ref['project_id'], + user_id=instance_ref['user_id'], + instance_id=instance_ref['id'], + instance_type=instance_ref['instance_type']['name'], + instance_type_id=instance_ref['instance_type_id'], + display_name=instance_ref['display_name'], + created_at=str(instance_ref['created_at']), + launched_at=str(instance_ref['launched_at']) \ + if instance_ref['launched_at'] else '', + image_id=image_id) + notifier_api.notify('compute.%s' % self.host, + 'compute.instance.rebuild', + notifier_api.INFO, + usage_info) @exception.wrap_exception @checks_instance_lock @@ -501,6 +548,21 @@ class ComputeManager(manager.SchedulerDependentManager): context = context.elevated() instance_ref = self.db.instance_get(context, instance_id) self.driver.destroy(instance_ref) + usage_info = dict( + tenant_id=instance_ref['project_id'], + user_id=instance_ref['user_id'], + instance_id=instance_ref['id'], + instance_type=instance_ref['instance_type']['name'], + instance_type_id=instance_ref['instance_type_id'], + display_name=instance_ref['display_name'], + created_at=str(instance_ref['created_at']), + launched_at=str(instance_ref['launched_at']) \ + if instance_ref['launched_at'] else '', + image_id=instance_ref['image_id']) + notifier_api.notify('compute.%s' % self.host, + 'compute.instance.resize.confirm', + notifier_api.INFO, + usage_info) @exception.wrap_exception @checks_instance_lock @@ -548,6 +610,21 @@ class ComputeManager(manager.SchedulerDependentManager): self.driver.revert_resize(instance_ref) self.db.migration_update(context, migration_id, {'status': 'reverted'}) + usage_info = dict( + tenant_id=instance_ref['project_id'], + user_id=instance_ref['user_id'], + instance_id=instance_ref['id'], + instance_type=instance_type['name'], + instance_type_id=instance_type['id'], + display_name=instance_ref['display_name'], + created_at=str(instance_ref['created_at']), + launched_at=str(instance_ref['launched_at']) \ + if instance_ref['launched_at'] else '', + image_id=instance_ref['image_id']) + notifier_api.notify('compute.%s' % self.host, + 'compute.instance.resize.revert', + notifier_api.INFO, + usage_info) @exception.wrap_exception @checks_instance_lock @@ -584,6 +661,23 @@ class ComputeManager(manager.SchedulerDependentManager): 'migration_id': migration_ref['id'], 'instance_id': instance_id, }, }) + usage_info = dict( + tenant_id=instance_ref['project_id'], + user_id=instance_ref['user_id'], + instance_id=instance_ref['id'], + instance_type=instance_ref['instance_type']['name'], + instance_type_id=instance_ref['instance_type_id'], + new_instance_type=instance_type['name'], + new_instance_type_id=instance_type['id'], + display_name=instance_ref['display_name'], + created_at=str(instance_ref['created_at']), + launched_at=str(instance_ref['launched_at']) \ + if instance_ref['launched_at'] else '', + image_id=instance_ref['image_id']) + notifier_api.notify('compute.%s' % self.host, + 'compute.instance.resize.prep', + notifier_api.INFO, + usage_info) @exception.wrap_exception @checks_instance_lock diff --git a/nova/db/api.py b/nova/db/api.py index ef8aa1143f8d..aca4038568ad 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -422,6 +422,11 @@ def instance_get_all(context): return IMPL.instance_get_all(context) +def instance_get_active_by_window(context, begin, end=None): + """Get instances active during a certain time window.""" + return IMPL.instance_get_active_by_window(context, begin, end) + + def instance_get_all_by_user(context, user_id): """Get all instances.""" return IMPL.instance_get_all_by_user(context, user_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 3681f30db61e..873cfe4d1c0d 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -878,6 +878,24 @@ def instance_get_all(context): all() +@require_admin_context +def instance_get_active_by_window(context, begin, end=None): + """Return instances that were continuously active over the given window""" + session = get_session() + query = session.query(models.Instance).\ + options(joinedload_all('fixed_ip.floating_ips')).\ + options(joinedload('security_groups')).\ + options(joinedload_all('fixed_ip.network')).\ + options(joinedload('instance_type')).\ + filter(models.Instance.launched_at < begin) + if end: + query = query.filter(or_(models.Instance.terminated_at == None, + models.Instance.terminated_at > end)) + else: + query = query.filter(models.Instance.terminated_at == None) + return query.all() + + @require_admin_context def instance_get_all_by_user(context, user_id): session = get_session() diff --git a/nova/notifier/test_notifier.py b/nova/notifier/test_notifier.py new file mode 100644 index 000000000000..d43f43e48672 --- /dev/null +++ b/nova/notifier/test_notifier.py @@ -0,0 +1,28 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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 json + +from nova import flags +from nova import log as logging + +FLAGS = flags.FLAGS + +NOTIFICATIONS = [] + + +def notify(message): + """Test notifier, stores notifications in memory for unittests.""" + NOTIFICATIONS.append(message) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 55e7ae0c46c6..30a65a4b175b 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -38,6 +38,7 @@ from nova.compute import manager as compute_manager from nova.compute import power_state from nova.db.sqlalchemy import models from nova.image import local +from nova.notifier import test_notifier LOG = logging.getLogger('nova.tests.compute') FLAGS = flags.FLAGS @@ -63,6 +64,7 @@ class ComputeTestCase(test.TestCase): super(ComputeTestCase, self).setUp() self.flags(connection_type='fake', stub_network=True, + notification_driver='nova.notifier.test_notifier', network_manager='nova.network.manager.FlatManager') self.compute = utils.import_object(FLAGS.compute_manager) self.compute_api = compute.API() @@ -70,6 +72,7 @@ class ComputeTestCase(test.TestCase): self.user = self.manager.create_user('fake', 'fake', 'fake') self.project = self.manager.create_project('fake', 'fake', 'fake') self.context = context.RequestContext('fake', 'fake', False) + test_notifier.NOTIFICATIONS = [] def fake_show(meh, context, id): return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}} @@ -305,6 +308,50 @@ class ComputeTestCase(test.TestCase): self.assert_(console) self.compute.terminate_instance(self.context, instance_id) + def test_run_instance_usage_notification(self): + """Ensure run instance generates apropriate usage notification""" + instance_id = self._create_instance() + self.compute.run_instance(self.context, instance_id) + self.assertEquals(len(test_notifier.NOTIFICATIONS), 1) + msg = test_notifier.NOTIFICATIONS[0] + self.assertEquals(msg['priority'], 'INFO') + self.assertEquals(msg['event_type'], 'compute.instance.create') + payload = msg['payload'] + self.assertEquals(payload['tenant_id'], self.project.id) + self.assertEquals(payload['user_id'], self.user.id) + self.assertEquals(payload['instance_id'], instance_id) + self.assertEquals(payload['instance_type'], 'm1.tiny') + type_id = instance_types.get_instance_type_by_name('m1.tiny')['id'] + self.assertEquals(str(payload['instance_type_id']), str(type_id)) + self.assertTrue('display_name' in payload) + self.assertTrue('created_at' in payload) + self.assertTrue('launched_at' in payload) + self.assertEquals(payload['image_id'], '1') + self.compute.terminate_instance(self.context, instance_id) + + def test_terminate_usage_notification(self): + """Ensure terminate_instance generates apropriate usage notification""" + instance_id = self._create_instance() + self.compute.run_instance(self.context, instance_id) + test_notifier.NOTIFICATIONS = [] + self.compute.terminate_instance(self.context, instance_id) + + self.assertEquals(len(test_notifier.NOTIFICATIONS), 1) + msg = test_notifier.NOTIFICATIONS[0] + self.assertEquals(msg['priority'], 'INFO') + self.assertEquals(msg['event_type'], 'compute.instance.delete') + payload = msg['payload'] + self.assertEquals(payload['tenant_id'], self.project.id) + self.assertEquals(payload['user_id'], self.user.id) + self.assertEquals(payload['instance_id'], instance_id) + self.assertEquals(payload['instance_type'], 'm1.tiny') + type_id = instance_types.get_instance_type_by_name('m1.tiny')['id'] + self.assertEquals(str(payload['instance_type_id']), str(type_id)) + self.assertTrue('display_name' in payload) + self.assertTrue('created_at' in payload) + self.assertTrue('launched_at' in payload) + self.assertEquals(payload['image_id'], '1') + def test_run_instance_existing(self): """Ensure failure when running an instance that already exists""" instance_id = self._create_instance() @@ -334,6 +381,36 @@ class ComputeTestCase(test.TestCase): self.compute.terminate_instance(self.context, instance_id) + def test_resize_instance_notification(self): + """Ensure instance can be migrated/resized""" + instance_id = self._create_instance() + context = self.context.elevated() + + self.compute.run_instance(self.context, instance_id) + test_notifier.NOTIFICATIONS = [] + + db.instance_update(self.context, instance_id, {'host': 'foo'}) + self.compute.prep_resize(context, instance_id, 1) + migration_ref = db.migration_get_by_instance_and_status(context, + instance_id, 'pre-migrating') + + self.assertEquals(len(test_notifier.NOTIFICATIONS), 1) + msg = test_notifier.NOTIFICATIONS[0] + self.assertEquals(msg['priority'], 'INFO') + self.assertEquals(msg['event_type'], 'compute.instance.resize.prep') + payload = msg['payload'] + self.assertEquals(payload['tenant_id'], self.project.id) + self.assertEquals(payload['user_id'], self.user.id) + self.assertEquals(payload['instance_id'], instance_id) + self.assertEquals(payload['instance_type'], 'm1.tiny') + type_id = instance_types.get_instance_type_by_name('m1.tiny')['id'] + self.assertEquals(str(payload['instance_type_id']), str(type_id)) + self.assertTrue('display_name' in payload) + self.assertTrue('created_at' in payload) + self.assertTrue('launched_at' in payload) + self.assertEquals(payload['image_id'], '1') + self.compute.terminate_instance(context, instance_id) + def test_resize_instance(self): """Ensure instance can be migrated/resized""" instance_id = self._create_instance() From e2c66d0e96467d510d01a5c5f60a56e8252dce5b Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Tue, 28 Jun 2011 09:08:35 +0000 Subject: [PATCH 22/39] Fix pep8 nits in audit script --- bin/instance-usage-audit | 53 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/bin/instance-usage-audit b/bin/instance-usage-audit index 1124cf550719..cef9b464b2f7 100755 --- a/bin/instance-usage-audit +++ b/bin/instance-usage-audit @@ -17,22 +17,22 @@ # under the License. """Cron script to generate usage notifications for instances neither created - nor destroyed in a given time period. - + nor destroyed in a given time period. + Together with the notifications generated by compute on instance - create/delete/resize, over that ime period, this allows an external - system consuming usage notification feeds to calculate instance usage + create/delete/resize, over that ime period, this allows an external + system consuming usage notification feeds to calculate instance usage for each tenant. Time periods are specified like so: - [mdy] + [mdy] 1m = previous month. If the script is run April 1, it will generate usages for March 1 thry March 31. 3m = 3 previous months. - 90d = previous 90 days. - 1y = previous year. If run on Jan 1, it generates usages for - Jan 1 thru Dec 31 of the previous year. + 90d = previous 90 days. + 1y = previous year. If run on Jan 1, it generates usages for + Jan 1 thru Dec 31 of the previous year. """ import datetime @@ -65,6 +65,7 @@ FLAGS = flags.FLAGS flags.DEFINE_string('instance_usage_audit_period', '1m', 'time period to generate instance usages for.') + def time_period(period): today = datetime.date.today() unit = period[-1] @@ -72,27 +73,27 @@ def time_period(period): raise ValueError('Time period must be m, d, or y') n = int(period[:-1]) if unit == 'm': - year = today.year - (n//12) - n = n % 12 - if n >= today.month: - year -= 1 - month = 12 + (today.month - n) - else: - month = today.month - n - begin = datetime.datetime(day=1, month=month, year=year) - end = datetime.datetime(day=1, month=today.month, year=today.year) + year = today.year - (n // 12) + n = n % 12 + if n >= today.month: + year -= 1 + month = 12 + (today.month - n) + else: + month = today.month - n + begin = datetime.datetime(day=1, month=month, year=year) + end = datetime.datetime(day=1, month=today.month, year=today.year) elif unit == 'y': - begin = datetime.datetime(day=1, month=1, year=today.year - n) - end = datetime.datetime(day=1, month=1, year=today.year) - + begin = datetime.datetime(day=1, month=1, year=today.year - n) + end = datetime.datetime(day=1, month=1, year=today.year) + elif unit == 'd': - b = today - datetime.timedelta(days=n) - begin = datetime.datetime(day=b.day, month=b.month, year=b.year) - end = datetime.datetime(day=today.day, - month=today.month, + b = today - datetime.timedelta(days=n) + begin = datetime.datetime(day=b.day, month=b.month, year=b.year) + end = datetime.datetime(day=today.day, + month=today.month, year=today.year) - + return (begin, end) if __name__ == '__main__': @@ -123,5 +124,3 @@ if __name__ == '__main__': 'compute.instance.exists', notifier_api.INFO, usage_info) - - From 4c1d05d27f207e71546f20c4e603839afc232b5a Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Tue, 28 Jun 2011 15:21:08 +0000 Subject: [PATCH 23/39] Fix issues due to renming of imange_id attrib. --- bin/instance-usage-audit | 2 +- nova/compute/manager.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/instance-usage-audit b/bin/instance-usage-audit index cef9b464b2f7..1fa2c2186acf 100755 --- a/bin/instance-usage-audit +++ b/bin/instance-usage-audit @@ -117,7 +117,7 @@ if __name__ == '__main__': created_at=str(instance_ref['created_at']), launched_at=str(instance_ref['launched_at']) \ if instance_ref['launched_at'] else '', - image_id=instance_ref['image_id'], + image_ref=instance_ref['image_ref'], audit_period_begining=str(begin), audit_period_ending=str(end)) notifier_api.notify('compute.%s' % FLAGS.host, diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d081937bdaf8..2c4f500f06e2 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -355,7 +355,7 @@ class ComputeManager(manager.SchedulerDependentManager): created_at=str(instance_ref['created_at']), launched_at=str(instance_ref['launched_at']) \ if instance_ref['launched_at'] else '', - image_id=instance_ref['image_id']) + image_ref=instance_ref['image_ref']) notifier_api.notify('compute.%s' % self.host, 'compute.instance.create', notifier_api.INFO, @@ -451,7 +451,7 @@ class ComputeManager(manager.SchedulerDependentManager): created_at=str(instance_ref['created_at']), launched_at=str(instance_ref['launched_at']) \ if instance_ref['launched_at'] else '', - image_id=instance_ref['image_id']) + image_ref=instance_ref['image_ref']) notifier_api.notify('compute.%s' % self.host, 'compute.instance.delete', notifier_api.INFO, @@ -502,7 +502,7 @@ class ComputeManager(manager.SchedulerDependentManager): created_at=str(instance_ref['created_at']), launched_at=str(instance_ref['launched_at']) \ if instance_ref['launched_at'] else '', - image_id=image_id) + image_ref=image_ref) notifier_api.notify('compute.%s' % self.host, 'compute.instance.rebuild', notifier_api.INFO, @@ -694,7 +694,7 @@ class ComputeManager(manager.SchedulerDependentManager): created_at=str(instance_ref['created_at']), launched_at=str(instance_ref['launched_at']) \ if instance_ref['launched_at'] else '', - image_id=instance_ref['image_id']) + image_ref=instance_ref['image_ref']) notifier_api.notify('compute.%s' % self.host, 'compute.instance.resize.confirm', notifier_api.INFO, @@ -756,7 +756,7 @@ class ComputeManager(manager.SchedulerDependentManager): created_at=str(instance_ref['created_at']), launched_at=str(instance_ref['launched_at']) \ if instance_ref['launched_at'] else '', - image_id=instance_ref['image_id']) + image_ref=instance_ref['image_ref']) notifier_api.notify('compute.%s' % self.host, 'compute.instance.resize.revert', notifier_api.INFO, @@ -809,7 +809,7 @@ class ComputeManager(manager.SchedulerDependentManager): created_at=str(instance_ref['created_at']), launched_at=str(instance_ref['launched_at']) \ if instance_ref['launched_at'] else '', - image_id=instance_ref['image_id']) + image_ref=instance_ref['image_ref']) notifier_api.notify('compute.%s' % self.host, 'compute.instance.resize.prep', notifier_api.INFO, From 66b2fef4b294c7a351cc5815632da520c6ee811b Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Tue, 28 Jun 2011 15:50:07 +0000 Subject: [PATCH 24/39] Fix yet more merge-skew. --- nova/compute/manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 2c4f500f06e2..ea5734ebd9da 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -441,6 +441,8 @@ class ComputeManager(manager.SchedulerDependentManager): # TODO(ja): should we keep it in a terminated state for a bit? self.db.instance_destroy(context, instance_id) + context = context.elevated() + instance_ref = self.db.instance_get(context, instance_id) usage_info = dict( tenant_id=instance_ref['project_id'], user_id=instance_ref['user_id'], From c69fc237f3628d579a35af1f7bf3fbb4adeb81b7 Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Tue, 28 Jun 2011 15:59:44 +0000 Subject: [PATCH 25/39] Fix thinko in previous fix :P --- nova/compute/manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index ea5734ebd9da..0a0ebd76807a 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -438,11 +438,10 @@ class ComputeManager(manager.SchedulerDependentManager): def terminate_instance(self, context, instance_id): """Terminate an instance on this host.""" self._shutdown_instance(context, instance_id, 'Terminating') + instance_ref = self.db.instance_get(context.elevated(), instance_id) # TODO(ja): should we keep it in a terminated state for a bit? self.db.instance_destroy(context, instance_id) - context = context.elevated() - instance_ref = self.db.instance_get(context, instance_id) usage_info = dict( tenant_id=instance_ref['project_id'], user_id=instance_ref['user_id'], From 24835b0348a9a6d8bd4e40107990d1abb41538c2 Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Tue, 28 Jun 2011 16:08:27 +0000 Subject: [PATCH 26/39] Fix merge issue in compute unittest. --- nova/tests/test_compute.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index e94a62679168..4d42b1fdf29b 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -348,7 +348,7 @@ class ComputeTestCase(test.TestCase): self.assertTrue('display_name' in payload) self.assertTrue('created_at' in payload) self.assertTrue('launched_at' in payload) - self.assertEquals(payload['image_id'], '1') + self.assertEquals(payload['image_ref'], '1') self.compute.terminate_instance(self.context, instance_id) def test_terminate_usage_notification(self): @@ -372,7 +372,7 @@ class ComputeTestCase(test.TestCase): self.assertTrue('display_name' in payload) self.assertTrue('created_at' in payload) self.assertTrue('launched_at' in payload) - self.assertEquals(payload['image_id'], '1') + self.assertEquals(payload['image_ref'], '1') def test_run_instance_existing(self): """Ensure failure when running an instance that already exists""" @@ -452,7 +452,7 @@ class ComputeTestCase(test.TestCase): self.assertTrue('display_name' in payload) self.assertTrue('created_at' in payload) self.assertTrue('launched_at' in payload) - self.assertEquals(payload['image_id'], '1') + self.assertEquals(payload['image_ref'], '1') self.compute.terminate_instance(context, instance_id) def test_resize_instance(self): From ec574986212b694bfed8109545b4b4dc578ec8f4 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 28 Jun 2011 14:49:40 -0500 Subject: [PATCH 27/39] Review feedback. --- nova/compute/manager.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 156b197e18df..fc9a89379ba2 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -547,15 +547,30 @@ class ComputeManager(manager.SchedulerDependentManager): :param rotation: int representing how many backups to keep around; None if rotation shouldn't be used (as in the case of snapshots) """ + # NOTE(jk0): Eventually extract this out to the ImageService? + def fetch_images(): + images = [] + offset = 0 + while True: + batch = image_service.detail(context, filters=filters, + offset=offset) + if not batch: + break + images += batch + offset += len(batch) + return images + image_service = nova.image.get_default_image_service() filters = {'property-image_type': 'backup', 'property-backup_type': backup_type, 'property-instance_uuid': instance_uuid} - images = image_service.detail(context, filters=filters) + + images = fetch_images() num_images = len(images) LOG.debug(_("Found %(num_images)d images (rotation: %(rotation)d)" % locals())) if num_images > rotation: + # TODO(jk0): Use db-level sorting in glance when it hits trunk. # Sort oldest (by created_at) to end of list images.sort(key=itemgetter('created_at'), reverse=True) From ee2eb1f712a87e73832618be6b79f74301d74a41 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 28 Jun 2011 14:58:34 -0500 Subject: [PATCH 28/39] Whoops. --- nova/compute/manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index fc9a89379ba2..6a7bb73cb5bd 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -550,14 +550,14 @@ class ComputeManager(manager.SchedulerDependentManager): # NOTE(jk0): Eventually extract this out to the ImageService? def fetch_images(): images = [] - offset = 0 + marker = 0 while True: batch = image_service.detail(context, filters=filters, - offset=offset) + marker=marker) if not batch: break images += batch - offset += len(batch) + marker += len(batch) return images image_service = nova.image.get_default_image_service() From ec1afee8399818db2ba11952a61c924da73f57a0 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 28 Jun 2011 15:17:23 -0500 Subject: [PATCH 29/39] OOPS --- nova/compute/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 6a7bb73cb5bd..fdb231e9ed59 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -550,14 +550,14 @@ class ComputeManager(manager.SchedulerDependentManager): # NOTE(jk0): Eventually extract this out to the ImageService? def fetch_images(): images = [] - marker = 0 + marker = None while True: batch = image_service.detail(context, filters=filters, marker=marker) if not batch: break images += batch - marker += len(batch) + marker = batch[-1]['id'] return images image_service = nova.image.get_default_image_service() From 498f2d671573fc19d551516f7ead5da8d052ee18 Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Tue, 28 Jun 2011 20:37:05 +0000 Subject: [PATCH 30/39] Refactored usage generation --- bin/instance-usage-audit | 16 ++------- nova/compute/manager.py | 77 +++++----------------------------------- nova/utils.py | 16 +++++++++ 3 files changed, 28 insertions(+), 81 deletions(-) diff --git a/bin/instance-usage-audit b/bin/instance-usage-audit index 1fa2c2186acf..a06c6b1b39bd 100755 --- a/bin/instance-usage-audit +++ b/bin/instance-usage-audit @@ -107,19 +107,9 @@ if __name__ == '__main__': end) print "%s instances" % len(instances) for instance_ref in instances: - usage_info = dict( - tenant_id=instance_ref['project_id'], - user_id=instance_ref['user_id'], - instance_id=instance_ref['id'], - instance_type=instance_ref['instance_type']['name'], - instance_type_id=instance_ref['instance_type_id'], - display_name=instance_ref['display_name'], - created_at=str(instance_ref['created_at']), - launched_at=str(instance_ref['launched_at']) \ - if instance_ref['launched_at'] else '', - image_ref=instance_ref['image_ref'], - audit_period_begining=str(begin), - audit_period_ending=str(end)) + usage_info = utils.usage_from_instance(instance_ref, + audit_period_begining=str(begin), + audit_period_ending=str(end)) notifier_api.notify('compute.%s' % FLAGS.host, 'compute.instance.exists', notifier_api.INFO, diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 0a0ebd76807a..98e02f5b2e36 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -345,17 +345,7 @@ class ComputeManager(manager.SchedulerDependentManager): self._update_launched_at(context, instance_id) self._update_state(context, instance_id) - usage_info = dict( - tenant_id=instance_ref['project_id'], - user_id=instance_ref['user_id'], - instance_id=instance_ref['id'], - instance_type=instance_ref['instance_type']['name'], - instance_type_id=instance_ref['instance_type_id'], - display_name=instance_ref['display_name'], - created_at=str(instance_ref['created_at']), - launched_at=str(instance_ref['launched_at']) \ - if instance_ref['launched_at'] else '', - image_ref=instance_ref['image_ref']) + usage_info = utils.usage_from_instance(instance_ref) notifier_api.notify('compute.%s' % self.host, 'compute.instance.create', notifier_api.INFO, @@ -442,17 +432,7 @@ class ComputeManager(manager.SchedulerDependentManager): # TODO(ja): should we keep it in a terminated state for a bit? self.db.instance_destroy(context, instance_id) - usage_info = dict( - tenant_id=instance_ref['project_id'], - user_id=instance_ref['user_id'], - instance_id=instance_ref['id'], - instance_type=instance_ref['instance_type']['name'], - instance_type_id=instance_ref['instance_type_id'], - display_name=instance_ref['display_name'], - created_at=str(instance_ref['created_at']), - launched_at=str(instance_ref['launched_at']) \ - if instance_ref['launched_at'] else '', - image_ref=instance_ref['image_ref']) + usage_info = utils.usage_from_instance(instance_ref) notifier_api.notify('compute.%s' % self.host, 'compute.instance.delete', notifier_api.INFO, @@ -493,17 +473,8 @@ class ComputeManager(manager.SchedulerDependentManager): self._update_image_ref(context, instance_id, image_ref) self._update_launched_at(context, instance_id) self._update_state(context, instance_id) - usage_info = dict( - tenant_id=instance_ref['project_id'], - user_id=instance_ref['user_id'], - instance_id=instance_ref['id'], - instance_type=instance_ref['instance_type']['name'], - instance_type_id=instance_ref['instance_type_id'], - display_name=instance_ref['display_name'], - created_at=str(instance_ref['created_at']), - launched_at=str(instance_ref['launched_at']) \ - if instance_ref['launched_at'] else '', - image_ref=image_ref) + usage_info = utils.usage_from_instance(instance_ref, + image_ref=image_ref) notifier_api.notify('compute.%s' % self.host, 'compute.instance.rebuild', notifier_api.INFO, @@ -685,17 +656,7 @@ class ComputeManager(manager.SchedulerDependentManager): context = context.elevated() instance_ref = self.db.instance_get(context, instance_id) self.driver.destroy(instance_ref) - usage_info = dict( - tenant_id=instance_ref['project_id'], - user_id=instance_ref['user_id'], - instance_id=instance_ref['id'], - instance_type=instance_ref['instance_type']['name'], - instance_type_id=instance_ref['instance_type_id'], - display_name=instance_ref['display_name'], - created_at=str(instance_ref['created_at']), - launched_at=str(instance_ref['launched_at']) \ - if instance_ref['launched_at'] else '', - image_ref=instance_ref['image_ref']) + usage_info = utils.usage_from_instance(instance_ref) notifier_api.notify('compute.%s' % self.host, 'compute.instance.resize.confirm', notifier_api.INFO, @@ -747,17 +708,7 @@ class ComputeManager(manager.SchedulerDependentManager): self.driver.revert_resize(instance_ref) self.db.migration_update(context, migration_id, {'status': 'reverted'}) - usage_info = dict( - tenant_id=instance_ref['project_id'], - user_id=instance_ref['user_id'], - instance_id=instance_ref['id'], - instance_type=instance_type['name'], - instance_type_id=instance_type['id'], - display_name=instance_ref['display_name'], - created_at=str(instance_ref['created_at']), - launched_at=str(instance_ref['launched_at']) \ - if instance_ref['launched_at'] else '', - image_ref=instance_ref['image_ref']) + usage_info = utils.usage_from_instance(instance_ref) notifier_api.notify('compute.%s' % self.host, 'compute.instance.resize.revert', notifier_api.INFO, @@ -798,19 +749,9 @@ class ComputeManager(manager.SchedulerDependentManager): 'migration_id': migration_ref['id'], 'instance_id': instance_id, }, }) - usage_info = dict( - tenant_id=instance_ref['project_id'], - user_id=instance_ref['user_id'], - instance_id=instance_ref['id'], - instance_type=instance_ref['instance_type']['name'], - instance_type_id=instance_ref['instance_type_id'], - new_instance_type=instance_type['name'], - new_instance_type_id=instance_type['id'], - display_name=instance_ref['display_name'], - created_at=str(instance_ref['created_at']), - launched_at=str(instance_ref['launched_at']) \ - if instance_ref['launched_at'] else '', - image_ref=instance_ref['image_ref']) + usage_info = utils.usage_from_instance(instance_ref, + new_instance_type=instance_type['name'], + new_instance_type_id=instance_type['id']) notifier_api.notify('compute.%s' % self.host, 'compute.instance.resize.prep', notifier_api.INFO, diff --git a/nova/utils.py b/nova/utils.py index 6d8324e5bd3b..aee2715ba6f0 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -279,6 +279,22 @@ EASIER_PASSWORD_SYMBOLS = ('23456789' # Removed: 0, 1 'ABCDEFGHJKLMNPQRSTUVWXYZ') # Removed: I, O +def usage_from_instance(instance_ref, **kw): + usage_info = dict( + tenant_id=instance_ref['project_id'], + user_id=instance_ref['user_id'], + instance_id=instance_ref['id'], + instance_type=instance_ref['instance_type']['name'], + instance_type_id=instance_ref['instance_type_id'], + display_name=instance_ref['display_name'], + created_at=str(instance_ref['created_at']), + launched_at=str(instance_ref['launched_at']) \ + if instance_ref['launched_at'] else '', + image_ref=instance_ref['image_ref']) + usage_info.update(kw) + return usage_info + + def generate_password(length=20, symbols=DEFAULT_PASSWORD_SYMBOLS): """Generate a random password from the supplied symbols. From 2916aa40f6dc0b06217ff7d3750ecdd3bb03e4fd Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 28 Jun 2011 16:03:41 -0500 Subject: [PATCH 31/39] Review feedback. --- nova/api/openstack/images.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 44d8c94a40f6..7ebf58023541 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -105,7 +105,8 @@ class Controller(object): try: return body["image"][param] except KeyError: - raise webob.exc.HTTPBadRequest() + raise webob.exc.HTTPBadRequest(explanation="Missing required " + "param: %s" % param) context = req.environ['nova.context'] content_type = req.get_content_type() @@ -131,7 +132,8 @@ class Controller(object): # NOTE(sirp): Unlike snapshot, backup is not a customer facing # API call; rather, it's used by the internal backup scheduler if not FLAGS.allow_admin_api: - raise webob.exc.HTTPBadRequest() + raise webob.exc.HTTPBadRequest( + explanation="Admin API Required") backup_type = get_param("backup_type") rotation = int(get_param("rotation")) @@ -141,7 +143,8 @@ class Controller(object): backup_type, rotation, extra_properties=props) else: LOG.error(_("Invalid image_type '%s' passed") % image_type) - raise webob.exc.HTTPBadRequest() + raise webob.exc.HTTPBadRequest(explanation="Invalue image_type: " + "%s" % image_type) return dict(image=self.get_builder(req).build(image, detail=True)) From d0ff8a737111e9155fd59816afa5c4fc2b34bb4c Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 28 Jun 2011 16:54:25 -0500 Subject: [PATCH 32/39] Let glance handle sorting. --- nova/compute/manager.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index fdb231e9ed59..f81e793fe2d9 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -43,7 +43,6 @@ import time import functools from eventlet import greenthread -from operator import itemgetter from nova import exception from nova import flags @@ -570,10 +569,6 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.debug(_("Found %(num_images)d images (rotation: %(rotation)d)" % locals())) if num_images > rotation: - # TODO(jk0): Use db-level sorting in glance when it hits trunk. - # Sort oldest (by created_at) to end of list - images.sort(key=itemgetter('created_at'), reverse=True) - # NOTE(sirp): this deletes all backups that exceed the rotation # limit excess = len(images) - rotation From 834b1741b4cd5e42393a8947a5c1fea80c625ee2 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 28 Jun 2011 17:26:08 -0500 Subject: [PATCH 33/39] Use milestone cut. --- bin/nova-api | 6 ++++++ .../versions/027_add_provider_firewall_rules.py | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bin/nova-api b/bin/nova-api index ea99a1b4822c..fff67251f2f9 100755 --- a/bin/nova-api +++ b/bin/nova-api @@ -23,8 +23,14 @@ Starts both the EC2 and OpenStack APIs in separate processes. """ +import os import sys +possible_topdir = os.path.normpath(os.path.join(os.path.abspath( + sys.argv[0]), os.pardir, os.pardir)) +if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): + sys.path.insert(0, possible_topdir) + import nova.service import nova.utils diff --git a/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py index 5aa30f7a8b88..cb3c73170a8b 100644 --- a/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py +++ b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py @@ -58,8 +58,7 @@ provider_fw_rules = Table('provider_fw_rules', meta, Column('to_port', Integer()), Column('cidr', String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False)) - ) + unicode_error=None, _warn_on_bytestring=False))) def upgrade(migrate_engine): From 0ca902cb90ea824ef199601b65dbc52e6c713079 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 28 Jun 2011 18:50:17 -0500 Subject: [PATCH 34/39] Review feedback --- nova/compute/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 404a2176baee..40a640083bca 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -571,7 +571,7 @@ class ComputeManager(manager.SchedulerDependentManager): marker = None while True: batch = image_service.detail(context, filters=filters, - marker=marker) + marker=marker, sort_key='created_at', sort_dir='desc') if not batch: break images += batch From fc40fa75a59d253859a559d1b8c336ebe7864b69 Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Wed, 29 Jun 2011 16:45:46 +0200 Subject: [PATCH 35/39] Fix nova-manage vm list --- bin/nova-manage | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 02f20347dac8..d5390b636948 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -617,7 +617,7 @@ class VmCommands(object): :param host: show all instance on specified host. :param instance: show specificed instance. """ - print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \ + print "%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s" \ " %-10s %-10s %-10s %-5s" % ( _('instance'), _('node'), @@ -639,14 +639,14 @@ class VmCommands(object): context.get_admin_context(), host) for instance in instances: - print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \ + print "%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s" \ " %-10s %-10s %-10s %-5d" % ( instance['hostname'], instance['host'], - instance['instance_type'], + instance['instance_type'].name, instance['state_description'], instance['launched_at'], - instance['image_id'], + instance['image_ref'], instance['kernel_id'], instance['ramdisk_id'], instance['project_id'], From c6e220af60079bd2e3f1a8991052b108692a1696 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 29 Jun 2011 07:47:51 -0700 Subject: [PATCH 36/39] change the default to recreate the db but allow -n for faster tests --- run_tests.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index 2ea221ae31b6..ddeb1dc4a0b5 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,7 +6,8 @@ function usage { echo "" echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" - echo " -r, --recreate-db Recreate the test database." + echo " -r, --recreate-db Recreate the test database (deprecated, as this is now the default)." + echo " -n, --no-recreate-db Don't recreate the test database." echo " -x, --stop Stop running tests after the first error or failure." echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." echo " -p, --pep8 Just run pep8" @@ -25,6 +26,7 @@ function process_option { -V|--virtual-env) let always_venv=1; let never_venv=0;; -N|--no-virtual-env) let always_venv=0; let never_venv=1;; -r|--recreate-db) let recreate_db=1;; + -n|--no-recreate-db) let recreate_db=0;; -f|--force) let force=1;; -p|--pep8) let just_pep8=1;; -*) noseopts="$noseopts $1";; @@ -41,7 +43,7 @@ noseargs= noseopts= wrapper="" just_pep8=0 -recreate_db=0 +recreate_db=1 for arg in "$@"; do process_option $arg From 698bb2e090988723e58f67b92bb38a9f7f2e49e1 Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Wed, 29 Jun 2011 16:52:55 +0200 Subject: [PATCH 37/39] Fix 'undefined name 'e'' pylint error --- bin/nova-manage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index d5390b636948..51e0c32c9627 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -878,7 +878,7 @@ class InstanceTypeCommands(object): try: instance_types.create(name, memory, vcpus, local_gb, flavorid, swap, rxtx_quota, rxtx_cap) - except exception.InvalidInput: + except exception.InvalidInput, e: print "Must supply valid parameters to create instance_type" print e sys.exit(1) From 45e5ae28377abc0eefd2e71ef553380b25283c48 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 29 Jun 2011 09:49:19 -0700 Subject: [PATCH 38/39] Fanout queues use unique queue names, so the consumer should have exclusive access. This means that they also get auto deleted when we're done with them, so they're not left around on a service restart. Fixes lp:803165 --- nova/rpc.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nova/rpc.py b/nova/rpc.py index 2e78a31e79f1..9f0b507fd63a 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -275,6 +275,11 @@ class FanoutAdapterConsumer(AdapterConsumer): unique = uuid.uuid4().hex self.queue = '%s_fanout_%s' % (topic, unique) self.durable = False + # Fanout creates unique queue names, so we should auto-remove + # them when done, so they're not left around on restart. + # Also, we're the only one that should be consuming. exclusive + # implies auto_delete, so we'll just set that.. + self.exclusive = True LOG.info(_('Created "%(exchange)s" fanout exchange ' 'with "%(key)s" routing key'), dict(exchange=self.exchange, key=self.routing_key)) From 74c222b6b4042053cc8c2d0038f37b3f8ee8b9fc Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Wed, 29 Jun 2011 14:52:56 -0400 Subject: [PATCH 39/39] don't pass zero in to glance image service if no limit or marker are present --- nova/api/openstack/common.py | 35 +++++++++++-------------- nova/api/openstack/images.py | 12 ++++----- nova/tests/api/openstack/test_common.py | 12 ++++++--- nova/tests/api/openstack/test_images.py | 20 +++++++------- 4 files changed, 41 insertions(+), 38 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 4da7ec0efb5b..aa8911b62295 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -45,23 +45,20 @@ def get_pagination_params(request): exc.HTTPBadRequest() exceptions to be raised. """ - try: - marker = int(request.GET.get('marker', 0)) - except ValueError: - raise webob.exc.HTTPBadRequest(_('marker param must be an integer')) + params = {} + for param in ['marker', 'limit']: + if not param in request.GET: + continue + try: + params[param] = int(request.GET[param]) + except ValueError: + msg = _('%s param must be an integer') % param + raise webob.exc.HTTPBadRequest(msg) + if params[param] < 0: + msg = _('%s param must be positive') % param + raise webob.exc.HTTPBadRequest(msg) - try: - limit = int(request.GET.get('limit', 0)) - except ValueError: - raise webob.exc.HTTPBadRequest(_('limit param must be an integer')) - - if limit < 0: - raise webob.exc.HTTPBadRequest(_('limit param must be positive')) - - if marker < 0: - raise webob.exc.HTTPBadRequest(_('marker param must be positive')) - - return(marker, limit) + return params def limited(items, request, max_limit=FLAGS.osapi_max_limit): @@ -100,10 +97,10 @@ def limited(items, request, max_limit=FLAGS.osapi_max_limit): def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit): """Return a slice of items according to the requested marker and limit.""" - (marker, limit) = get_pagination_params(request) + params = get_pagination_params(request) - if limit == 0: - limit = max_limit + limit = params.get('limit', max_limit) + marker = params.get('marker') limit = min(max_limit, limit) start_index = 0 diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index d43340e104de..64d003a0f2ac 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -181,9 +181,9 @@ class ControllerV11(Controller): """ context = req.environ['nova.context'] filters = self._get_filters(req) - (marker, limit) = common.get_pagination_params(req) - images = self._image_service.index( - context, filters=filters, marker=marker, limit=limit) + page_params = common.get_pagination_params(req) + images = self._image_service.index(context, filters=filters, + **page_params) builder = self.get_builder(req).build return dict(images=[builder(image, detail=False) for image in images]) @@ -195,9 +195,9 @@ class ControllerV11(Controller): """ context = req.environ['nova.context'] filters = self._get_filters(req) - (marker, limit) = common.get_pagination_params(req) - images = self._image_service.detail( - context, filters=filters, marker=marker, limit=limit) + page_params = common.get_pagination_params(req) + images = self._image_service.detail(context, filters=filters, + **page_params) builder = self.get_builder(req).build return dict(images=[builder(image, detail=True) for image in images]) diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index 9a9d9125c068..29cb8b944e40 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -161,12 +161,12 @@ class PaginationParamsTest(test.TestCase): def test_no_params(self): """ Test no params. """ req = Request.blank('/') - self.assertEqual(common.get_pagination_params(req), (0, 0)) + self.assertEqual(common.get_pagination_params(req), {}) def test_valid_marker(self): """ Test valid marker param. """ req = Request.blank('/?marker=1') - self.assertEqual(common.get_pagination_params(req), (1, 0)) + self.assertEqual(common.get_pagination_params(req), {'marker': 1}) def test_invalid_marker(self): """ Test invalid marker param. """ @@ -177,10 +177,16 @@ class PaginationParamsTest(test.TestCase): def test_valid_limit(self): """ Test valid limit param. """ req = Request.blank('/?limit=10') - self.assertEqual(common.get_pagination_params(req), (0, 10)) + self.assertEqual(common.get_pagination_params(req), {'limit': 10}) def test_invalid_limit(self): """ Test invalid limit param. """ req = Request.blank('/?limit=-2') self.assertRaises( webob.exc.HTTPBadRequest, common.get_pagination_params, req) + + def test_valid_limit_and_marker(self): + """ Test valid limit and marker parameters. """ + req = Request.blank('/?limit=20&marker=40') + self.assertEqual(common.get_pagination_params(req), + {'marker': 40, 'limit': 20}) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 446d68e9edbd..fc4fc84e29df 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -802,7 +802,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {'name': 'testname'} image_service.index( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images?name=testname') @@ -817,7 +817,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {'status': 'ACTIVE'} image_service.index( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images?status=ACTIVE') @@ -832,7 +832,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {'property-test': '3'} image_service.index( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images?property-test=3') @@ -847,7 +847,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {'status': 'ACTIVE'} image_service.index( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images?status=ACTIVE&UNSUPPORTEDFILTER=testname') @@ -862,7 +862,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {} image_service.index( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images') @@ -877,7 +877,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {'name': 'testname'} image_service.detail( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images/detail?name=testname') @@ -892,7 +892,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {'status': 'ACTIVE'} image_service.detail( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images/detail?status=ACTIVE') @@ -907,7 +907,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {'property-test': '3'} image_service.detail( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images/detail?property-test=3') @@ -922,7 +922,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {'status': 'ACTIVE'} image_service.detail( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images/detail?status=ACTIVE&UNSUPPORTEDFILTER=testname') @@ -937,7 +937,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): context = object() filters = {} image_service.detail( - context, filters=filters, marker=0, limit=0).AndReturn([]) + context, filters=filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images/detail')