diff options
| author | Brian Lamar <brian.lamar@rackspace.com> | 2011-05-03 18:02:19 +0000 |
|---|---|---|
| committer | Tarmac <> | 2011-05-03 18:02:19 +0000 |
| commit | 95052ace83860ee3bdc5420f3d7f5096f6f4bede (patch) | |
| tree | 16686a1a73c5c110edf01ed2656d909513d78ebd /nova | |
| parent | ce019de9ca633218f031077f6317edb373f1ea88 (diff) | |
| parent | 29e9aa173ea20a7d5cb816ce7478d6c0c2c38b80 (diff) | |
Adding support for server rebuild to v1.0 and v1.1 of the Openstack API
Diffstat (limited to 'nova')
| -rw-r--r-- | nova/api/openstack/servers.py | 83 | ||||
| -rw-r--r-- | nova/api/openstack/views/servers.py | 4 | ||||
| -rw-r--r-- | nova/compute/api.py | 28 | ||||
| -rw-r--r-- | nova/compute/manager.py | 87 | ||||
| -rw-r--r-- | nova/compute/power_state.py | 21 | ||||
| -rw-r--r-- | nova/exception.py | 6 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_servers.py | 168 | ||||
| -rw-r--r-- | nova/tests/test_exception.py | 4 | ||||
| -rw-r--r-- | nova/virt/xenapi/vmops.py | 8 |
9 files changed, 354 insertions, 55 deletions
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8505c3f71..3cf78e32c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -317,10 +317,6 @@ class Controller(common.OpenstackController): return faults.Fault(exc.HTTPBadRequest()) return exc.HTTPAccepted() - def _action_rebuild(self, input_dict, req, id): - LOG.debug(_("Rebuild server action is not implemented")) - return faults.Fault(exc.HTTPNotImplemented()) - def _action_resize(self, input_dict, req, id): """ Resizes a given instance to the flavor size requested """ try: @@ -603,6 +599,28 @@ class ControllerV10(Controller): except exception.TimeoutException: return exc.HTTPRequestTimeout() + def _action_rebuild(self, info, request, instance_id): + context = request.environ['nova.context'] + instance_id = int(instance_id) + + try: + image_id = info["rebuild"]["imageId"] + except (KeyError, TypeError): + msg = _("Could not parse imageId from request.") + LOG.debug(msg) + return faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + try: + self.compute_api.rebuild(context, instance_id, image_id) + except exception.BuildInProgress: + msg = _("Instance %d is currently being rebuilt.") % instance_id + LOG.debug(msg) + return faults.Fault(exc.HTTPConflict(explanation=msg)) + + response = exc.HTTPAccepted() + response.empty_body = True + return response + class ControllerV11(Controller): def _image_id_from_req_data(self, data): @@ -639,6 +657,63 @@ class ControllerV11(Controller): def _limit_items(self, items, req): return common.limited_by_marker(items, req) + def _validate_metadata(self, metadata): + """Ensure that we can work with the metadata given.""" + try: + metadata.iteritems() + except AttributeError as ex: + msg = _("Unable to parse metadata key/value pairs.") + LOG.debug(msg) + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + def _decode_personalities(self, personalities): + """Decode the Base64-encoded personalities.""" + for personality in personalities: + try: + path = personality["path"] + contents = personality["contents"] + except (KeyError, TypeError): + msg = _("Unable to parse personality path/contents.") + LOG.info(msg) + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + try: + personality["contents"] = base64.b64decode(contents) + except TypeError: + msg = _("Personality content could not be Base64 decoded.") + LOG.info(msg) + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + def _action_rebuild(self, info, request, instance_id): + context = request.environ['nova.context'] + instance_id = int(instance_id) + + try: + image_ref = info["rebuild"]["imageRef"] + except (KeyError, TypeError): + msg = _("Could not parse imageRef from request.") + LOG.debug(msg) + return faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + image_id = common.get_id_from_href(image_ref) + personalities = info["rebuild"].get("personality", []) + metadata = info["rebuild"].get("metadata", {}) + + self._validate_metadata(metadata) + self._decode_personalities(personalities) + + try: + self.compute_api.rebuild(context, instance_id, image_id, metadata, + personalities) + except exception.BuildInProgress: + msg = _("Instance %d is currently being rebuilt.") % instance_id + LOG.debug(msg) + return faults.Fault(exc.HTTPConflict(explanation=msg)) + + response = exc.HTTPAccepted() + response.empty_body = True + return response + def _get_server_admin_password(self, server): """ Determine the admin password for a server on creation """ password = server.get('adminPass') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index bdc85f4a1..0be468edc 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -66,7 +66,9 @@ class ViewBuilder(object): power_state.SHUTDOWN: 'SHUTDOWN', power_state.SHUTOFF: 'SHUTOFF', power_state.CRASHED: 'ERROR', - power_state.FAILED: 'ERROR'} + power_state.FAILED: 'ERROR', + power_state.BUILDING: 'BUILD', + } inst_dict = { 'id': int(inst['id']), diff --git a/nova/compute/api.py b/nova/compute/api.py index c85f0f53a..be26d8ca3 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -32,6 +32,7 @@ from nova import rpc from nova import utils from nova import volume from nova.compute import instance_types +from nova.compute import power_state from nova.scheduler import api as scheduler_api from nova.db import base @@ -501,6 +502,33 @@ class API(base.Base): """Reboot the given instance.""" self._cast_compute_message('reboot_instance', context, instance_id) + def rebuild(self, context, instance_id, image_id, metadata=None, + files_to_inject=None): + """Rebuild the given instance with the provided metadata.""" + instance = db.api.instance_get(context, instance_id) + + if instance["state"] == power_state.BUILDING: + msg = _("Instance already building") + raise exception.BuildInProgress(msg) + + metadata = metadata or {} + self._check_metadata_properties_quota(context, metadata) + + files_to_inject = files_to_inject or [] + self._check_injected_file_quota(context, files_to_inject) + + self.db.instance_update(context, instance_id, {"metadata": metadata}) + + rebuild_params = { + "image_id": image_id, + "injected_files": files_to_inject, + } + + self._cast_compute_message('rebuild_instance', + context, + instance_id, + params=rebuild_params) + def revert_resize(self, context, instance_id): """Reverts a resize, deleting the 'new' instance in the process.""" context = context.elevated() diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 61564cb7d..1ff78007b 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -137,17 +137,33 @@ class ComputeManager(manager.SchedulerDependentManager): """Initialization for a standalone compute service.""" self.driver.init_host(host=self.host) - def _update_state(self, context, instance_id): + def _update_state(self, context, instance_id, state=None): """Update the state of an instance from the driver info.""" - # FIXME(ja): include other fields from state? instance_ref = self.db.instance_get(context, instance_id) - try: - info = self.driver.get_info(instance_ref['name']) - state = info['state'] - except exception.NotFound: - state = power_state.FAILED + + if state is None: + try: + info = self.driver.get_info(instance_ref['name']) + except exception.NotFound: + info = None + + if info is not None: + state = info['state'] + else: + state = power_state.FAILED + self.db.instance_set_state(context, instance_id, state) + def _update_launched_at(self, context, instance_id, launched_at=None): + """Update the launched_at parameter of the given instance.""" + data = {'launched_at': launched_at or datetime.datetime.utcnow()} + self.db.instance_update(context, instance_id, data) + + def _update_image_id(self, context, instance_id, image_id): + """Update the image_id for the given instance.""" + data = {'image_id': image_id} + self.db.instance_update(context, instance_id, data) + def get_console_topic(self, context, **kwargs): """Retrieves the console host for a project on this host. @@ -231,24 +247,15 @@ class ComputeManager(manager.SchedulerDependentManager): instance_id) # TODO(vish) check to make sure the availability zone matches - self.db.instance_set_state(context, - instance_id, - power_state.NOSTATE, - 'spawning') + self._update_state(context, instance_id, power_state.BUILDING) try: self.driver.spawn(instance_ref) - now = datetime.datetime.utcnow() - self.db.instance_update(context, - instance_id, - {'launched_at': now}) - except Exception: # pylint: disable=W0702 - LOG.exception(_("Instance '%s' failed to spawn. Is virtualization" - " enabled in the BIOS?"), instance_id, - context=context) - self.db.instance_set_state(context, - instance_id, - power_state.SHUTDOWN) + except Exception as ex: # pylint: disable=W0702 + msg = _("Instance '%(instance_id)s' failed to spawn. Is " + "virtualization enabled in the BIOS? Details: " + "%(ex)s") % locals() + LOG.exception(msg) if not FLAGS.stub_network and FLAGS.auto_assign_floating_ip: public_ip = self.network_api.allocate_floating_ip(context) @@ -262,6 +269,8 @@ class ComputeManager(manager.SchedulerDependentManager): floating_ip, fixed_ip, affect_auto_assigned=True) + + self._update_launched_at(context, instance_id) self._update_state(context, instance_id) @exception.wrap_exception @@ -318,6 +327,33 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception @checks_instance_lock + def rebuild_instance(self, context, instance_id, image_id): + """Destroy and re-make this instance. + + A 'rebuild' effectively purges all existing data from the system and + remakes the VM with given 'metadata' and 'personalities'. + + :param context: `nova.RequestContext` object + :param instance_id: Instance identifier (integer) + :param image_id: Image identifier (integer) + """ + context = context.elevated() + + instance_ref = self.db.instance_get(context, instance_id) + LOG.audit(_("Rebuilding instance %s"), instance_id, context=context) + + self._update_state(context, instance_id, power_state.BUILDING) + + self.driver.destroy(instance_ref) + instance_ref.image_id = image_id + self.driver.spawn(instance_ref) + + self._update_image_id(context, instance_id, image_id) + self._update_launched_at(context, instance_id) + self._update_state(context, instance_id) + + @exception.wrap_exception + @checks_instance_lock def reboot_instance(self, context, instance_id): """Reboot an instance on this host.""" context = context.elevated() @@ -1072,8 +1108,7 @@ class ComputeManager(manager.SchedulerDependentManager): if vm_instance is None: # NOTE(justinsb): We have to be very careful here, because a # concurrent operation could be in progress (e.g. a spawn) - if db_state == power_state.NOSTATE: - # Assume that NOSTATE => spawning + if db_state == power_state.BUILDING: # TODO(justinsb): This does mean that if we crash during a # spawn, the machine will never leave the spawning state, # but this is just the way nova is; this function isn't @@ -1104,9 +1139,7 @@ class ComputeManager(manager.SchedulerDependentManager): if vm_state != db_state: LOG.info(_("DB/VM state mismatch. Changing state from " "'%(db_state)s' to '%(vm_state)s'") % locals()) - self.db.instance_set_state(context, - db_instance['id'], - vm_state) + self._update_state(context, db_instance['id'], vm_state) # NOTE(justinsb): We no longer auto-remove SHUTOFF instances # It's quite hard to get them back when we do. diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py index ef013b2ef..c468fe6b3 100644 --- a/nova/compute/power_state.py +++ b/nova/compute/power_state.py @@ -30,20 +30,23 @@ SHUTOFF = 0x05 CRASHED = 0x06 SUSPENDED = 0x07 FAILED = 0x08 +BUILDING = 0x09 # TODO(justinsb): Power state really needs to be a proper class, # so that we're not locked into the libvirt status codes and can put mapping # logic here rather than spread throughout the code _STATE_MAP = { - NOSTATE: 'pending', - RUNNING: 'running', - BLOCKED: 'blocked', - PAUSED: 'paused', - SHUTDOWN: 'shutdown', - SHUTOFF: 'shutdown', - CRASHED: 'crashed', - SUSPENDED: 'suspended', - FAILED: 'failed to spawn'} + NOSTATE: 'pending', + RUNNING: 'running', + BLOCKED: 'blocked', + PAUSED: 'paused', + SHUTDOWN: 'shutdown', + SHUTOFF: 'shutdown', + CRASHED: 'crashed', + SUSPENDED: 'suspended', + FAILED: 'failed to spawn', + BUILDING: 'building', +} def name(code): diff --git a/nova/exception.py b/nova/exception.py index 8cdd0cb5a..5caad4cf3 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -50,7 +50,7 @@ class Error(Exception): class ApiError(Error): def __init__(self, message='Unknown', code=None): - self.message = message + self.msg = message self.code = code if code: outstr = '%s: %s' % (code, message) @@ -59,6 +59,10 @@ class ApiError(Error): super(ApiError, self).__init__(outstr) +class BuildInProgress(Error): + pass + + class DBError(Error): """Wraps an implementation specific exception.""" def __init__(self, inner_exception): diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index bbce77bf0..5c643fcef 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1058,15 +1058,175 @@ class ServersTest(test.TestCase): req.body = json.dumps(body) res = req.get_response(fakes.wsgi_app()) - def test_server_rebuild(self): - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality={})) + def test_server_rebuild_accepted(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + self.assertEqual(res.body, "") + + def test_server_rebuild_rejected_when_building(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + + state = power_state.BUILDING + new_return_server = return_server_with_power_state(state) + self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 409) + + def test_server_rebuild_bad_entity(self): + body = { + "rebuild": { + }, + } + req = webob.Request.blank('/v1.0/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_rebuild_accepted_minimum_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + + def test_server_rebuild_rejected_when_building_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + state = power_state.BUILDING + new_return_server = return_server_with_power_state(state) + self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 409) + + def test_server_rebuild_accepted_with_metadata_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "metadata": { + "new": "metadata", + }, + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + + def test_server_rebuild_accepted_with_bad_metadata_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "metadata": "stack", + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_rebuild_bad_entity_v11(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_rebuild_bad_personality_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "personality": [{ + "path": "/path/to/file", + "contents": "INVALID b64", + }] + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_rebuild_personality_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "personality": [{ + "path": "/path/to/file", + "contents": base64.b64encode("Test String"), + }] + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) def test_delete_server_instance(self): req = webob.Request.blank('/v1.0/servers/1') diff --git a/nova/tests/test_exception.py b/nova/tests/test_exception.py index 1b0e41d9a..4d3b9cc73 100644 --- a/nova/tests/test_exception.py +++ b/nova/tests/test_exception.py @@ -26,9 +26,9 @@ class ApiErrorTestCase(test.TestCase): err = exception.ApiError('fake error') self.assertEqual(err.__str__(), 'fake error') self.assertEqual(err.code, None) - self.assertEqual(err.message, 'fake error') + self.assertEqual(err.msg, 'fake error') # with 'code' arg err = exception.ApiError('fake error', 'blah code') self.assertEqual(err.__str__(), 'blah code: fake error') self.assertEqual(err.code, 'blah code') - self.assertEqual(err.message, 'fake error') + self.assertEqual(err.msg, 'fake error') diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 808708e8b..30f31517d 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -210,8 +210,6 @@ class VMOps(object): def _wait_for_boot(): try: state = self.get_info(instance_name)['state'] - db.instance_set_state(context.get_admin_context(), - instance['id'], state) if state == power_state.RUNNING: LOG.debug(_('Instance %s: booted'), instance_name) timer.stop() @@ -219,11 +217,7 @@ class VMOps(object): return True except Exception, exc: LOG.warn(exc) - LOG.exception(_('instance %s: failed to boot'), - instance_name) - db.instance_set_state(context.get_admin_context(), - instance['id'], - power_state.SHUTDOWN) + LOG.exception(_('Instance %s: failed to boot'), instance_name) timer.stop() return False |
