Accept a full serverRef to OSAPI POST /images (snapshot)
This commit is contained in:
commit
006cbeb5f1
@ -13,6 +13,8 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
from nova import compute
|
from nova import compute
|
||||||
@ -99,21 +101,27 @@ class Controller(object):
|
|||||||
raise webob.exc.HTTPBadRequest()
|
raise webob.exc.HTTPBadRequest()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server_id = self._server_id_from_req_data(body)
|
server_id = self._server_id_from_req(req, body)
|
||||||
image_name = body["image"]["name"]
|
image_name = body["image"]["name"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise webob.exc.HTTPBadRequest()
|
raise webob.exc.HTTPBadRequest()
|
||||||
|
|
||||||
image = self._compute_service.snapshot(context, server_id, image_name)
|
props = self._get_extra_properties(req, body)
|
||||||
|
|
||||||
|
image = self._compute_service.snapshot(context, server_id,
|
||||||
|
image_name, props)
|
||||||
return dict(image=self.get_builder(req).build(image, detail=True))
|
return dict(image=self.get_builder(req).build(image, detail=True))
|
||||||
|
|
||||||
def get_builder(self, request):
|
def get_builder(self, request):
|
||||||
"""Indicates that you must use a Controller subclass."""
|
"""Indicates that you must use a Controller subclass."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _server_id_from_req_data(self, data):
|
def _server_id_from_req(self, req, data):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _get_extra_properties(self, req, data):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class ControllerV10(Controller):
|
class ControllerV10(Controller):
|
||||||
"""Version 1.0 specific controller logic."""
|
"""Version 1.0 specific controller logic."""
|
||||||
@ -149,8 +157,12 @@ class ControllerV10(Controller):
|
|||||||
builder = self.get_builder(req).build
|
builder = self.get_builder(req).build
|
||||||
return dict(images=[builder(image, detail=True) for image in images])
|
return dict(images=[builder(image, detail=True) for image in images])
|
||||||
|
|
||||||
def _server_id_from_req_data(self, data):
|
def _server_id_from_req(self, req, data):
|
||||||
return data['image']['serverId']
|
try:
|
||||||
|
return data['image']['serverId']
|
||||||
|
except KeyError:
|
||||||
|
msg = _("Expected serverId attribute on server entity.")
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
|
||||||
class ControllerV11(Controller):
|
class ControllerV11(Controller):
|
||||||
@ -189,8 +201,27 @@ class ControllerV11(Controller):
|
|||||||
builder = self.get_builder(req).build
|
builder = self.get_builder(req).build
|
||||||
return dict(images=[builder(image, detail=True) for image in images])
|
return dict(images=[builder(image, detail=True) for image in images])
|
||||||
|
|
||||||
def _server_id_from_req_data(self, data):
|
def _server_id_from_req(self, req, data):
|
||||||
return data['image']['serverRef']
|
try:
|
||||||
|
server_ref = data['image']['serverRef']
|
||||||
|
except KeyError:
|
||||||
|
msg = _("Expected serverRef attribute on server entity.")
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
head, tail = os.path.split(server_ref)
|
||||||
|
|
||||||
|
if head and head != os.path.join(req.application_url, 'servers'):
|
||||||
|
msg = _("serverRef must match request url")
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
return tail
|
||||||
|
|
||||||
|
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_resource(version='1.0'):
|
def create_resource(version='1.0'):
|
||||||
|
@ -46,13 +46,9 @@ class ViewBuilder(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
image['status'] = image['status'].upper()
|
image['status'] = image['status'].upper()
|
||||||
|
|
||||||
def _build_server(self, image, instance_id):
|
def _build_server(self, image, image_obj):
|
||||||
"""Indicates that you must use a ViewBuilder subclass."""
|
"""Indicates that you must use a ViewBuilder subclass."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
|
|
||||||
def generate_server_ref(self, server_id):
|
|
||||||
"""Return an href string pointing to this server."""
|
|
||||||
return os.path.join(self._url, "servers", str(server_id))
|
|
||||||
|
|
||||||
def generate_href(self, image_id):
|
def generate_href(self, image_id):
|
||||||
"""Return an href string pointing to this object."""
|
"""Return an href string pointing to this object."""
|
||||||
@ -60,8 +56,6 @@ class ViewBuilder(object):
|
|||||||
|
|
||||||
def build(self, image_obj, detail=False):
|
def build(self, image_obj, detail=False):
|
||||||
"""Return a standardized image structure for display by the API."""
|
"""Return a standardized image structure for display by the API."""
|
||||||
properties = image_obj.get("properties", {})
|
|
||||||
|
|
||||||
self._format_dates(image_obj)
|
self._format_dates(image_obj)
|
||||||
|
|
||||||
if "status" in image_obj:
|
if "status" in image_obj:
|
||||||
@ -72,11 +66,7 @@ class ViewBuilder(object):
|
|||||||
"name": image_obj.get("name"),
|
"name": image_obj.get("name"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if "instance_id" in properties:
|
self._build_server(image, image_obj)
|
||||||
try:
|
|
||||||
self._build_server(image, int(properties["instance_id"]))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if detail:
|
if detail:
|
||||||
image.update({
|
image.update({
|
||||||
@ -94,15 +84,21 @@ class ViewBuilder(object):
|
|||||||
class ViewBuilderV10(ViewBuilder):
|
class ViewBuilderV10(ViewBuilder):
|
||||||
"""OpenStack API v1.0 Image Builder"""
|
"""OpenStack API v1.0 Image Builder"""
|
||||||
|
|
||||||
def _build_server(self, image, instance_id):
|
def _build_server(self, image, image_obj):
|
||||||
image["serverId"] = instance_id
|
try:
|
||||||
|
image['serverId'] = int(image_obj['properties']['instance_id'])
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ViewBuilderV11(ViewBuilder):
|
class ViewBuilderV11(ViewBuilder):
|
||||||
"""OpenStack API v1.1 Image Builder"""
|
"""OpenStack API v1.1 Image Builder"""
|
||||||
|
|
||||||
def _build_server(self, image, instance_id):
|
def _build_server(self, image, image_obj):
|
||||||
image["serverRef"] = self.generate_server_ref(instance_id)
|
try:
|
||||||
|
image['serverRef'] = image_obj['properties']['instance_ref']
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
|
||||||
def build(self, image_obj, detail=False):
|
def build(self, image_obj, detail=False):
|
||||||
"""Return a standardized image structure for display by the API."""
|
"""Return a standardized image structure for display by the API."""
|
||||||
|
@ -701,7 +701,7 @@ class API(base.Base):
|
|||||||
raise exception.Error(_("Unable to find host for Instance %s")
|
raise exception.Error(_("Unable to find host for Instance %s")
|
||||||
% instance_id)
|
% instance_id)
|
||||||
|
|
||||||
def snapshot(self, context, instance_id, name):
|
def snapshot(self, context, instance_id, name, extra_properties=None):
|
||||||
"""Snapshot the given instance.
|
"""Snapshot the given instance.
|
||||||
|
|
||||||
:returns: A dict containing image metadata
|
:returns: A dict containing image metadata
|
||||||
@ -709,6 +709,7 @@ class API(base.Base):
|
|||||||
properties = {'instance_id': str(instance_id),
|
properties = {'instance_id': str(instance_id),
|
||||||
'user_id': str(context.user_id),
|
'user_id': str(context.user_id),
|
||||||
'image_state': 'creating'}
|
'image_state': 'creating'}
|
||||||
|
properties.update(extra_properties or {})
|
||||||
sent_meta = {'name': name, 'is_public': False,
|
sent_meta = {'name': name, 'is_public': False,
|
||||||
'status': 'creating', 'properties': properties}
|
'status': 'creating', 'properties': properties}
|
||||||
recv_meta = self.image_service.create(context, sent_meta)
|
recv_meta = self.image_service.create(context, sent_meta)
|
||||||
|
@ -140,9 +140,10 @@ def stub_out_networking(stubs):
|
|||||||
|
|
||||||
|
|
||||||
def stub_out_compute_api_snapshot(stubs):
|
def stub_out_compute_api_snapshot(stubs):
|
||||||
def snapshot(self, context, instance_id, name):
|
def snapshot(self, context, instance_id, name, extra_properties=None):
|
||||||
return dict(id='123', status='ACTIVE',
|
props = dict(instance_id=instance_id, instance_ref=instance_id)
|
||||||
properties=dict(instance_id='123'))
|
props.update(extra_properties or {})
|
||||||
|
return dict(id='123', status='ACTIVE', name=name, properties=props)
|
||||||
stubs.Set(nova.compute.API, 'snapshot', snapshot)
|
stubs.Set(nova.compute.API, 'snapshot', snapshot)
|
||||||
|
|
||||||
|
|
||||||
|
@ -618,7 +618,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
{
|
{
|
||||||
'id': 124,
|
'id': 124,
|
||||||
'name': 'queued backup',
|
'name': 'queued backup',
|
||||||
'serverId': 42,
|
|
||||||
'updated': self.NOW_API_FORMAT,
|
'updated': self.NOW_API_FORMAT,
|
||||||
'created': self.NOW_API_FORMAT,
|
'created': self.NOW_API_FORMAT,
|
||||||
'status': 'QUEUED',
|
'status': 'QUEUED',
|
||||||
@ -626,7 +625,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
{
|
{
|
||||||
'id': 125,
|
'id': 125,
|
||||||
'name': 'saving backup',
|
'name': 'saving backup',
|
||||||
'serverId': 42,
|
|
||||||
'updated': self.NOW_API_FORMAT,
|
'updated': self.NOW_API_FORMAT,
|
||||||
'created': self.NOW_API_FORMAT,
|
'created': self.NOW_API_FORMAT,
|
||||||
'status': 'SAVING',
|
'status': 'SAVING',
|
||||||
@ -635,7 +633,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
{
|
{
|
||||||
'id': 126,
|
'id': 126,
|
||||||
'name': 'active backup',
|
'name': 'active backup',
|
||||||
'serverId': 42,
|
|
||||||
'updated': self.NOW_API_FORMAT,
|
'updated': self.NOW_API_FORMAT,
|
||||||
'created': self.NOW_API_FORMAT,
|
'created': self.NOW_API_FORMAT,
|
||||||
'status': 'ACTIVE'
|
'status': 'ACTIVE'
|
||||||
@ -643,7 +640,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
{
|
{
|
||||||
'id': 127,
|
'id': 127,
|
||||||
'name': 'killed backup',
|
'name': 'killed backup',
|
||||||
'serverId': 42,
|
|
||||||
'updated': self.NOW_API_FORMAT,
|
'updated': self.NOW_API_FORMAT,
|
||||||
'created': self.NOW_API_FORMAT,
|
'created': self.NOW_API_FORMAT,
|
||||||
'status': 'FAILED',
|
'status': 'FAILED',
|
||||||
@ -689,7 +685,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
{
|
{
|
||||||
'id': 124,
|
'id': 124,
|
||||||
'name': 'queued backup',
|
'name': 'queued backup',
|
||||||
'serverRef': "http://localhost/v1.1/servers/42",
|
'serverRef': "http://localhost:8774/v1.1/servers/42",
|
||||||
'updated': self.NOW_API_FORMAT,
|
'updated': self.NOW_API_FORMAT,
|
||||||
'created': self.NOW_API_FORMAT,
|
'created': self.NOW_API_FORMAT,
|
||||||
'status': 'QUEUED',
|
'status': 'QUEUED',
|
||||||
@ -711,7 +707,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
{
|
{
|
||||||
'id': 125,
|
'id': 125,
|
||||||
'name': 'saving backup',
|
'name': 'saving backup',
|
||||||
'serverRef': "http://localhost/v1.1/servers/42",
|
'serverRef': "http://localhost:8774/v1.1/servers/42",
|
||||||
'updated': self.NOW_API_FORMAT,
|
'updated': self.NOW_API_FORMAT,
|
||||||
'created': self.NOW_API_FORMAT,
|
'created': self.NOW_API_FORMAT,
|
||||||
'status': 'SAVING',
|
'status': 'SAVING',
|
||||||
@ -734,7 +730,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
{
|
{
|
||||||
'id': 126,
|
'id': 126,
|
||||||
'name': 'active backup',
|
'name': 'active backup',
|
||||||
'serverRef': "http://localhost/v1.1/servers/42",
|
'serverRef': "http://localhost:8774/v1.1/servers/42",
|
||||||
'updated': self.NOW_API_FORMAT,
|
'updated': self.NOW_API_FORMAT,
|
||||||
'created': self.NOW_API_FORMAT,
|
'created': self.NOW_API_FORMAT,
|
||||||
'status': 'ACTIVE',
|
'status': 'ACTIVE',
|
||||||
@ -756,7 +752,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
{
|
{
|
||||||
'id': 127,
|
'id': 127,
|
||||||
'name': 'killed backup',
|
'name': 'killed backup',
|
||||||
'serverRef': "http://localhost/v1.1/servers/42",
|
'serverRef': "http://localhost:8774/v1.1/servers/42",
|
||||||
'updated': self.NOW_API_FORMAT,
|
'updated': self.NOW_API_FORMAT,
|
||||||
'created': self.NOW_API_FORMAT,
|
'created': self.NOW_API_FORMAT,
|
||||||
'status': 'FAILED',
|
'status': 'FAILED',
|
||||||
@ -1002,6 +998,30 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
response = req.get_response(fakes.wsgi_app())
|
response = req.get_response(fakes.wsgi_app())
|
||||||
self.assertEqual(200, response.status_int)
|
self.assertEqual(200, response.status_int)
|
||||||
|
|
||||||
|
def test_create_image_v1_1_actual_server_ref(self):
|
||||||
|
|
||||||
|
serverRef = 'http://localhost/v1.1/servers/1'
|
||||||
|
body = dict(image=dict(serverRef=serverRef, name='Backup 1'))
|
||||||
|
req = webob.Request.blank('/v1.1/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)
|
||||||
|
result = json.loads(response.body)
|
||||||
|
self.assertEqual(result['image']['serverRef'], serverRef)
|
||||||
|
|
||||||
|
def test_create_image_v1_1_server_ref_bad_hostname(self):
|
||||||
|
|
||||||
|
serverRef = 'http://asdf/v1.1/servers/1'
|
||||||
|
body = dict(image=dict(serverRef=serverRef, name='Backup 1'))
|
||||||
|
req = webob.Request.blank('/v1.1/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_v1_1_xml_serialization(self):
|
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='Backup 1'))
|
||||||
@ -1018,7 +1038,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
<image
|
<image
|
||||||
created="None"
|
created="None"
|
||||||
id="123"
|
id="123"
|
||||||
name="None"
|
name="Backup 1"
|
||||||
serverRef="http://localhost/v1.1/servers/123"
|
serverRef="http://localhost/v1.1/servers/123"
|
||||||
status="ACTIVE"
|
status="ACTIVE"
|
||||||
updated="None"
|
updated="None"
|
||||||
@ -1065,7 +1085,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
|||||||
image_id += 1
|
image_id += 1
|
||||||
|
|
||||||
# Backup for User 1
|
# Backup for User 1
|
||||||
backup_properties = {'instance_id': '42', 'user_id': '1'}
|
server_ref = 'http://localhost:8774/v1.1/servers/42'
|
||||||
|
backup_properties = {'instance_ref': server_ref, 'user_id': '1'}
|
||||||
for status in ('queued', 'saving', 'active', 'killed'):
|
for status in ('queued', 'saving', 'active', 'killed'):
|
||||||
add_fixture(id=image_id, name='%s backup' % status,
|
add_fixture(id=image_id, name='%s backup' % status,
|
||||||
is_public=False, status=status,
|
is_public=False, status=status,
|
||||||
|
Loading…
Reference in New Issue
Block a user