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

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. 

14 

15 

16from oslo_log import log as logging 

17from oslo_utils import strutils 

18from webob import exc 

19 

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 

33 

34CONF = nova.conf.CONF 

35 

36LOG = logging.getLogger(__name__) 

37 

38MIN_VER_NOVA_COMPUTE_EVACUATE_STOPPED = 62 

39 

40 

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() 

46 

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"]) 

52 

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) 

61 

62 password = evacuate_body['adminPass'] 

63 elif not on_shared_storage: 

64 password = utils.generate_password() 

65 

66 return password 

67 

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() 

73 

74 return password 

75 

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']) 

101 

102 context.can(evac_policies.BASE_POLICY_NAME, 

103 target={'user_id': instance.user_id, 

104 'project_id': instance.project_id}) 

105 

106 evacuate_body = body["evacuate"] 

107 host = evacuate_body.get("host") 

108 force = None 

109 

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 

122 

123 on_shared_storage = self._get_on_shared_storage(req, evacuate_body) 

124 

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) 

136 

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) 

144 

145 if instance.host == host: 

146 msg = _("The target host can't be the same one.") 

147 raise exc.HTTPBadRequest(explanation=msg) 

148 

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()) 

168 

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