summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorBrian Waldon <brian.waldon@rackspace.com>2011-07-28 16:21:12 -0400
committerBrian Waldon <brian.waldon@rackspace.com>2011-07-28 16:21:12 -0400
commit8141ef4139fbf8512150ce970cea4dc4bee22e1a (patch)
tree455720c324ca50cc7a9e3bc9d0dd573f13355d0f /nova
parent84909f4a7733dde453afcc5cc540854ac1bc458c (diff)
downloadnova-8141ef4139fbf8512150ce970cea4dc4bee22e1a.tar.gz
nova-8141ef4139fbf8512150ce970cea4dc4bee22e1a.tar.xz
nova-8141ef4139fbf8512150ce970cea4dc4bee22e1a.zip
moving server backup to /servers/<id>/action instead of POST /images
Diffstat (limited to 'nova')
-rw-r--r--nova/api/openstack/create_instance_helper.py12
-rw-r--r--nova/api/openstack/images.py112
-rw-r--r--nova/api/openstack/servers.py133
-rw-r--r--nova/tests/api/openstack/test_images.py76
-rw-r--r--nova/tests/api/openstack/test_servers.py169
5 files changed, 280 insertions, 222 deletions
diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
index 73ff191e8..e165899e7 100644
--- a/nova/api/openstack/create_instance_helper.py
+++ b/nova/api/openstack/create_instance_helper.py
@@ -300,6 +300,7 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
action_deserializer = {
'createImage': self._action_create_image,
+ 'createBackup': self._action_create_image,
}.get(action_name, self.default)
action_data = action_deserializer(action_node)
@@ -308,7 +309,16 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
def _action_create_image(self, node):
data = {}
- attributes = ['name', 'image_type', 'backup_type', 'rotation']
+ value = node.getAttribute('name')
+ if value:
+ data['name'] = value
+ metadata_node = self.find_first_child_named(node, 'metadata')
+ data['metadata'] = self.extract_metadata(metadata_node)
+ return data
+
+ def _action_create_image(self, node):
+ data = {}
+ attributes = ['name', 'backup_type', 'rotation']
for attribute in attributes:
value = node.getAttribute(attribute)
if value:
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 517a51662..b3e16c997 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -102,72 +102,27 @@ class Controller(object):
"""Indicates that you must use a Controller subclass."""
raise NotImplementedError()
- def _server_id_from_req(self, req, data):
- raise NotImplementedError()
-
- def _get_extra_properties(self, req, data):
- return {}
-
class ControllerV10(Controller):
"""Version 1.0 specific controller logic."""
def create(self, req, body):
- """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(explanation="Missing required "
- "param: %s" % param)
-
- 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")
+ """Snapshot a server instance and save the image."""
+ try:
+ image = body["image"]
+ except (KeyError, TypeError):
+ msg = _("Invalid image entity")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
try:
- server_id = self._server_id_from_req(req, body)
- except KeyError:
- raise webob.exc.HTTPBadRequest()
-
- image_name = get_param("name")
- props = self._get_extra_properties(req, body)
-
- if image_type == "snapshot":
- image = self._compute_service.snapshot(
- context, server_id, image_name,
- extra_properties=props)
- 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(
- explanation="Admin API Required")
-
- backup_type = get_param("backup_type")
- rotation = int(get_param("rotation"))
-
- image = self._compute_service.backup(
- context, server_id, image_name,
- backup_type, rotation, extra_properties=props)
- else:
- LOG.error(_("Invalid image_type '%s' passed") % image_type)
- raise webob.exc.HTTPBadRequest(explanation="Invalue image_type: "
- "%s" % image_type)
+ image_name = image["name"]
+ server_id = image["serverId"]
+ except KeyError as missing_key:
+ msg = _("Image entity requires %s") % missing_key
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ context = req.environ["nova.context"]
+ image = self._compute_service.snapshot(context, server_id, image_name)
return dict(image=self.get_builder(req).build(image, detail=True))
@@ -202,13 +157,6 @@ class ControllerV10(Controller):
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=True) for image in images])
- def _server_id_from_req(self, req, data):
- try:
- return data['image']['serverId']
- except KeyError:
- msg = _("Expected serverId attribute on server entity.")
- raise webob.exc.HTTPBadRequest(explanation=msg)
-
class ControllerV11(Controller):
"""Version 1.1 specific controller logic."""
@@ -246,38 +194,6 @@ class ControllerV11(Controller):
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=True) for image in images])
- def _server_id_from_req(self, req, data):
- try:
- server_ref = data['image']['serverRef']
- except KeyError:
- msg = _("Expected serverRef attribute on server entity.")
- raise webob.exc.HTTPBadRequest(explanation=msg)
-
- if not server_ref.startswith('http'):
- return server_ref
-
- passed = urlparse.urlparse(server_ref)
- expected = urlparse.urlparse(req.application_url)
- version = expected.path.split('/')[1]
- expected_prefix = "/%s/servers/" % version
- _empty, _sep, server_id = passed.path.partition(expected_prefix)
- scheme_ok = passed.scheme == expected.scheme
- host_ok = passed.hostname == expected.hostname
- port_ok = (passed.port == expected.port or
- passed.port == FLAGS.osapi_port)
- if not (scheme_ok and port_ok and host_ok and server_id):
- msg = _("serverRef must match request url")
- raise webob.exc.HTTPBadRequest(explanation=msg)
-
- return server_id
-
- def _get_extra_properties(self, req, data):
- server_ref = data['image']['serverRef']
- if not server_ref.startswith('http'):
- server_ref = os.path.join(req.application_url, 'servers',
- server_ref)
- return {'instance_ref': server_ref}
-
def create(self, *args, **kwargs):
raise webob.exc.HTTPMethodNotAllowed()
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 0cc81009b..fafde1c15 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -166,11 +166,79 @@ class Controller(object):
'createImage': self._action_create_image,
}
+ if FLAGS.allow_admin_api:
+ admin_actions = {
+ 'createBackup': self._action_create_backup,
+ }
+ self.actions.update(admin_actions)
+
for key in self.actions.keys():
if key in body:
return self.actions[key](body, req, id)
+
raise exc.HTTPNotImplemented()
+ def _action_create_backup(self, input_dict, req, instance_id):
+ """Backup a server instance.
+
+ 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.
+
+ """
+ entity = input_dict["createBackup"]
+
+ try:
+ image_name = entity["name"]
+ backup_type = entity["backup_type"]
+ rotation = entity["rotation"]
+
+ except KeyError as missing_key:
+ msg = _("createBackup entity requires %s attribute") % missing_key
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ except TypeError:
+ msg = _("Malformed createBackup entity")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ try:
+ rotation = int(rotation)
+ except ValueError:
+ msg = _("createBackup attribute 'rotation' must be an integer")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ # preserve link to server in image properties
+ server_ref = os.path.join(req.application_url,
+ 'servers',
+ str(instance_id))
+ props = {'instance_ref': server_ref}
+
+ metadata = entity.get('metadata', {})
+ try:
+ props.update(metadata)
+ except ValueError:
+ msg = _("Invalid metadata")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ context = req.environ["nova.context"]
+ image = self.compute_api.backup(context,
+ instance_id,
+ image_name,
+ backup_type,
+ rotation,
+ extra_properties=props)
+
+ # build location of newly-created image entity
+ image_id = str(image['id'])
+ image_ref = os.path.join(req.application_url, 'images', image_id)
+
+ resp = webob.Response(status_int=202)
+ resp.headers['Location'] = image_ref
+ return resp
+
def _action_create_image(self, input_dict, req, id):
return exc.HTTPNotImplemented()
@@ -599,30 +667,22 @@ class ControllerV11(Controller):
return webob.Response(status_int=202)
- def _action_create_image(self, input_dict, req, instance_id):
- """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.
- """
- entity = input_dict.get('createImage', {})
+ def _action_create_image(self, input_dict, req, instance_id):
+ """Snapshot a server instance."""
+ entity = input_dict.get("createImage", {})
- def get_param(param):
- try:
- return entity[param]
- except KeyError:
- msg = _("Missing required param: %s") % param
- raise webob.exc.HTTPBadRequest(explanation=msg)
+ try:
+ image_name = entity["name"]
- context = req.environ['nova.context']
+ except KeyError:
+ msg = _("createImage entity requires name attribute")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
- image_name = get_param("name")
- image_type = entity.get("image_type", "snapshot")
+ except TypeError:
+ msg = _("Malformed createImage entity")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
# preserve link to server in image properties
server_ref = os.path.join(req.application_url,
@@ -637,36 +697,15 @@ class ControllerV11(Controller):
msg = _("Invalid metadata")
raise webob.exc.HTTPBadRequest(explanation=msg)
- if image_type == "snapshot":
- image = self.compute_api.snapshot(context,
- instance_id,
- image_name,
- extra_properties=props)
-
- 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:
- msg = _("Admin API Required")
- raise webob.exc.HTTPBadRequest(explanation=msg)
-
- backup_type = get_param("backup_type")
- rotation = int(get_param("rotation"))
-
- image = self.compute_api.backup(context,
- instance_id,
- image_name,
- backup_type,
- rotation,
- extra_properties=props)
- else:
- msg = _("Invalid image_type '%s'") % image_type
- raise webob.exc.HTTPBadRequest(explanation=msg)
+ context = req.environ['nova.context']
+ image = self.compute_api.snapshot(context,
+ instance_id,
+ image_name,
+ extra_properties=props)
# build location of newly-created image entity
- image_ref = os.path.join(req.application_url,
- 'images',
- str(image['id']))
+ image_id = str(image['id'])
+ image_ref = os.path.join(req.application_url, 'images', image_id)
resp = webob.Response(status_int=202)
resp.headers['Location'] = image_ref
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 50a6be66c..fa5422955 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -1045,82 +1045,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
- 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)
-
- 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.
- """
- # 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='backup',
- name='Backup 1',
- 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(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', 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)
- 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'))
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 9d2c7b73f..62501ed9d 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -2304,6 +2304,152 @@ class ServersTest(test.TestCase):
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
+ def test_create_backup(self):
+ """The happy path for creating backups"""
+ FLAGS.allow_admin_api = True
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ self.assertTrue(response.headers['Location'])
+
+ def test_create_backup_v1_1(self):
+ """The happy path for creating backups through v1.1 api"""
+ FLAGS.allow_admin_api = True
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ self.assertTrue(response.headers['Location'])
+
+ def test_create_backup_admin_api_off(self):
+ """The happy path for creating backups"""
+ FLAGS.allow_admin_api = False
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(501, response.status_int)
+
+ def test_create_backup_with_metadata(self):
+ FLAGS.allow_admin_api = True
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ 'metadata': {'123': 'asdf'},
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ self.assertTrue(response.headers['Location'])
+
+ def test_create_backup_no_name(self):
+ """Name is required for backups"""
+ FLAGS.allow_admin_api = True
+
+ body = {
+ 'createBackup': {
+ '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)
+
+ def test_create_backup_no_rotation(self):
+ """Rotation is required for backup requests"""
+ FLAGS.allow_admin_api = True
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ '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"""
+ FLAGS.allow_admin_api = True
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ '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_backup_bad_entity(self):
+ FLAGS.allow_admin_api = True
+
+ body = {'createBackup': 'go'}
+ 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)
+
class TestServerActionXMLDeserializer(test.TestCase):
@@ -2343,6 +2489,29 @@ class TestServerActionXMLDeserializer(test.TestCase):
}
self.assertEquals(request['body'], expected)
+ def test_create_backup_with_metadata(self):
+ serial_request = """
+<createBackup xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test"
+ rotation="12"
+ backup_type="daily">
+ <metadata>
+ <meta key="key1">value1</meta>
+ </metadata>
+</createBackup>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "createBackup": {
+ "name": "new-server-test",
+ "rotation": "12",
+ "backup_type": "daily",
+ "metadata": {"key1": "value1"},
+ },
+ }
+ self.assertEquals(request['body'], expected)
+
+
+
class TestServerCreateRequestXMLDeserializerV10(unittest.TestCase):