Extension for rebuild-for-ha

Adds an extension for blueprint rebuild-for-ha

Evacuation of VM from a failed host for HA.
New api supports recreate/rebuild modes. With shared storage,
VM is recreated on the target from disk.
Otherwise, rebuild from original image is performed.

DocImpact

Change-Id: Id7e3e68dbaaf74e9314440d98195a92454078fd9
Co-authored-by: Oshrit Feder <oshritf@il.ibm.com>
This commit is contained in:
Kravchenko Pavel 2013-02-07 14:38:02 +02:00
parent fe16fded3d
commit 01a25e9890
26 changed files with 460 additions and 0 deletions

View File

@ -184,6 +184,14 @@
"namespace": "http://docs.openstack.org/compute/ext/deferred-delete/api/v1.1",
"updated": "2011-09-01T00:00:00+00:00"
},
{
"alias": "os-evacuate",
"description": "Enables server evacuation",
"links": [],
"name": "Evacuate",
"namespace": "http://docs.openstack.org/compute/ext/evacuate/api/v2",
"updated": "2012-12-05T00:00:00+00:00"
},
{
"alias": "os-fixed-ips",
"description": "Fixed IPs support.",

View File

@ -84,6 +84,9 @@
<extension alias="os-deferred-delete" updated="2011-09-01T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/deferred-delete/api/v1.1" name="DeferredDelete">
<description>Instance deferred delete.</description>
</extension>
<extension alias="os-evacuate" updated="2012-12-05T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/evacuate/api/v2" name="Evacuate">
<description>Enables server evacuation</description>
</extension>
<extension alias="os-fixed-ips" updated="2012-10-18T13:25:27-06:00" namespace="http://docs.openstack.org/compute/ext/fixed_ips/api/v2" name="FixedIPs">
<description>Fixed IPs support.</description>
</extension>

View File

@ -0,0 +1,7 @@
{
"evacuate": {
"host": "TargetHost",
"adminPass": "MySecretPass",
"onSharedStorage": "True"
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<rescue xmlns="http://docs.openstack.org/compute/api/v2"
host="TargetHost"
adminPass="MySecretPass"
onSharedStorage="True"/>

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": "xjDVAYHmc34s",
"id": "784f5005-bec9-4c22-8c42-5a7dcba88d82",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/784f5005-bec9-4c22-8c42-5a7dcba88d82",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/784f5005-bec9-4c22-8c42-5a7dcba88d82",
"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="70b4263f-bae1-4ac4-a1c5-e5bb193767bc" adminPass="ttv8YPD3tRPm">
<metadata/>
<atom:link href="http://openstack.example.com/v2/openstack/servers/70b4263f-bae1-4ac4-a1c5-e5bb193767bc" rel="self"/>
<atom:link href="http://openstack.example.com/openstack/servers/70b4263f-bae1-4ac4-a1c5-e5bb193767bc" rel="bookmark"/>
</server>

View File

@ -40,6 +40,7 @@
"compute_extension:createserverext": "",
"compute_extension:deferred_delete": "",
"compute_extension:disk_config": "",
"compute_extension:evacuate": "rule:admin_api",
"compute_extension:extended_server_attributes": "rule:admin_api",
"compute_extension:extended_status": "",
"compute_extension:extended_availability_zone": "",

View File

@ -0,0 +1,98 @@
# Copyright 2013 OpenStack, LLC.
#
# 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 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 import log as logging
from nova import utils
LOG = logging.getLogger(__name__)
authorize = extensions.extension_authorizer('compute', 'evacuate')
class Controller(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(Controller, self).__init__(*args, **kwargs)
self.compute_api = compute.API()
@wsgi.action('evacuate')
def _evacuate(self, req, id, body):
"""
Permit admins to evacuate a server from a failed host
to a new one.
"""
context = req.environ["nova.context"]
if not context.is_admin:
msg = _("Instance evacuate is admin only functionality")
raise exc.HTTPForbidden(explanation=msg)
authorize(context)
try:
if len(body) != 1:
raise exc.HTTPBadRequest(_("Malformed request body"))
evacuate_body = body["evacuate"]
host = evacuate_body["host"]
on_shared_storage = utils.bool_from_str(
evacuate_body["onSharedStorage"])
if 'adminPass' in evacuate_body:
# check that if requested to evacuate server on shared storage
# password not specified
if on_shared_storage:
msg = _("admin password can't be changed on existing disk")
raise exc.HTTPBadRequest(explanation=msg)
password = evacuate_body['adminPass']
elif not on_shared_storage:
password = utils.generate_password()
except (TypeError, KeyError):
msg = _("host and onSharedStorage must be specified.")
raise exc.HTTPBadRequest(explanation=msg)
try:
instance = self.compute_api.get(context, id)
self.compute_api.evacuate(context, instance, host,
on_shared_storage, password)
except exception.InstanceInvalidState as state_error:
common.raise_http_conflict_for_instance_invalid_state(state_error,
'evacuate')
except Exception as e:
msg = _("Error in evacuate, %s") % e
LOG.exception(msg, instance=instance)
raise exc.HTTPBadRequest(explanation=msg)
if password:
return {'adminPass': password}
class Evacuate(extensions.ExtensionDescriptor):
"""Enables server evacuation."""
name = "Evacuate"
alias = "os-evacuate"
namespace = "http://docs.openstack.org/compute/ext/evacuate/api/v2"
updated = "2013-01-06T00:00:00+00:00"
def get_controller_extensions(self):
controller = Controller()
extension = extensions.ControllerExtension(self, 'servers', controller)
return [extension]

View File

@ -0,0 +1,156 @@
# Copyright 2013 OpenStack LLC.
#
# 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 webob
from nova.compute import api as compute_api
from nova.compute import vm_states
from nova import context
from nova.openstack.common import cfg
from nova.openstack.common import jsonutils
from nova import test
from nova.tests.api.openstack import fakes
CONF = cfg.CONF
CONF.import_opt('password_length', 'nova.utils')
def fake_compute_api(*args, **kwargs):
return True
def fake_compute_api_get(self, context, instance_id):
return {
'id': 1,
'uuid': instance_id,
'vm_state': vm_states.ACTIVE,
'task_state': None, 'host': 'host1'
}
class EvacuateTest(test.TestCase):
_methods = ('resize', 'evacuate')
def setUp(self):
super(EvacuateTest, self).setUp()
self.stubs.Set(compute_api.API, 'get', fake_compute_api_get)
self.UUID = uuid.uuid4()
for _method in self._methods:
self.stubs.Set(compute_api.API, _method, fake_compute_api)
def test_evacuate_instance_with_no_target(self):
ctxt = context.get_admin_context()
ctxt.user_id = 'fake'
ctxt.project_id = 'fake'
ctxt.is_admin = True
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(res.status_int, 400)
def test_evacuate_instance_with_target(self):
ctxt = context.get_admin_context()
ctxt.user_id = 'fake'
ctxt.project_id = 'fake'
ctxt.is_admin = True
app = fakes.wsgi_app(fake_auth_context=ctxt)
uuid = self.UUID
req = webob.Request.blank('/v2/fake/servers/%s/action' % uuid)
req.method = 'POST'
req.body = jsonutils.dumps({
'evacuate': {
'host': 'my_host',
'onSharedStorage': 'false',
'adminPass': 'MyNewPass'
}
})
req.content_type = 'application/json'
def fake_update(inst, context, instance,
task_state, expected_task_state):
return None
self.stubs.Set(compute_api.API, 'update', fake_update)
resp = req.get_response(app)
self.assertEqual(resp.status_int, 200)
resp_json = jsonutils.loads(resp.body)
self.assertEqual("MyNewPass", resp_json['adminPass'])
def test_evacuate_shared_and_pass(self):
ctxt = context.get_admin_context()
ctxt.user_id = 'fake'
ctxt.project_id = 'fake'
ctxt.is_admin = True
app = fakes.wsgi_app(fake_auth_context=ctxt)
uuid = self.UUID
req = webob.Request.blank('/v2/fake/servers/%s/action' % uuid)
req.method = 'POST'
req.body = jsonutils.dumps({
'evacuate': {
'host': 'my_host',
'onSharedStorage': 'True',
'adminPass': 'MyNewPass'
}
})
req.content_type = 'application/json'
def fake_update(inst, context, instance,
task_state, expected_task_state):
return None
self.stubs.Set(compute_api.API, 'update', fake_update)
res = req.get_response(app)
self.assertEqual(res.status_int, 400)
def test_evacuate_not_shared_pass_generated(self):
ctxt = context.get_admin_context()
ctxt.user_id = 'fake'
ctxt.project_id = 'fake'
ctxt.is_admin = True
app = fakes.wsgi_app(fake_auth_context=ctxt)
uuid = self.UUID
req = webob.Request.blank('/v2/fake/servers/%s/action' % uuid)
req.method = 'POST'
req.body = jsonutils.dumps({
'evacuate': {
'host': 'my_host',
'onSharedStorage': 'False',
}
})
req.content_type = 'application/json'
def fake_update(inst, context, instance,
task_state, expected_task_state):
return None
self.stubs.Set(compute_api.API, 'update', fake_update)
resp = req.get_response(app)
self.assertEqual(resp.status_int, 200)
resp_json = jsonutils.loads(resp.body)
self.assertEqual(CONF.password_length, len(resp_json['adminPass']))

View File

@ -167,6 +167,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"DeferredDelete",
"DiskConfig",
"ExtendedAvailabilityZone",
"Evacuate",
"ExtendedStatus",
"ExtendedServerAttributes",
"FixedIPs",

View File

@ -117,6 +117,7 @@ policy_data = """
"compute_extension:createserverext": "",
"compute_extension:deferred_delete": "",
"compute_extension:disk_config": "",
"compute_extension:evacuate": "",
"compute_extension:extended_server_attributes": "",
"compute_extension:extended_status": "",
"compute_extension:extended_availability_zone": "",

View File

@ -184,6 +184,14 @@
"namespace": "http://docs.openstack.org/compute/ext/deferred-delete/api/v1.1",
"updated": "%(timestamp)s"
},
{
"alias": "os-evacuate",
"description": "%(text)s",
"links": [],
"name": "Evacuate",
"namespace": "http://docs.openstack.org/compute/ext/evacuate/api/v2",
"updated": "%(timestamp)s"
},
{
"alias": "os-fixed-ips",
"description": "Fixed IPs support.",

View File

@ -69,6 +69,9 @@
<extension alias="os-deferred-delete" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/deferred-delete/api/v1.1" name="DeferredDelete">
<description>%(text)s</description>
</extension>
<extension alias="os-evacuate" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/evacuate/api/v2" name="Evacuate">
<description>%(text)s</description>
</extension>
<extension alias="os-fixed-ips" name="FixedIPs" namespace="http://docs.openstack.org/compute/ext/fixed_ips/api/v2" updated="2012-10-18T13:25:27-06:00">
<description>Fixed IPs support.</description>
</extension>

View File

@ -0,0 +1,7 @@
{
"evacuate": {
"host": "%(host)s",
"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"
host="%(host)s"
adminPass="%(adminPass)s"
onSharedStorage="%(onSharedStorage)s"/>

View File

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

View File

@ -0,0 +1 @@
<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

@ -42,6 +42,7 @@ from nova.openstack.common.log import logging
from nova.openstack.common import timeutils
import nova.quota
from nova.scheduler import driver
from nova.servicegroup import api as service_group_api
from nova import test
from nova.tests.api.openstack.compute.contrib import test_fping
from nova.tests.api.openstack.compute.contrib import test_networks
@ -2881,3 +2882,36 @@ class ExtendedAvailabilityZoneJsonTests(ServersSampleBase):
class ExtendedAvailabilityZoneXmlTests(ExtendedAvailabilityZoneJsonTests):
ctype = 'xml'
class EvacuateJsonTest(ServersSampleBase):
extension_name = ("nova.api.openstack.compute.contrib"
".evacuate.Evacuate")
def test_server_evacuate(self):
uuid = self._post_server()
req_subs = {
'host': 'TargetHost',
"adminPass": "MySecretPass",
"onSharedStorage": 'False'
}
def fake_service_is_up(self, service):
"""Simulate validation of instance host is down."""
return False
self.stubs.Set(service_group_api.API, 'service_is_up',
fake_service_is_up)
response = self._do_post('servers/%s/action' % uuid,
'server-evacuate-req', req_subs)
self.assertEqual(response.status, 200)
subs = self._get_regexes()
return self._verify_response('server-evacuate-resp', subs,
response)
class EvacuateXmlTest(EvacuateJsonTest):
ctype = 'xml'