Coverage for nova/api/openstack/compute/evacuate.py: 98%
97 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright 2013 OpenStack Foundation
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
16from oslo_log import log as logging
17from oslo_utils import strutils
18from webob import exc
20from nova.api.openstack import api_version_request
21from nova.api.openstack import common
22from nova.api.openstack.compute.schemas import evacuate
23from nova.api.openstack import wsgi
24from nova.api import validation
25from nova.compute import api as compute
26from nova.compute import vm_states
27import nova.conf
28from nova import exception
29from nova.i18n import _
30from nova import objects
31from nova.policies import evacuate as evac_policies
32from nova import utils
34CONF = nova.conf.CONF
36LOG = logging.getLogger(__name__)
38MIN_VER_NOVA_COMPUTE_EVACUATE_STOPPED = 62
41class EvacuateController(wsgi.Controller):
42 def __init__(self):
43 super(EvacuateController, self).__init__()
44 self.compute_api = compute.API()
45 self.host_api = compute.HostAPI()
47 def _get_on_shared_storage(self, req, evacuate_body):
48 if api_version_request.is_supported(req, min_version='2.14'):
49 return None
50 else:
51 return strutils.bool_from_string(evacuate_body["onSharedStorage"])
53 def _get_password(self, req, evacuate_body, on_shared_storage):
54 password = None
55 if 'adminPass' in evacuate_body:
56 # check that if requested to evacuate server on shared storage
57 # password not specified
58 if on_shared_storage:
59 msg = _("admin password can't be changed on existing disk")
60 raise exc.HTTPBadRequest(explanation=msg)
62 password = evacuate_body['adminPass']
63 elif not on_shared_storage:
64 password = utils.generate_password()
66 return password
68 def _get_password_v214(self, req, evacuate_body):
69 if 'adminPass' in evacuate_body:
70 password = evacuate_body['adminPass']
71 else:
72 password = utils.generate_password()
74 return password
76 # TODO(eliqiao): Should be responding here with 202 Accept
77 # because evacuate is an async call, but keep to 200 for
78 # backwards compatibility reasons.
79 @wsgi.expected_errors((400, 403, 404, 409))
80 @wsgi.action('evacuate')
81 @validation.schema(evacuate.evacuate, "2.0", "2.13")
82 @validation.schema(evacuate.evacuate_v214, "2.14", "2.28")
83 @validation.schema(evacuate.evacuate_v229, "2.29", "2.67")
84 @validation.schema(evacuate.evacuate_v268, "2.68", "2.94")
85 @validation.schema(evacuate.evacuate_v295, "2.95")
86 @validation.response_body_schema(
87 evacuate.evacuate_response, "2.0", "2.13"
88 )
89 @validation.response_body_schema(evacuate.evacuate_response_v214, "2.14")
90 def _evacuate(self, req, id, body):
91 """Permit admins to evacuate a server from a failed host
92 to a new one.
93 """
94 context = req.environ["nova.context"]
95 instance = common.get_instance(self.compute_api, context, id,
96 expected_attrs=['trusted_certs',
97 'pci_requests',
98 'pci_devices',
99 'resources',
100 'migration_context'])
102 context.can(evac_policies.BASE_POLICY_NAME,
103 target={'user_id': instance.user_id,
104 'project_id': instance.project_id})
106 evacuate_body = body["evacuate"]
107 host = evacuate_body.get("host")
108 force = None
110 target_state = None
111 if api_version_request.is_supported(req, min_version='2.95'):
112 min_ver = objects.service.get_minimum_version_all_cells(
113 context, ['nova-compute'])
114 if min_ver < MIN_VER_NOVA_COMPUTE_EVACUATE_STOPPED:
115 raise exception.NotSupportedComputeForEvacuateV295(
116 {'currently': min_ver,
117 'expected': MIN_VER_NOVA_COMPUTE_EVACUATE_STOPPED})
118 # Starts to 2.95 any evacuated instances will be stopped at
119 # destination. Previously an active or stopped instance would have
120 # kept its state.
121 target_state = vm_states.STOPPED
123 on_shared_storage = self._get_on_shared_storage(req, evacuate_body)
125 if api_version_request.is_supported(req, min_version='2.29'):
126 force = body["evacuate"].get("force", False)
127 force = strutils.bool_from_string(force, strict=True)
128 if force is True and not host:
129 message = _("Can't force to a non-provided destination")
130 raise exc.HTTPBadRequest(explanation=message)
131 if api_version_request.is_supported(req, min_version='2.14'):
132 password = self._get_password_v214(req, evacuate_body)
133 else:
134 password = self._get_password(req, evacuate_body,
135 on_shared_storage)
137 if host is not None:
138 try:
139 self.host_api.service_get_by_compute_host(context, host)
140 except (exception.ComputeHostNotFound,
141 exception.HostMappingNotFound):
142 msg = _("Compute host %s not found.") % host
143 raise exc.HTTPNotFound(explanation=msg)
145 if instance.host == host:
146 msg = _("The target host can't be the same one.")
147 raise exc.HTTPBadRequest(explanation=msg)
149 try:
150 self.compute_api.evacuate(context, instance, host,
151 on_shared_storage, password, force,
152 target_state)
153 except exception.InstanceInvalidState as state_error:
154 common.raise_http_conflict_for_instance_invalid_state(state_error,
155 'evacuate', id)
156 except (
157 exception.ComputeServiceInUse,
158 exception.ForbiddenPortsWithAccelerator,
159 exception.ExtendedResourceRequestOldCompute,
160 ) as e:
161 raise exc.HTTPBadRequest(explanation=e.format_message())
162 except exception.UnsupportedRPCVersion as e:
163 raise exc.HTTPConflict(explanation=e.format_message())
164 except (
165 exception.ForbiddenSharesNotSupported,
166 exception.ForbiddenWithShare) as e:
167 raise exc.HTTPConflict(explanation=e.format_message())
169 if (not api_version_request.is_supported(req, min_version='2.14') and
170 CONF.api.enable_instance_password):
171 return {'adminPass': password}
172 else:
173 return None