Adds create backup server extension for the V3 API
Moves create_backup functionality out of admin_actions into its own extension. This is part of blueprint v3-admin-actions-split which allows more selective enablement of features contained in the admin actions extension. Note that XML api samples are no longer generated because bp remove-v3-xml-api has been approved. Partially implements bp v3-api-admin-actions-split DocImpact: Adds os-create-backup extension and moves the create_backup functionality out of os-admin-actions into this new extension. Change-Id: I4858a06df20c552bd55ff2841adbcfc761304f42
This commit is contained in:
16
doc/v3/api_samples/os-create-backup/server-post-req.json
Normal file
16
doc/v3/api_samples/os-create-backup/server-post-req.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"server" : {
|
||||
"name" : "new-server-test",
|
||||
"image_ref" : "http://glance.openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b",
|
||||
"flavor_ref" : "http://openstack.example.com/flavors/1",
|
||||
"metadata" : {
|
||||
"My Server Name" : "Apache1"
|
||||
},
|
||||
"personality" : [
|
||||
{
|
||||
"path" : "/etc/banner.txt",
|
||||
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
16
doc/v3/api_samples/os-create-backup/server-post-resp.json
Normal file
16
doc/v3/api_samples/os-create-backup/server-post-resp.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"server": {
|
||||
"admin_password": "DM3QzjhGTzLB",
|
||||
"id": "bebeec79-497e-4711-a311-d0d2e3dfc73b",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://openstack.example.com/v3/servers/bebeec79-497e-4711-a311-d0d2e3dfc73b",
|
||||
"rel": "self"
|
||||
},
|
||||
{
|
||||
"href": "http://openstack.example.com/servers/bebeec79-497e-4711-a311-d0d2e3dfc73b",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@
|
||||
"compute_extension:v3:os-admin-actions:discoverable": "",
|
||||
"compute_extension:v3:os-admin-actions:reset_network": "rule:admin_api",
|
||||
"compute_extension:v3:os-admin-actions:inject_network_info": "rule:admin_api",
|
||||
"compute_extension:v3:os-admin-actions:create_backup": "rule:admin_or_owner",
|
||||
"compute_extension:v3:os-admin-actions:reset_state": "rule:admin_api",
|
||||
"compute_extension:v3:os-admin-password": "",
|
||||
"compute_extension:v3:os-admin-password:discoverable": "",
|
||||
@@ -83,6 +82,8 @@
|
||||
"compute_extension:v3:os-remote-consoles": "",
|
||||
"compute_extension:v3:os-remote-consoles:discoverable": "",
|
||||
"compute_extension:createserverext": "",
|
||||
"compute_extension:v3:os-create-backup:discoverable": "",
|
||||
"compute_extension:v3:os-create-backup": "rule:admin_or_owner",
|
||||
"compute_extension:deferred_delete": "",
|
||||
"compute_extension:v3:os-deferred-delete": "",
|
||||
"compute_extension:v3:os-deferred-delete:discoverable": "",
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os.path
|
||||
|
||||
import webob
|
||||
from webob import exc
|
||||
|
||||
@@ -71,74 +69,6 @@ class AdminActionsController(wsgi.Controller):
|
||||
raise exc.HTTPConflict(explanation=e.format_message())
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@extensions.expected_errors((400, 404, 409, 413))
|
||||
@wsgi.action('create_backup')
|
||||
def _create_backup(self, req, id, body):
|
||||
"""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.
|
||||
|
||||
"""
|
||||
context = req.environ["nova.context"]
|
||||
authorize(context, 'create_backup')
|
||||
entity = body["create_backup"]
|
||||
|
||||
try:
|
||||
image_name = entity["name"]
|
||||
backup_type = entity["backup_type"]
|
||||
rotation = entity["rotation"]
|
||||
|
||||
except KeyError as missing_key:
|
||||
msg = _("create_backup entity requires %s attribute") % missing_key
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
except TypeError:
|
||||
msg = _("Malformed create_backup entity")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
try:
|
||||
rotation = int(rotation)
|
||||
except ValueError:
|
||||
msg = _("create_backup attribute 'rotation' must be an integer")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
if rotation < 0:
|
||||
msg = _("create_backup attribute 'rotation' must be greater "
|
||||
"than or equal to zero")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
props = {}
|
||||
metadata = entity.get('metadata', {})
|
||||
common.check_img_metadata_properties_quota(context, metadata)
|
||||
try:
|
||||
props.update(metadata)
|
||||
except ValueError:
|
||||
msg = _("Invalid metadata")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
instance = common.get_instance(self.compute_api, context, id,
|
||||
want_objects=True)
|
||||
try:
|
||||
image = self.compute_api.backup(context, instance, image_name,
|
||||
backup_type, rotation, extra_properties=props)
|
||||
except exception.InstanceInvalidState as state_error:
|
||||
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
||||
'create_backup')
|
||||
|
||||
resp = webob.Response(status_int=202)
|
||||
|
||||
# build location of newly-created image entity if rotation is not zero
|
||||
if rotation > 0:
|
||||
image_id = str(image['id'])
|
||||
image_ref = os.path.join(req.application_url, 'images', image_id)
|
||||
resp.headers['Location'] = image_ref
|
||||
|
||||
return resp
|
||||
|
||||
@extensions.expected_errors((400, 404))
|
||||
@wsgi.action('reset_state')
|
||||
def _reset_state(self, req, id, body):
|
||||
|
||||
123
nova/api/openstack/compute/plugins/v3/create_backup.py
Normal file
123
nova/api/openstack/compute/plugins/v3/create_backup.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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 os.path
|
||||
|
||||
import webob
|
||||
from webob import exc
|
||||
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
from nova import compute
|
||||
from nova import exception
|
||||
from nova.openstack.common.gettextutils import _
|
||||
from nova.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
ALIAS = "os-create-backup"
|
||||
authorize = extensions.extension_authorizer('compute', "v3:" + ALIAS)
|
||||
|
||||
|
||||
class CreateBackupController(wsgi.Controller):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateBackupController, self).__init__(*args, **kwargs)
|
||||
self.compute_api = compute.API()
|
||||
|
||||
@extensions.expected_errors((400, 404, 409, 413))
|
||||
@wsgi.action('create_backup')
|
||||
def _create_backup(self, req, id, body):
|
||||
"""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.
|
||||
|
||||
"""
|
||||
context = req.environ["nova.context"]
|
||||
authorize(context)
|
||||
entity = body["create_backup"]
|
||||
|
||||
try:
|
||||
image_name = entity["name"]
|
||||
backup_type = entity["backup_type"]
|
||||
rotation = entity["rotation"]
|
||||
|
||||
except KeyError as missing_key:
|
||||
msg = _("create_backup entity requires %s attribute") % missing_key
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
except TypeError:
|
||||
msg = _("Malformed create_backup entity")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
try:
|
||||
rotation = int(rotation)
|
||||
except ValueError:
|
||||
msg = _("create_backup attribute 'rotation' must be an integer")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
if rotation < 0:
|
||||
msg = _("create_backup attribute 'rotation' must be greater "
|
||||
"than or equal to zero")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
props = {}
|
||||
metadata = entity.get('metadata', {})
|
||||
common.check_img_metadata_properties_quota(context, metadata)
|
||||
try:
|
||||
props.update(metadata)
|
||||
except ValueError:
|
||||
msg = _("Invalid metadata")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
instance = common.get_instance(self.compute_api, context, id,
|
||||
want_objects=True)
|
||||
|
||||
try:
|
||||
image = self.compute_api.backup(context, instance, image_name,
|
||||
backup_type, rotation, extra_properties=props)
|
||||
except exception.InstanceInvalidState as state_error:
|
||||
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
||||
'create_backup')
|
||||
|
||||
resp = webob.Response(status_int=202)
|
||||
|
||||
# build location of newly-created image entity if rotation is not zero
|
||||
if rotation > 0:
|
||||
image_id = str(image['id'])
|
||||
image_ref = os.path.join(req.application_url, 'images', image_id)
|
||||
resp.headers['Location'] = image_ref
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
class CreateBackup(extensions.V3APIExtensionBase):
|
||||
"""Create a backup of a server."""
|
||||
|
||||
name = "CreateBackup"
|
||||
alias = ALIAS
|
||||
namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS
|
||||
version = 1
|
||||
|
||||
def get_controller_extensions(self):
|
||||
controller = CreateBackupController()
|
||||
extension = extensions.ControllerExtension(self, 'servers', controller)
|
||||
return [extension]
|
||||
|
||||
def get_resources(self):
|
||||
return []
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
import webob
|
||||
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack.compute.plugins.v3 import admin_actions
|
||||
from nova.compute import vm_states
|
||||
import nova.context
|
||||
@@ -183,185 +182,6 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase):
|
||||
self.mox.StubOutWithMock(self.compute_api, 'get')
|
||||
|
||||
|
||||
class CreateBackupTests(CommonMixin, test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
super(CreateBackupTests, self).setUp()
|
||||
self.mox.StubOutWithMock(common,
|
||||
'check_img_metadata_properties_quota')
|
||||
self.mox.StubOutWithMock(self.compute_api,
|
||||
'backup')
|
||||
|
||||
def _make_url(self, uuid):
|
||||
return '/servers/%s/action' % uuid
|
||||
|
||||
def test_create_backup_with_metadata(self):
|
||||
metadata = {'123': 'asdf'}
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
'metadata': metadata,
|
||||
},
|
||||
}
|
||||
|
||||
image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
|
||||
properties=metadata)
|
||||
|
||||
common.check_img_metadata_properties_quota(self.context, metadata)
|
||||
instance = self._stub_instance_get()
|
||||
self.compute_api.backup(self.context, instance, 'Backup 1',
|
||||
'daily', 1,
|
||||
extra_properties=metadata).AndReturn(image)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._make_request(self._make_url(instance['uuid']), body)
|
||||
self.assertEqual(202, res.status_int)
|
||||
self.assertIn('fake-image-id', res.headers['Location'])
|
||||
|
||||
def test_create_backup_no_name(self):
|
||||
# Name is required for backups.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url('fake'), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_no_rotation(self):
|
||||
# Rotation is required for backup requests.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url('fake'), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_negative_rotation(self):
|
||||
"""Rotation must be greater than or equal to zero
|
||||
for backup requests
|
||||
"""
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': -1,
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url('fake'), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_no_backup_type(self):
|
||||
# Backup Type (daily or weekly) is required for backup requests.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url('fake'), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_bad_entity(self):
|
||||
body = {'create_backup': 'go'}
|
||||
res = self._make_request(self._make_url('fake'), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_rotation_is_zero(self):
|
||||
# The happy path for creating backups if rotation is zero.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 0,
|
||||
},
|
||||
}
|
||||
|
||||
image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
|
||||
properties={})
|
||||
common.check_img_metadata_properties_quota(self.context, {})
|
||||
instance = self._stub_instance_get()
|
||||
self.compute_api.backup(self.context, instance, 'Backup 1',
|
||||
'daily', 0,
|
||||
extra_properties={}).AndReturn(image)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._make_request(self._make_url(instance['uuid']), body)
|
||||
self.assertEqual(202, res.status_int)
|
||||
self.assertNotIn('Location', res.headers)
|
||||
|
||||
def test_create_backup_rotation_is_positive(self):
|
||||
# The happy path for creating backups if rotation is positive.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
|
||||
image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
|
||||
properties={})
|
||||
common.check_img_metadata_properties_quota(self.context, {})
|
||||
instance = self._stub_instance_get()
|
||||
self.compute_api.backup(self.context, instance, 'Backup 1',
|
||||
'daily', 1,
|
||||
extra_properties={}).AndReturn(image)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._make_request(self._make_url(instance['uuid']), body)
|
||||
self.assertEqual(202, res.status_int)
|
||||
self.assertIn('fake-image-id', res.headers['Location'])
|
||||
|
||||
def test_create_backup_raises_conflict_on_invalid_state(self):
|
||||
body_map = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
args_map = {
|
||||
'create_backup': (
|
||||
('Backup 1', 'daily', 1), {'extra_properties': {}}
|
||||
),
|
||||
}
|
||||
common.check_img_metadata_properties_quota(self.context, {})
|
||||
self._test_invalid_state('create_backup', method='backup',
|
||||
body_map=body_map,
|
||||
compute_api_args_map=args_map)
|
||||
|
||||
def test_create_backup_with_non_existed_instance(self):
|
||||
body_map = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
common.check_img_metadata_properties_quota(self.context, {})
|
||||
self._test_non_existing_instance('create_backup',
|
||||
body_map=body_map)
|
||||
|
||||
def test_create_backup_with_invalid_create_backup(self):
|
||||
body = {
|
||||
'create_backupup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url('fake'), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
|
||||
class ResetStateTests(test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
super(ResetStateTests, self).setUp()
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack.compute.plugins.v3 import create_backup
|
||||
from nova.openstack.common import uuidutils
|
||||
from nova import test
|
||||
from nova.tests.api.openstack.compute.plugins.v3 import \
|
||||
admin_only_action_common
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
|
||||
class CreateBackupTests(admin_only_action_common.CommonMixin,
|
||||
test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
super(CreateBackupTests, self).setUp()
|
||||
self.controller = create_backup.CreateBackupController()
|
||||
self.compute_api = self.controller.compute_api
|
||||
|
||||
def _fake_controller(*args, **kwargs):
|
||||
return self.controller
|
||||
|
||||
self.stubs.Set(create_backup, 'CreateBackupController',
|
||||
_fake_controller)
|
||||
self.app = fakes.wsgi_app_v3(init_only=('servers',
|
||||
'os-create-backup'),
|
||||
fake_auth_context=self.context)
|
||||
self.mox.StubOutWithMock(self.compute_api, 'get')
|
||||
self.mox.StubOutWithMock(common,
|
||||
'check_img_metadata_properties_quota')
|
||||
self.mox.StubOutWithMock(self.compute_api, 'backup')
|
||||
|
||||
def _make_url(self, uuid=None):
|
||||
if uuid is None:
|
||||
uuid = uuidutils.generate_uuid()
|
||||
return '/servers/%s/action' % uuid
|
||||
|
||||
def test_create_backup_with_metadata(self):
|
||||
metadata = {'123': 'asdf'}
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
'metadata': metadata,
|
||||
},
|
||||
}
|
||||
|
||||
image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
|
||||
properties=metadata)
|
||||
|
||||
common.check_img_metadata_properties_quota(self.context, metadata)
|
||||
instance = self._stub_instance_get()
|
||||
self.compute_api.backup(self.context, instance, 'Backup 1',
|
||||
'daily', 1,
|
||||
extra_properties=metadata).AndReturn(image)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._make_request(self._make_url(instance.uuid), body)
|
||||
self.assertEqual(202, res.status_int)
|
||||
self.assertIn('fake-image-id', res.headers['Location'])
|
||||
|
||||
def test_create_backup_no_name(self):
|
||||
# Name is required for backups.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url(), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_no_rotation(self):
|
||||
# Rotation is required for backup requests.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url(), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_negative_rotation(self):
|
||||
"""Rotation must be greater than or equal to zero
|
||||
for backup requests
|
||||
"""
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': -1,
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url(), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_no_backup_type(self):
|
||||
# Backup Type (daily or weekly) is required for backup requests.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url(), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_bad_entity(self):
|
||||
body = {'create_backup': 'go'}
|
||||
res = self._make_request(self._make_url(), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_create_backup_rotation_is_zero(self):
|
||||
# The happy path for creating backups if rotation is zero.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 0,
|
||||
},
|
||||
}
|
||||
|
||||
image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
|
||||
properties={})
|
||||
common.check_img_metadata_properties_quota(self.context, {})
|
||||
instance = self._stub_instance_get()
|
||||
self.compute_api.backup(self.context, instance, 'Backup 1',
|
||||
'daily', 0,
|
||||
extra_properties={}).AndReturn(image)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._make_request(self._make_url(instance.uuid), body)
|
||||
self.assertEqual(202, res.status_int)
|
||||
self.assertNotIn('Location', res.headers)
|
||||
|
||||
def test_create_backup_rotation_is_positive(self):
|
||||
# The happy path for creating backups if rotation is positive.
|
||||
body = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
|
||||
image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
|
||||
properties={})
|
||||
common.check_img_metadata_properties_quota(self.context, {})
|
||||
instance = self._stub_instance_get()
|
||||
self.compute_api.backup(self.context, instance, 'Backup 1',
|
||||
'daily', 1,
|
||||
extra_properties={}).AndReturn(image)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._make_request(self._make_url(instance.uuid), body)
|
||||
self.assertEqual(202, res.status_int)
|
||||
self.assertIn('fake-image-id', res.headers['Location'])
|
||||
|
||||
def test_create_backup_raises_conflict_on_invalid_state(self):
|
||||
body_map = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
args_map = {
|
||||
'create_backup': (
|
||||
('Backup 1', 'daily', 1), {'extra_properties': {}}
|
||||
),
|
||||
}
|
||||
common.check_img_metadata_properties_quota(self.context, {})
|
||||
self._test_invalid_state('create_backup', method='backup',
|
||||
body_map=body_map,
|
||||
compute_api_args_map=args_map)
|
||||
|
||||
def test_create_backup_with_non_existed_instance(self):
|
||||
body_map = {
|
||||
'create_backup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
common.check_img_metadata_properties_quota(self.context, {})
|
||||
self._test_non_existing_instance('create_backup',
|
||||
body_map=body_map)
|
||||
|
||||
def test_create_backup_with_invalid_create_backup(self):
|
||||
body = {
|
||||
'create_backupup': {
|
||||
'name': 'Backup 1',
|
||||
'backup_type': 'daily',
|
||||
'rotation': 1,
|
||||
},
|
||||
}
|
||||
res = self._make_request(self._make_url(), body)
|
||||
self.assertEqual(400, res.status_int)
|
||||
@@ -119,7 +119,6 @@ policy_data = """
|
||||
"compute_extension:admin_actions:migrate": "",
|
||||
"compute_extension:v3:os-admin-actions:reset_network": "",
|
||||
"compute_extension:v3:os-admin-actions:inject_network_info": "",
|
||||
"compute_extension:v3:os-admin-actions:create_backup": "",
|
||||
"compute_extension:v3:os-admin-actions:reset_state": "",
|
||||
"compute_extension:v3:os-admin-password": "",
|
||||
"compute_extension:aggregates": "rule:admin_api",
|
||||
@@ -150,6 +149,7 @@ policy_data = """
|
||||
"compute_extension:consoles": "",
|
||||
"compute_extension:v3:os-remote-consoles": "",
|
||||
"compute_extension:createserverext": "",
|
||||
"compute_extension:v3:os-create-backup": "",
|
||||
"compute_extension:deferred_delete": "",
|
||||
"compute_extension:v3:os-deferred-delete": "",
|
||||
"compute_extension:disk_config": "",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"server" : {
|
||||
"name" : "new-server-test",
|
||||
"image_ref" : "%(glance_host)s/images/%(image_id)s",
|
||||
"flavor_ref" : "%(host)s/flavors/1",
|
||||
"metadata" : {
|
||||
"My Server Name" : "Apache1"
|
||||
},
|
||||
"personality" : [
|
||||
{
|
||||
"path" : "/etc/banner.txt",
|
||||
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"server": {
|
||||
"admin_password": "%(password)s",
|
||||
"id": "%(id)s",
|
||||
"links": [
|
||||
{
|
||||
"href": "%(host)s/v3/servers/%(uuid)s",
|
||||
"rel": "self"
|
||||
},
|
||||
{
|
||||
"href": "%(host)s/servers/%(uuid)s",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from nova.tests.image import fake
|
||||
from nova.tests.integrated.v3 import test_servers
|
||||
|
||||
|
||||
@@ -40,21 +39,6 @@ class AdminActionsSamplesJsonTest(test_servers.ServersSampleBase):
|
||||
'admin-actions-inject-network-info', {})
|
||||
self.assertEqual(response.status, 202)
|
||||
|
||||
def test_post_backup_server(self):
|
||||
# Get api samples to backup server request.
|
||||
def image_details(self, context, **kwargs):
|
||||
"""This stub is specifically used on the backup action."""
|
||||
# NOTE(maurosr): I've added this simple stub cause backup action
|
||||
# was trapped in infinite loop during fetch image phase since the
|
||||
# fake Image Service always returns the same set of images
|
||||
return []
|
||||
|
||||
self.stubs.Set(fake._FakeImageService, 'detail', image_details)
|
||||
|
||||
response = self._do_post('servers/%s/action' % self.uuid,
|
||||
'admin-actions-backup-server', {})
|
||||
self.assertEqual(response.status, 202)
|
||||
|
||||
def test_post_reset_state(self):
|
||||
# get api samples to server reset state request.
|
||||
response = self._do_post('servers/%s/action' % self.uuid,
|
||||
|
||||
38
nova/tests/integrated/v3/test_create_backup.py
Normal file
38
nova/tests/integrated/v3/test_create_backup.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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 mock
|
||||
|
||||
from nova.tests.image import fake
|
||||
from nova.tests.integrated.v3 import test_servers
|
||||
|
||||
|
||||
class CreateBackupSamplesJsonTest(test_servers.ServersSampleBase):
|
||||
extension_name = "os-create-backup"
|
||||
|
||||
def setUp(self):
|
||||
"""setUp Method for PauseServer api samples extension
|
||||
|
||||
This method creates the server that will be used in each tests
|
||||
"""
|
||||
super(CreateBackupSamplesJsonTest, self).setUp()
|
||||
self.uuid = self._post_server()
|
||||
|
||||
@mock.patch.object(fake._FakeImageService, 'detail', return_value=[])
|
||||
def test_post_backup_server(self, mock_method):
|
||||
# Get api samples to backup server request.
|
||||
response = self._do_post('servers/%s/action' % self.uuid,
|
||||
'create-backup-req', {})
|
||||
self.assertEqual(202, response.status)
|
||||
@@ -69,6 +69,7 @@ nova.api.v3.extensions =
|
||||
config_drive = nova.api.openstack.compute.plugins.v3.config_drive:ConfigDrive
|
||||
console_output = nova.api.openstack.compute.plugins.v3.console_output:ConsoleOutput
|
||||
consoles = nova.api.openstack.compute.plugins.v3.consoles:Consoles
|
||||
create_backup = nova.api.openstack.compute.plugins.v3.create_backup:CreateBackup
|
||||
deferred_delete = nova.api.openstack.compute.plugins.v3.deferred_delete:DeferredDelete
|
||||
evacuate = nova.api.openstack.compute.plugins.v3.evacuate:Evacuate
|
||||
extended_availability_zone = nova.api.openstack.compute.plugins.v3.extended_availability_zone:ExtendedAvailabilityZone
|
||||
|
||||
Reference in New Issue
Block a user