# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """The hosts admin extension.""" from oslo_log import log as logging import six import webob.exc from nova.api.openstack import extensions from nova import compute from nova import context as nova_context from nova import exception from nova.i18n import _ from nova.i18n import _LI from nova import objects LOG = logging.getLogger(__name__) authorize = extensions.extension_authorizer('compute', 'hosts') class HostController(object): """The Hosts API controller for the OpenStack API.""" def __init__(self): self.api = compute.HostAPI() super(HostController, self).__init__() def index(self, req): """Returns a dict in the format: | {'hosts': [{'host_name': 'some.host.name', | 'service': 'cells', | 'zone': 'internal'}, | {'host_name': 'some.other.host.name', | 'service': 'cells', | 'zone': 'internal'}, | {'host_name': 'some.celly.host.name', | 'service': 'cells', | 'zone': 'internal'}, | {'host_name': 'console1.host.com', | 'service': 'consoleauth', | 'zone': 'internal'}, | {'host_name': 'network1.host.com', | 'service': 'network', | 'zone': 'internal'}, | {'host_name': 'netwwork2.host.com', | 'service': 'network', | 'zone': 'internal'}, | {'host_name': 'compute1.host.com', | 'service': 'compute', | 'zone': 'nova'}, | {'host_name': 'compute2.host.com', | 'service': 'compute', | 'zone': 'nova'}, | {'host_name': 'sched1.host.com', | 'service': 'scheduler', | 'zone': 'internal'}, | {'host_name': 'sched2.host.com', | 'service': 'scheduler', | 'zone': 'internal'}, | {'host_name': 'vol1.host.com', | 'service': 'volume', | 'zone': 'internal'}]} """ context = req.environ['nova.context'] authorize(context) # NOTE(alex_xu): back-compatible with db layer hard-code admin # permission checks nova_context.require_admin_context(context) filters = {'disabled': False} zone = req.GET.get('zone', None) if zone: filters['availability_zone'] = zone services = self.api.service_get_all(context, filters=filters, set_zones=True) hosts = [] api_services = ('nova-osapi_compute', 'nova-ec2', 'nova-metadata') for service in services: if service.binary not in api_services: hosts.append({'host_name': service['host'], 'service': service['topic'], 'zone': service['availability_zone']}) return {'hosts': hosts} def update(self, req, id, body): """Updates a specified body. :param body: example format {'status': 'enable', 'maintenance_mode': 'enable'} """ def read_enabled(orig_val, msg): """Checks a specified orig_val and returns True for 'enabled' and False for 'disabled'. :param orig_val: A string with either 'enable' or 'disable'. May be surrounded by whitespace, and case doesn't matter :param msg: The message to be passed to HTTPBadRequest. A single %s will be replaced with orig_val. """ val = orig_val.strip().lower() if val == "enable": return True elif val == "disable": return False else: raise webob.exc.HTTPBadRequest(explanation=msg % orig_val) context = req.environ['nova.context'] authorize(context) # NOTE(alex_xu): back-compatible with db layer hard-code admin # permission checks. This has to be left only for API v2.0 because # this version has to be stable even if it means that only admins # can call this method while the policy could be changed. nova_context.require_admin_context(context) # See what the user wants to 'update' params = {k.strip().lower(): v for k, v in six.iteritems(body)} orig_status = status = params.pop('status', None) orig_maint_mode = maint_mode = params.pop('maintenance_mode', None) # Validate the request if len(params) > 0: # Some extra param was passed. Fail. explanation = _("Invalid update setting: '%s'") % list( params.keys())[0] raise webob.exc.HTTPBadRequest(explanation=explanation) if orig_status is not None: status = read_enabled(orig_status, _("Invalid status: '%s'")) if orig_maint_mode is not None: maint_mode = read_enabled(orig_maint_mode, _("Invalid mode: '%s'")) if status is None and maint_mode is None: explanation = _("'status' or 'maintenance_mode' needed for " "host update") raise webob.exc.HTTPBadRequest(explanation=explanation) # Make the calls and merge the results result = {'host': id} if status is not None: result['status'] = self._set_enabled_status(context, id, status) if maint_mode is not None: result['maintenance_mode'] = self._set_host_maintenance(context, id, maint_mode) return result def _set_host_maintenance(self, context, host_name, mode=True): """Start/Stop host maintenance window. On start, it triggers guest VMs evacuation. """ LOG.info(_LI("Putting host %(host_name)s in maintenance mode " "%(mode)s."), {'host_name': host_name, 'mode': mode}) try: result = self.api.set_host_maintenance(context, host_name, mode) except NotImplementedError: msg = _("Virt driver does not implement host maintenance mode.") raise webob.exc.HTTPNotImplemented(explanation=msg) except exception.NotFound as e: raise webob.exc.HTTPNotFound(explanation=e.format_message()) except exception.ComputeServiceUnavailable as e: raise webob.exc.HTTPBadRequest(explanation=e.format_message()) if result not in ("on_maintenance", "off_maintenance"): raise webob.exc.HTTPBadRequest(explanation=result) return result def _set_enabled_status(self, context, host_name, enabled): """Sets the specified host's ability to accept new instances. :param enabled: a boolean - if False no new VMs will be able to start on the host """ if enabled: LOG.info(_LI("Enabling host %s."), host_name) else: LOG.info(_LI("Disabling host %s."), host_name) try: result = self.api.set_host_enabled(context, host_name=host_name, enabled=enabled) except NotImplementedError: msg = _("Virt driver does not implement host disabled status.") raise webob.exc.HTTPNotImplemented(explanation=msg) except exception.NotFound as e: raise webob.exc.HTTPNotFound(explanation=e.format_message()) except exception.ComputeServiceUnavailable as e: raise webob.exc.HTTPBadRequest(explanation=e.format_message()) if result not in ("enabled", "disabled"): raise webob.exc.HTTPBadRequest(explanation=result) return result def _host_power_action(self, req, host_name, action): """Reboots, shuts down or powers up the host.""" context = req.environ['nova.context'] authorize(context) # NOTE(alex_xu): back-compatible with db layer hard-code admin # permission checks. This has to be left only for API v2.0 because # this version has to be stable even if it means that only admins # can call this method while the policy could be changed. nova_context.require_admin_context(context) try: result = self.api.host_power_action(context, host_name=host_name, action=action) except NotImplementedError: msg = _("Virt driver does not implement host power management.") raise webob.exc.HTTPNotImplemented(explanation=msg) except exception.NotFound as e: raise webob.exc.HTTPNotFound(explanation=e.format_message()) except exception.ComputeServiceUnavailable as e: raise webob.exc.HTTPBadRequest(explanation=e.format_message()) return {"host": host_name, "power_action": result} def startup(self, req, id): return self._host_power_action(req, host_name=id, action="startup") def shutdown(self, req, id): return self._host_power_action(req, host_name=id, action="shutdown") def reboot(self, req, id): return self._host_power_action(req, host_name=id, action="reboot") @staticmethod def _get_total_resources(host_name, compute_node): return {'resource': {'host': host_name, 'project': '(total)', 'cpu': compute_node['vcpus'], 'memory_mb': compute_node['memory_mb'], 'disk_gb': compute_node['local_gb']}} @staticmethod def _get_used_now_resources(host_name, compute_node): return {'resource': {'host': host_name, 'project': '(used_now)', 'cpu': compute_node['vcpus_used'], 'memory_mb': compute_node['memory_mb_used'], 'disk_gb': compute_node['local_gb_used']}} @staticmethod def _get_resource_totals_from_instances(host_name, instances): cpu_sum = 0 mem_sum = 0 hdd_sum = 0 for instance in instances: cpu_sum += instance['vcpus'] mem_sum += instance['memory_mb'] hdd_sum += instance['root_gb'] + instance['ephemeral_gb'] return {'resource': {'host': host_name, 'project': '(used_max)', 'cpu': cpu_sum, 'memory_mb': mem_sum, 'disk_gb': hdd_sum}} @staticmethod def _get_resources_by_project(host_name, instances): # Getting usage resource per project project_map = {} for instance in instances: resource = project_map.setdefault(instance['project_id'], {'host': host_name, 'project': instance['project_id'], 'cpu': 0, 'memory_mb': 0, 'disk_gb': 0}) resource['cpu'] += instance['vcpus'] resource['memory_mb'] += instance['memory_mb'] resource['disk_gb'] += (instance['root_gb'] + instance['ephemeral_gb']) return project_map def show(self, req, id): """Shows the physical/usage resource given by hosts. :param id: hostname :returns: expected to use HostShowTemplate. ex.:: {'host': {'resource':D},..} D: {'host': 'hostname','project': 'admin', 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30} """ context = req.environ['nova.context'] # NOTE(eliqiao): back-compatible with db layer hard-code admin # permission checks. This has to be left only for API v2.0 because # this version has to be stable even if it means that only admins # can call this method while the policy could be changed. nova_context.require_admin_context(context) host_name = id try: compute_node = ( objects.ComputeNode.get_first_node_by_host_for_old_compat( context, host_name)) except exception.NotFound as e: raise webob.exc.HTTPNotFound(explanation=e.format_message()) instances = self.api.instance_get_all_by_host(context, host_name) resources = [self._get_total_resources(host_name, compute_node)] resources.append(self._get_used_now_resources(host_name, compute_node)) resources.append(self._get_resource_totals_from_instances(host_name, instances)) by_proj_resources = self._get_resources_by_project(host_name, instances) for resource in six.itervalues(by_proj_resources): resources.append({'resource': resource}) return {'host': resources} class Hosts(extensions.ExtensionDescriptor): """Admin-only host administration.""" name = "Hosts" alias = "os-hosts" namespace = "http://docs.openstack.org/compute/ext/hosts/api/v1.1" updated = "2011-06-29T00:00:00Z" def get_resources(self): resources = [extensions.ResourceExtension('os-hosts', HostController(), collection_actions={'update': 'PUT'}, member_actions={"startup": "GET", "shutdown": "GET", "reboot": "GET"})] return resources