Add APIv2 support to make host optional on evacuate

In the event of an unrecoverable hardware failure, support needs to
relocate an instance to another compute so it can be rebuilt.

The changes involved in this patch are:
  [*] Add a new v2 extension to determine that the host argument
      on evacuate is now optional.(Extended_evacuate_find_host)
  [*] Doc regeneration.

DocImpact: The evacuate target host is now optional.
If 'host' field is not sent in the request, the scheduler will
determine the target host.
This will include nova client changes ( on the proper commit ) to support
this new optional parameter.

Implements: blueprint find-host-and-evacuate-instance

Change-Id: Ib34fb3120263b746ad2f8fe89c14137e36a07a53
Co-Authored-By: Juan M. Olle <juan.m.olle@intel.com>
Co-Authored-By: Andres Buraschi <andres.buraschi@intel.com>
Co-Authored-By: Anuj Mathur <anujm@thoughtworks.com>
Co-Authored-By: Navneet Kumar <navneetk@thoughtworks.com>
Co-Authored-By: Claxton Correya <claxton@gmail.com>
This commit is contained in:
Leandro I. Costantino 2014-02-08 12:23:30 -03:00
parent c5b418893e
commit d5a70de479
25 changed files with 379 additions and 9 deletions

View File

@ -272,6 +272,14 @@
"namespace": "http://docs.openstack.org/compute/ext/evacuate/api/v2",
"updated": "2013-01-06T00:00:00Z"
},
{
"alias": "os-extended-evacuate-find-host",
"description": "Enables server evacuation without target host. Scheduler will select\n one to target.\n ",
"links": [],
"name": "ExtendedEvacuateFindHost",
"namespace": "http://docs.openstack.org/compute/ext/extended_evacuate_find_host/api/v2",
"updated": "2014-02-12T00:00:00Z"
},
{
"alias": "os-extended-floating-ips",
"description": "Adds optional fixed_address to the add floating IP command.",

View File

@ -118,6 +118,11 @@
<extension alias="os-evacuate" updated="2013-01-06T00:00:00Z" namespace="http://docs.openstack.org/compute/ext/evacuate/api/v2" name="Evacuate">
<description>Enables server evacuation.</description>
</extension>
<extension alias="os-extended-evacuate-find-host" updated="2014-02-12T00:00:00Z" namespace="http://docs.openstack.org/compute/ext/extended_evacuate_find_host/api/v2" name="ExtendedEvacuateFindHost">
<description>Enables server evacuation without target host. Scheduler will select
one to target.
</description>
</extension>
<extension alias="os-extended-floating-ips" updated="2013-04-19T00:00:00Z" namespace="http://docs.openstack.org/compute/ext/extended_floating_ips/api/v2" name="ExtendedFloatingIps">
<description>Adds optional fixed_address to the add floating IP command.</description>
</extension>

View File

@ -0,0 +1,6 @@
{
"evacuate": {
"adminPass": "MySecretPass",
"onSharedStorage": "False"
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<evacuate xmlns="http://docs.openstack.org/compute/api/v2"
adminPass="MySecretPass"
onSharedStorage="False"/>

View File

@ -0,0 +1,3 @@
{
"adminPass": "MySecretPass"
}

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<adminPass>MySecretPass</adminPass>

View File

@ -0,0 +1,16 @@
{
"server" : {
"name" : "new-server-test",
"imageRef" : "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "http://openstack.example.com/openstack/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
},
"personality" : [
{
"path" : "/etc/banner.txt",
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
}
]
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b" flavorRef="http://openstack.example.com/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "y6hsKno56L6R",
"id": "1c650ba2-6a76-41d1-805c-64f4e312200e",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/1c650ba2-6a76-41d1-805c-64f4e312200e",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/1c650ba2-6a76-41d1-805c-64f4e312200e",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="b7fe468c-c283-4757-ba25-7e313fa55617" adminPass="CiyJLbaJS3j3">
<metadata/>
<atom:link href="http://openstack.example.com/v2/openstack/servers/b7fe468c-c283-4757-ba25-7e313fa55617" rel="self"/>
<atom:link href="http://openstack.example.com/openstack/servers/b7fe468c-c283-4757-ba25-7e313fa55617" rel="bookmark"/>
</server>

View File

@ -28,15 +28,17 @@ authorize = extensions.extension_authorizer('compute', 'evacuate')
class Controller(wsgi.Controller):
def __init__(self, *args, **kwargs):
def __init__(self, ext_mgr, *args, **kwargs):
super(Controller, self).__init__(*args, **kwargs)
self.compute_api = compute.API()
self.host_api = compute.HostAPI()
self.ext_mgr = ext_mgr
@wsgi.action('evacuate')
def _evacuate(self, req, id, body):
"""Permit admins to evacuate a server from a failed host
to a new one.
If host is empty, the scheduler will select one.
"""
context = req.environ["nova.context"]
authorize(context)
@ -45,12 +47,18 @@ class Controller(wsgi.Controller):
raise exc.HTTPBadRequest(_("Malformed request body"))
evacuate_body = body["evacuate"]
host = evacuate_body.get("host")
if (not host and
not self.ext_mgr.is_loaded('os-extended-evacuate-find-host')):
msg = _("host must be specified.")
raise exc.HTTPBadRequest(explanation=msg)
try:
host = evacuate_body["host"]
on_shared_storage = strutils.bool_from_string(
evacuate_body["onSharedStorage"])
except (TypeError, KeyError):
msg = _("host and onSharedStorage must be specified.")
msg = _("onSharedStorage must be specified.")
raise exc.HTTPBadRequest(explanation=msg)
password = None
@ -65,11 +73,12 @@ class Controller(wsgi.Controller):
elif not on_shared_storage:
password = utils.generate_password()
try:
self.host_api.service_get_by_compute_host(context, host)
except exception.NotFound:
msg = _("Compute host %s not found.") % host
raise exc.HTTPNotFound(explanation=msg)
if host is not None:
try:
self.host_api.service_get_by_compute_host(context, host)
except exception.NotFound:
msg = _("Compute host %s not found.") % host
raise exc.HTTPNotFound(explanation=msg)
try:
instance = self.compute_api.get(context, id, want_objects=True)
@ -99,6 +108,6 @@ class Evacuate(extensions.ExtensionDescriptor):
updated = "2013-01-06T00:00:00Z"
def get_controller_extensions(self):
controller = Controller()
controller = Controller(self.ext_mgr)
extension = extensions.ControllerExtension(self, 'servers', controller)
return [extension]

View File

@ -0,0 +1,26 @@
#
# 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 extensions
class Extended_evacuate_find_host(extensions.ExtensionDescriptor):
"""Enables server evacuation without target host. Scheduler will select
one to target.
"""
name = "ExtendedEvacuateFindHost"
alias = "os-extended-evacuate-find-host"
namespace = ("http://docs.openstack.org/compute/ext/"
"extended_evacuate_find_host/api/v2")
updated = "2014-02-12T00:00:00Z"

View File

@ -69,6 +69,11 @@ class EvacuateTest(test.NoDBTestCase):
for _method in self._methods:
self.stubs.Set(compute_api.API, _method, fake_compute_api)
self.flags(
osapi_compute_extension=[
'nova.api.openstack.compute.contrib.select_extensions'],
osapi_compute_ext_list=['Evacuate'])
def _get_admin_context(self, user_id='fake', project_id='fake'):
ctxt = context.get_admin_context()
ctxt.user_id = user_id

View File

@ -0,0 +1,114 @@
# Copyright 2013 OpenStack Foundation
#
# 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 uuid
import mock
import webob
from nova.compute import vm_states
from nova import context
from nova.objects import instance as instance_obj
from nova.openstack.common import jsonutils
from nova import test
from nova.tests.api.openstack import fakes
from nova.tests import fake_instance
class ExtendedEvacuateFindHostTest(test.NoDBTestCase):
def setUp(self):
super(ExtendedEvacuateFindHostTest, self).setUp()
self.flags(
osapi_compute_extension=[
'nova.api.openstack.compute.contrib.select_extensions'],
osapi_compute_ext_list=['Extended_evacuate_find_host',
'Evacuate'])
self.UUID = uuid.uuid4()
def _get_admin_context(self, user_id='fake', project_id='fake'):
ctxt = context.get_admin_context()
ctxt.user_id = user_id
ctxt.project_id = project_id
return ctxt
def _fake_compute_api(*args, **kwargs):
return True
def _fake_compute_api_get(self, context, instance_id, **kwargs):
instance = fake_instance.fake_db_instance(id=1, uuid=uuid,
task_state=None,
host='host1',
vm_state=vm_states.ACTIVE)
instance = instance_obj.Instance._from_db_object(context,
instance_obj.Instance(),
instance)
return instance
def _fake_service_get_by_compute_host(self, context, host):
return {'host_name': host,
'service': 'compute',
'zone': 'nova'
}
@mock.patch('nova.compute.api.HostAPI.service_get_by_compute_host')
@mock.patch('nova.compute.api.API.get')
@mock.patch('nova.compute.api.API.evacuate')
def test_evacuate_instance_with_no_target(self, evacuate_mock,
api_get_mock,
service_get_mock):
service_get_mock.side_effects = self._fake_service_get_by_compute_host
api_get_mock.side_effects = self._fake_compute_api_get
evacuate_mock.side_effects = self._fake_compute_api
ctxt = self._get_admin_context()
app = fakes.wsgi_app(fake_auth_context=ctxt)
req = webob.Request.blank('/v2/fake/servers/%s/action' % self.UUID)
req.method = 'POST'
req.body = jsonutils.dumps({
'evacuate': {
'onSharedStorage': 'False',
'adminPass': 'MyNewPass'
}
})
req.content_type = 'application/json'
res = req.get_response(app)
self.assertEqual(200, res.status_int)
evacuate_mock.assert_called_once_with(mock.ANY, mock.ANY, None,
mock.ANY, mock.ANY)
@mock.patch('nova.compute.api.HostAPI.service_get_by_compute_host')
@mock.patch('nova.compute.api.API.get')
def test_no_target_fails_if_extension_not_loaded(self, api_get_mock,
service_get_mock):
self.flags(
osapi_compute_extension=[
'nova.api.openstack.compute.contrib.select_extensions'],
osapi_compute_ext_list=['Evacuate'])
service_get_mock.side_effects = self._fake_service_get_by_compute_host
api_get_mock.side_effects = self._fake_compute_api_get
ctxt = self._get_admin_context()
app = fakes.wsgi_app(fake_auth_context=ctxt)
req = webob.Request.blank('/v2/fake/servers/%s/action' % self.UUID)
req.method = 'POST'
req.body = jsonutils.dumps({
'evacuate': {
'onSharedStorage': 'False',
'adminPass': 'MyNewPass'
}
})
req.content_type = 'application/json'
res = req.get_response(app)
self.assertEqual(400, res.status_int)

View File

@ -671,6 +671,14 @@
"name": "ServerGroups",
"namespace": "http://docs.openstack.org/compute/ext/servergroups/api/v2",
"updated": "%(isotime)s"
},
{
"alias": "os-extended-evacuate-find-host",
"description": "%(text)s",
"links": [],
"name": "ExtendedEvacuateFindHost",
"namespace": "http://docs.openstack.org/compute/ext/extended_evacuate_find_host/api/v2",
"updated": "%(isotime)s"
}
]
}

View File

@ -251,4 +251,7 @@
<extension alias="os-server-groups" updated="%(isotime)s" namespace="http://docs.openstack.org/compute/ext/servergroups/api/v2" name="ServerGroups">
<description>%(text)s</description>
</extension>
<extension alias="os-extended-evacuate-find-host" updated="%(isotime)s" namespace="http://docs.openstack.org/compute/ext/extended_evacuate_find_host/api/v2" name="ExtendedEvacuateFindHost">
<description>%(text)s</description>
</extension>
</extensions>

View File

@ -0,0 +1,6 @@
{
"evacuate": {
"adminPass": "%(adminPass)s",
"onSharedStorage": "%(onSharedStorage)s"
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<evacuate xmlns="http://docs.openstack.org/compute/api/v2"
adminPass="%(adminPass)s"
onSharedStorage="%(onSharedStorage)s"/>

View File

@ -0,0 +1,3 @@
{
"adminPass": "%(password)s"
}

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<adminPass>%(password)s</adminPass>

View File

@ -0,0 +1,16 @@
{
"server" : {
"name" : "new-server-test",
"imageRef" : "%(host)s/openstack/images/%(image_id)s",
"flavorRef" : "%(host)s/openstack/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
},
"personality" : [
{
"path" : "/etc/banner.txt",
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
}
]
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="%(host)s/openstack/images/%(image_id)s" flavorRef="%(host)s/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "%(password)s",
"id": "%(id)s",
"links": [
{
"href": "%(host)s/v2/openstack/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(host)s/openstack/servers/%(uuid)s",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="%(id)s" adminPass="%(password)s">
<metadata/>
<atom:link href="%(host)s/v2/openstack/servers/%(uuid)s" rel="self"/>
<atom:link href="%(host)s/openstack/servers/%(uuid)s" rel="bookmark"/>
</server>

View File

@ -3238,6 +3238,53 @@ class EvacuateXmlTest(EvacuateJsonTest):
ctype = 'xml'
class EvacuateFindHostSampleJsonTest(ServersSampleBase):
extends_name = ("nova.api.openstack.compute.contrib"
".evacuate.Evacuate")
extension_name = ("nova.api.openstack.compute.contrib"
".extended_evacuate_find_host.Extended_evacuate_find_host")
@mock.patch('nova.compute.manager.ComputeManager._check_instance_exists')
@mock.patch('nova.compute.api.HostAPI.service_get_by_compute_host')
@mock.patch('nova.conductor.manager.ComputeTaskManager.rebuild_instance')
def test_server_evacuate(self, rebuild_mock, service_get_mock,
check_instance_mock):
self.uuid = self._post_server()
req_subs = {
"adminPass": "MySecretPass",
"onSharedStorage": 'False'
}
check_instance_mock.return_value = False
def fake_service_get_by_compute_host(self, context, host):
return {
'host_name': host,
'service': 'compute',
'zone': 'nova'
}
service_get_mock.side_effect = fake_service_get_by_compute_host
with mock.patch.object(service_group_api.API, 'service_is_up',
return_value=False):
response = self._do_post('servers/%s/action' % self.uuid,
'server-evacuate-find-host-req', req_subs)
subs = self._get_regexes()
self._verify_response('server-evacuate-find-host-resp', subs,
response, 200)
rebuild_mock.assert_called_once_with(mock.ANY, instance=mock.ANY,
orig_image_ref=mock.ANY, image_ref=mock.ANY,
injected_files=mock.ANY, new_pass="MySecretPass",
orig_sys_metadata=mock.ANY, bdms=mock.ANY, recreate=mock.ANY,
on_shared_storage=False, preserve_ephemeral=mock.ANY,
host=None)
class EvacuateFindHostSampleXmlTests(EvacuateFindHostSampleJsonTest):
ctype = "xml"
class FloatingIpDNSJsonTest(ApiSampleTestBaseV2):
extension_name = ("nova.api.openstack.compute.contrib.floating_ip_dns."
"Floating_ip_dns")