Coverage for nova/api/openstack/compute/hosts.py: 97%

160 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-17 15:08 +0000

1# Copyright (c) 2011 OpenStack Foundation 

2# All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16"""The hosts admin extension.""" 

17 

18from oslo_log import log as logging 

19import webob.exc 

20 

21from nova.api.openstack import common 

22from nova.api.openstack.compute.schemas import hosts 

23from nova.api.openstack import wsgi 

24from nova.api import validation 

25from nova.compute import api as compute 

26from nova import context as nova_context 

27from nova import exception 

28from nova import objects 

29from nova.policies import hosts as hosts_policies 

30 

31LOG = logging.getLogger(__name__) 

32 

33 

34class HostController(wsgi.Controller): 

35 """The Hosts API controller for the OpenStack API.""" 

36 

37 def __init__(self): 

38 super(HostController, self).__init__() 

39 self.api = compute.HostAPI() 

40 

41 @wsgi.Controller.api_version("2.1", "2.42") 

42 @validation.query_schema(hosts.index_query) 

43 @wsgi.expected_errors(()) 

44 def index(self, req): 

45 """Returns a dict in the format 

46 

47 | {'hosts': [{'host_name': 'some.host.name', 

48 | 'service': 'cells', 

49 | 'zone': 'internal'}, 

50 | {'host_name': 'some.other.host.name', 

51 | 'service': 'cells', 

52 | 'zone': 'internal'}, 

53 | {'host_name': 'some.celly.host.name', 

54 | 'service': 'cells', 

55 | 'zone': 'internal'}, 

56 | {'host_name': 'compute1.host.com', 

57 | 'service': 'compute', 

58 | 'zone': 'nova'}, 

59 | {'host_name': 'compute2.host.com', 

60 | 'service': 'compute', 

61 | 'zone': 'nova'}, 

62 | {'host_name': 'sched1.host.com', 

63 | 'service': 'scheduler', 

64 | 'zone': 'internal'}, 

65 | {'host_name': 'sched2.host.com', 

66 | 'service': 'scheduler', 

67 | 'zone': 'internal'}, 

68 | {'host_name': 'vol1.host.com', 

69 | 'service': 'volume', 

70 | 'zone': 'internal'}]} 

71 

72 """ 

73 context = req.environ['nova.context'] 

74 context.can(hosts_policies.POLICY_NAME % 'list', 

75 target={}) 

76 filters = {'disabled': False} 

77 zone = req.GET.get('zone', None) 

78 if zone: 

79 filters['availability_zone'] = zone 

80 services = self.api.service_get_all(context, filters=filters, 

81 set_zones=True, all_cells=True) 

82 hosts = [] 

83 api_services = ('nova-osapi_compute', 'nova-metadata') 

84 for service in services: 

85 if service.binary not in api_services: 85 ↛ 84line 85 didn't jump to line 84 because the condition on line 85 was always true

86 hosts.append({'host_name': service['host'], 

87 'service': service['topic'], 

88 'zone': service['availability_zone']}) 

89 return {'hosts': hosts} 

90 

91 @wsgi.Controller.api_version("2.1", "2.42") 

92 @wsgi.expected_errors((400, 404, 501)) 

93 @validation.schema(hosts.update) 

94 def update(self, req, id, body): 

95 """Return booleanized version of body dict. 

96 

97 :param Request req: The request object (containing 'nova-context' 

98 env var). 

99 :param str id: The host name. 

100 :param dict body: example format {'host': {'status': 'enable', 

101 'maintenance_mode': 'enable'}} 

102 :return: Same dict as body but 'enable' strings for 'status' and 

103 'maintenance_mode' are converted into True, else False. 

104 :rtype: dict 

105 """ 

106 def read_enabled(orig_val): 

107 # Convert enable/disable str to a bool. 

108 val = orig_val.strip().lower() 

109 return val == "enable" 

110 

111 context = req.environ['nova.context'] 

112 context.can(hosts_policies.POLICY_NAME % 'update', 

113 target={}) 

114 # See what the user wants to 'update' 

115 status = body.get('status') 

116 maint_mode = body.get('maintenance_mode') 

117 if status is not None: 

118 status = read_enabled(status) 

119 if maint_mode is not None: 

120 maint_mode = read_enabled(maint_mode) 

121 # Make the calls and merge the results 

122 result = {'host': id} 

123 if status is not None: 

124 result['status'] = self._set_enabled_status(context, id, status) 

125 if maint_mode is not None: 

126 result['maintenance_mode'] = self._set_host_maintenance(context, 

127 id, 

128 maint_mode) 

129 return result 

130 

131 def _set_host_maintenance(self, context, host_name, mode=True): 

132 """Start/Stop host maintenance window. On start, it triggers 

133 guest VMs evacuation. 

134 """ 

135 LOG.info("Putting host %(host_name)s in maintenance mode %(mode)s.", 

136 {'host_name': host_name, 'mode': mode}) 

137 try: 

138 result = self.api.set_host_maintenance(context, host_name, mode) 

139 except NotImplementedError: 

140 common.raise_feature_not_supported() 

141 except (exception.HostNotFound, exception.HostMappingNotFound) as e: 

142 raise webob.exc.HTTPNotFound(explanation=e.format_message()) 

143 except exception.ComputeServiceUnavailable as e: 

144 raise webob.exc.HTTPBadRequest(explanation=e.format_message()) 

145 if result not in ("on_maintenance", "off_maintenance"): 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true

146 raise webob.exc.HTTPBadRequest(explanation=result) 

147 return result 

148 

149 def _set_enabled_status(self, context, host_name, enabled): 

150 """Sets the specified host's ability to accept new instances. 

151 :param enabled: a boolean - if False no new VMs will be able to start 

152 on the host. 

153 """ 

154 if enabled: 

155 LOG.info("Enabling host %s.", host_name) 

156 else: 

157 LOG.info("Disabling host %s.", host_name) 

158 try: 

159 result = self.api.set_host_enabled(context, host_name, enabled) 

160 except NotImplementedError: 

161 common.raise_feature_not_supported() 

162 except (exception.HostNotFound, exception.HostMappingNotFound) as e: 

163 raise webob.exc.HTTPNotFound(explanation=e.format_message()) 

164 except exception.ComputeServiceUnavailable as e: 

165 raise webob.exc.HTTPBadRequest(explanation=e.format_message()) 

166 if result not in ("enabled", "disabled"): 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true

167 raise webob.exc.HTTPBadRequest(explanation=result) 

168 return result 

169 

170 def _host_power_action(self, req, host_name, action): 

171 """Reboots, shuts down or powers up the host.""" 

172 context = req.environ['nova.context'] 

173 try: 

174 result = self.api.host_power_action(context, host_name, action) 

175 except NotImplementedError: 

176 common.raise_feature_not_supported() 

177 except (exception.HostNotFound, exception.HostMappingNotFound) as e: 

178 raise webob.exc.HTTPNotFound(explanation=e.format_message()) 

179 except exception.ComputeServiceUnavailable as e: 

180 raise webob.exc.HTTPBadRequest(explanation=e.format_message()) 

181 return {"host": host_name, "power_action": result} 

182 

183 @wsgi.Controller.api_version("2.1", "2.42") 

184 @wsgi.expected_errors((400, 404, 501)) 

185 @validation.query_schema(hosts.startup_query) 

186 def startup(self, req, id): 

187 context = req.environ['nova.context'] 

188 context.can(hosts_policies.POLICY_NAME % 'start', 

189 target={}) 

190 return self._host_power_action(req, host_name=id, action="startup") 

191 

192 @wsgi.Controller.api_version("2.1", "2.42") 

193 @wsgi.expected_errors((400, 404, 501)) 

194 @validation.query_schema(hosts.shutdown_query) 

195 def shutdown(self, req, id): 

196 context = req.environ['nova.context'] 

197 context.can(hosts_policies.POLICY_NAME % 'shutdown', 

198 target={}) 

199 return self._host_power_action(req, host_name=id, action="shutdown") 

200 

201 @wsgi.Controller.api_version("2.1", "2.42") 

202 @wsgi.expected_errors((400, 404, 501)) 

203 @validation.query_schema(hosts.reboot_query) 

204 def reboot(self, req, id): 

205 context = req.environ['nova.context'] 

206 context.can(hosts_policies.POLICY_NAME % 'reboot', 

207 target={}) 

208 return self._host_power_action(req, host_name=id, action="reboot") 

209 

210 @staticmethod 

211 def _get_total_resources(host_name, compute_node): 

212 return {'resource': {'host': host_name, 

213 'project': '(total)', 

214 'cpu': compute_node.vcpus, 

215 'memory_mb': compute_node.memory_mb, 

216 'disk_gb': compute_node.local_gb}} 

217 

218 @staticmethod 

219 def _get_used_now_resources(host_name, compute_node): 

220 return {'resource': {'host': host_name, 

221 'project': '(used_now)', 

222 'cpu': compute_node.vcpus_used, 

223 'memory_mb': compute_node.memory_mb_used, 

224 'disk_gb': compute_node.local_gb_used}} 

225 

226 @staticmethod 

227 def _get_resource_totals_from_instances(host_name, instances): 

228 cpu_sum = 0 

229 mem_sum = 0 

230 hdd_sum = 0 

231 for instance in instances: 

232 cpu_sum += instance['vcpus'] 

233 mem_sum += instance['memory_mb'] 

234 hdd_sum += instance['root_gb'] + instance['ephemeral_gb'] 

235 

236 return {'resource': {'host': host_name, 

237 'project': '(used_max)', 

238 'cpu': cpu_sum, 

239 'memory_mb': mem_sum, 

240 'disk_gb': hdd_sum}} 

241 

242 @staticmethod 

243 def _get_resources_by_project(host_name, instances): 

244 # Getting usage resource per project 

245 project_map = {} 

246 for instance in instances: 

247 resource = project_map.setdefault(instance['project_id'], 

248 {'host': host_name, 

249 'project': instance['project_id'], 

250 'cpu': 0, 

251 'memory_mb': 0, 

252 'disk_gb': 0}) 

253 resource['cpu'] += instance['vcpus'] 

254 resource['memory_mb'] += instance['memory_mb'] 

255 resource['disk_gb'] += (instance['root_gb'] + 

256 instance['ephemeral_gb']) 

257 return project_map 

258 

259 @wsgi.Controller.api_version("2.1", "2.42") 

260 @wsgi.expected_errors(404) 

261 @validation.query_schema(hosts.show_query) 

262 def show(self, req, id): 

263 """Shows the physical/usage resource given by hosts. 

264 

265 :param id: hostname 

266 :returns: expected to use HostShowTemplate. 

267 ex.:: 

268 

269 {'host': {'resource':D},..} 

270 D: {'host': 'hostname','project': 'admin', 

271 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30} 

272 """ 

273 context = req.environ['nova.context'] 

274 context.can(hosts_policies.POLICY_NAME % 'show', 

275 target={}) 

276 host_name = id 

277 try: 

278 mapping = objects.HostMapping.get_by_host(context, host_name) 

279 nova_context.set_target_cell(context, mapping.cell_mapping) 

280 compute_node = ( 

281 objects.ComputeNode.get_first_node_by_host_for_old_compat( 

282 context, host_name)) 

283 instances = self.api.instance_get_all_by_host(context, host_name) 

284 except (exception.ComputeHostNotFound, 

285 exception.HostMappingNotFound) as e: 

286 raise webob.exc.HTTPNotFound(explanation=e.format_message()) 

287 resources = [self._get_total_resources(host_name, compute_node)] 

288 resources.append(self._get_used_now_resources(host_name, 

289 compute_node)) 

290 resources.append(self._get_resource_totals_from_instances(host_name, 

291 instances)) 

292 by_proj_resources = self._get_resources_by_project(host_name, 

293 instances) 

294 for resource in by_proj_resources.values(): 

295 resources.append({'resource': resource}) 

296 return {'host': resources}