Coverage for nova/api/openstack/compute/hosts.py: 97%
160 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 (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.
16"""The hosts admin extension."""
18from oslo_log import log as logging
19import webob.exc
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
31LOG = logging.getLogger(__name__)
34class HostController(wsgi.Controller):
35 """The Hosts API controller for the OpenStack API."""
37 def __init__(self):
38 super(HostController, self).__init__()
39 self.api = compute.HostAPI()
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
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'}]}
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}
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.
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"
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
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
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
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}
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")
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")
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")
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}}
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}}
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']
236 return {'resource': {'host': host_name,
237 'project': '(used_max)',
238 'cpu': cpu_sum,
239 'memory_mb': mem_sum,
240 'disk_gb': hdd_sum}}
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
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.
265 :param id: hostname
266 :returns: expected to use HostShowTemplate.
267 ex.::
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}