summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEd Leafe <ed@leafe.com>2011-07-07 18:30:47 +0000
committerTarmac <>2011-07-07 18:30:47 +0000
commit877b1bebe49ba87be373b5b2c8eff6d6c51f0ff7 (patch)
tree097f80d4331ca98141e181374bf6c189c1a4973a
parent8af474f32f8e53142cc5f10e567a03b274693d44 (diff)
parent9d29dc60d904f2c5037d03cead71933dc62777ff (diff)
downloadnova-877b1bebe49ba87be373b5b2c8eff6d6c51f0ff7.tar.gz
nova-877b1bebe49ba87be373b5b2c8eff6d6c51f0ff7.tar.xz
nova-877b1bebe49ba87be373b5b2c8eff6d6c51f0ff7.zip
Adds the os-hosts API extension for interacting with hosts while performing maintenance. This differs from the previous merge prop as it uses a RESTful design instead of GET-based actions.
This supersedes the previous merge prop (https://code.launchpad.net/~ed-leafe/nova/maintmode/+merge/66303). The code has been both simplified and cleaned up a great deal, so there is no real need for the previous interim code.
-rw-r--r--nova/api/openstack/contrib/hosts.py114
-rw-r--r--nova/compute/api.py5
-rw-r--r--nova/compute/manager.py6
-rw-r--r--nova/scheduler/api.py5
-rw-r--r--nova/scheduler/manager.py4
-rw-r--r--nova/scheduler/zone_manager.py16
-rw-r--r--nova/tests/test_hosts.py102
-rw-r--r--nova/virt/driver.py4
-rw-r--r--nova/virt/fake.py4
-rw-r--r--nova/virt/hyperv.py4
-rw-r--r--nova/virt/libvirt/connection.py4
-rw-r--r--nova/virt/vmwareapi_conn.py4
-rw-r--r--nova/virt/xenapi/vmops.py26
-rw-r--r--nova/virt/xenapi_conn.py4
-rw-r--r--plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost49
15 files changed, 344 insertions, 7 deletions
diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py
new file mode 100644
index 000000000..55e57e1a4
--- /dev/null
+++ b/nova/api/openstack/contrib/hosts.py
@@ -0,0 +1,114 @@
+# Copyright (c) 2011 Openstack, LLC.
+# 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."""
+
+import webob.exc
+
+from nova import compute
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import faults
+from nova.scheduler import api as scheduler_api
+
+
+LOG = logging.getLogger("nova.api.hosts")
+FLAGS = flags.FLAGS
+
+
+def _list_hosts(req, service=None):
+ """Returns a summary list of hosts, optionally filtering
+ by service type.
+ """
+ context = req.environ['nova.context']
+ hosts = scheduler_api.get_host_list(context)
+ if service:
+ hosts = [host for host in hosts
+ if host["service"] == service]
+ return hosts
+
+
+def check_host(fn):
+ """Makes sure that the host exists."""
+ def wrapped(self, req, id, service=None, *args, **kwargs):
+ listed_hosts = _list_hosts(req, service)
+ hosts = [h["host_name"] for h in listed_hosts]
+ if id in hosts:
+ return fn(self, req, id, *args, **kwargs)
+ else:
+ raise exception.HostNotFound(host=id)
+ return wrapped
+
+
+class HostController(object):
+ """The Hosts API controller for the OpenStack API."""
+ def __init__(self):
+ self.compute_api = compute.API()
+ super(HostController, self).__init__()
+
+ def index(self, req):
+ return {'hosts': _list_hosts(req)}
+
+ @check_host
+ def update(self, req, id, body):
+ for raw_key, raw_val in body.iteritems():
+ key = raw_key.lower().strip()
+ val = raw_val.lower().strip()
+ # NOTE: (dabo) Right now only 'status' can be set, but other
+ # actions may follow.
+ if key == "status":
+ if val[:6] in ("enable", "disabl"):
+ return self._set_enabled_status(req, id,
+ enabled=(val.startswith("enable")))
+ else:
+ explanation = _("Invalid status: '%s'") % raw_val
+ raise webob.exc.HTTPBadRequest(explanation=explanation)
+ else:
+ explanation = _("Invalid update setting: '%s'") % raw_key
+ raise webob.exc.HTTPBadRequest(explanation=explanation)
+
+ def _set_enabled_status(self, req, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ context = req.environ['nova.context']
+ state = "enabled" if enabled else "disabled"
+ LOG.audit(_("Setting host %(host)s to %(state)s.") % locals())
+ result = self.compute_api.set_host_enabled(context, host=host,
+ enabled=enabled)
+ return {"host": host, "status": result}
+
+
+class Hosts(extensions.ExtensionDescriptor):
+ def get_name(self):
+ return "Hosts"
+
+ def get_alias(self):
+ return "os-hosts"
+
+ def get_description(self):
+ return "Host administration"
+
+ def get_namespace(self):
+ return "http://docs.openstack.org/ext/hosts/api/v1.1"
+
+ def get_updated(self):
+ return "2011-06-29T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = [extensions.ResourceExtension('os-hosts', HostController(),
+ collection_actions={'update': 'PUT'}, member_actions={})]
+ return resources
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 28459dc75..b0eedcd64 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -912,6 +912,11 @@ class API(base.Base):
"""Unpause the given instance."""
self._cast_compute_message('unpause_instance', context, instance_id)
+ def set_host_enabled(self, context, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ return self._call_compute_message("set_host_enabled", context,
+ instance_id=None, host=host, params={"enabled": enabled})
+
@scheduler_api.reroute_compute("diagnostics")
def get_diagnostics(self, context, instance_id):
"""Retrieve diagnostics for the given instance."""
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index bbbddde0a..91a604934 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -875,6 +875,12 @@ class ComputeManager(manager.SchedulerDependentManager):
result))
@exception.wrap_exception
+ def set_host_enabled(self, context, instance_id=None, host=None,
+ enabled=None):
+ """Sets the specified host's ability to accept new instances."""
+ return self.driver.set_host_enabled(host, enabled)
+
+ @exception.wrap_exception
def get_diagnostics(self, context, instance_id):
"""Retrieve diagnostics for an instance on this host."""
instance_ref = self.db.instance_get(context, instance_id)
diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py
index 0f4fc48c8..137b671c0 100644
--- a/nova/scheduler/api.py
+++ b/nova/scheduler/api.py
@@ -51,6 +51,11 @@ def _call_scheduler(method, context, params=None):
return rpc.call(context, queue, kwargs)
+def get_host_list(context):
+ """Return a list of hosts associated with this zone."""
+ return _call_scheduler('get_host_list', context)
+
+
def get_zone_list(context):
"""Return a list of zones assoicated with this zone."""
items = _call_scheduler('get_zone_list', context)
diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py
index 6cb75aa8d..749d66cad 100644
--- a/nova/scheduler/manager.py
+++ b/nova/scheduler/manager.py
@@ -56,6 +56,10 @@ class SchedulerManager(manager.Manager):
"""Poll child zones periodically to get status."""
self.zone_manager.ping(context)
+ def get_host_list(self, context=None):
+ """Get a list of hosts from the ZoneManager."""
+ return self.zone_manager.get_host_list()
+
def get_zone_list(self, context=None):
"""Get a list of zones from the ZoneManager."""
return self.zone_manager.get_zone_list()
diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py
index ba7403c15..6093443a9 100644
--- a/nova/scheduler/zone_manager.py
+++ b/nova/scheduler/zone_manager.py
@@ -115,6 +115,18 @@ class ZoneManager(object):
"""Return the list of zones we know about."""
return [zone.to_dict() for zone in self.zone_states.values()]
+ def get_host_list(self):
+ """Returns a list of dicts for each host that the Zone Manager
+ knows about. Each dict contains the host_name and the service
+ for that host.
+ """
+ all_hosts = self.service_states.keys()
+ ret = []
+ for host in self.service_states:
+ for svc in self.service_states[host]:
+ ret.append({"service": svc, "host_name": host})
+ return ret
+
def get_zone_capabilities(self, context):
"""Roll up all the individual host info to generic 'service'
capabilities. Each capability is aggregated into
@@ -127,13 +139,15 @@ class ZoneManager(object):
combined = {} # { <service>_<cap> : (min, max), ... }
for host, host_dict in hosts_dict.iteritems():
for service_name, service_dict in host_dict.iteritems():
+ if not service_dict.get("enabled", True):
+ # Service is disabled; do no include it
+ continue
for cap, value in service_dict.iteritems():
key = "%s_%s" % (service_name, cap)
min_value, max_value = combined.get(key, (value, value))
min_value = min(min_value, value)
max_value = max(max_value, value)
combined[key] = (min_value, max_value)
-
return combined
def _refresh_from_db(self, context):
diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py
new file mode 100644
index 000000000..548f81f8b
--- /dev/null
+++ b/nova/tests/test_hosts.py
@@ -0,0 +1,102 @@
+# Copyright (c) 2011 Openstack, LLC.
+# 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.
+
+import stubout
+import webob.exc
+
+from nova import context
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import test
+from nova.api.openstack.contrib import hosts as os_hosts
+from nova.scheduler import api as scheduler_api
+
+
+FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.tests.hosts')
+# Simulate the hosts returned by the zone manager.
+HOST_LIST = [
+ {"host_name": "host_c1", "service": "compute"},
+ {"host_name": "host_c2", "service": "compute"},
+ {"host_name": "host_v1", "service": "volume"},
+ {"host_name": "host_v2", "service": "volume"}]
+
+
+def stub_get_host_list(req):
+ return HOST_LIST
+
+
+def stub_set_host_enabled(context, host, enabled):
+ # We'll simulate success and failure by assuming
+ # that 'host_c1' always succeeds, and 'host_c2'
+ # always fails
+ fail = (host == "host_c2")
+ status = "enabled" if (enabled ^ fail) else "disabled"
+ return status
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context.get_admin_context()}
+
+
+class HostTestCase(test.TestCase):
+ """Test Case for hosts."""
+
+ def setUp(self):
+ super(HostTestCase, self).setUp()
+ self.controller = os_hosts.HostController()
+ self.req = FakeRequest()
+ self.stubs.Set(scheduler_api, 'get_host_list', stub_get_host_list)
+ self.stubs.Set(self.controller.compute_api, 'set_host_enabled',
+ stub_set_host_enabled)
+
+ def test_list_hosts(self):
+ """Verify that the compute hosts are returned."""
+ hosts = os_hosts._list_hosts(self.req)
+ self.assertEqual(hosts, HOST_LIST)
+
+ compute_hosts = os_hosts._list_hosts(self.req, "compute")
+ expected = [host for host in HOST_LIST
+ if host["service"] == "compute"]
+ self.assertEqual(compute_hosts, expected)
+
+ def test_disable_host(self):
+ dis_body = {"status": "disable"}
+ result_c1 = self.controller.update(self.req, "host_c1", body=dis_body)
+ self.assertEqual(result_c1["status"], "disabled")
+ result_c2 = self.controller.update(self.req, "host_c2", body=dis_body)
+ self.assertEqual(result_c2["status"], "enabled")
+
+ def test_enable_host(self):
+ en_body = {"status": "enable"}
+ result_c1 = self.controller.update(self.req, "host_c1", body=en_body)
+ self.assertEqual(result_c1["status"], "enabled")
+ result_c2 = self.controller.update(self.req, "host_c2", body=en_body)
+ self.assertEqual(result_c2["status"], "disabled")
+
+ def test_bad_status_value(self):
+ bad_body = {"status": "bad"}
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ self.req, "host_c1", body=bad_body)
+
+ def test_bad_update_key(self):
+ bad_body = {"crazy": "bad"}
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ self.req, "host_c1", body=bad_body)
+
+ def test_bad_host(self):
+ self.assertRaises(exception.HostNotFound, self.controller.update,
+ self.req, "bogus_host_name", body={"status": "disable"})
diff --git a/nova/virt/driver.py b/nova/virt/driver.py
index 1c9797973..3c4a073bf 100644
--- a/nova/virt/driver.py
+++ b/nova/virt/driver.py
@@ -249,3 +249,7 @@ class ComputeDriver(object):
def poll_rescued_instances(self, timeout):
"""Poll for rescued instances"""
raise NotImplementedError()
+
+ def set_host_enabled(self, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ raise NotImplementedError()
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 5fe9d674f..ea0a59f21 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -514,3 +514,7 @@ class FakeConnection(driver.ComputeDriver):
def get_host_stats(self, refresh=False):
"""Return fake Host Status of ram, disk, network."""
return self.host_status
+
+ def set_host_enabled(self, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ pass
diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py
index f6783f3aa..5c1dc772d 100644
--- a/nova/virt/hyperv.py
+++ b/nova/virt/hyperv.py
@@ -499,3 +499,7 @@ class HyperVConnection(driver.ComputeDriver):
def get_host_stats(self, refresh=False):
"""See xenapi_conn.py implementation."""
pass
+
+ def set_host_enabled(self, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ pass
diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py
index 233d14203..e912c2bec 100644
--- a/nova/virt/libvirt/connection.py
+++ b/nova/virt/libvirt/connection.py
@@ -1591,3 +1591,7 @@ class LibvirtConnection(driver.ComputeDriver):
def get_host_stats(self, refresh=False):
"""See xenapi_conn.py implementation."""
pass
+
+ def set_host_enabled(self, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ pass
diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py
index 3c6345ec8..d80e14931 100644
--- a/nova/virt/vmwareapi_conn.py
+++ b/nova/virt/vmwareapi_conn.py
@@ -190,6 +190,10 @@ class VMWareESXConnection(driver.ComputeDriver):
"""This method is supported only by libvirt."""
return
+ def set_host_enabled(self, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ pass
+
class VMWareAPISession(object):
"""
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index b116c8467..cb96930c1 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -24,6 +24,7 @@ import json
import M2Crypto
import os
import pickle
+import random
import subprocess
import time
import uuid
@@ -932,6 +933,31 @@ class VMOps(object):
# TODO: implement this!
return 'http://fakeajaxconsole/fake_url'
+ def set_host_enabled(self, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ args = {"enabled": json.dumps(enabled)}
+ json_resp = self._call_xenhost("set_host_enabled", args)
+ resp = json.loads(json_resp)
+ return resp["status"]
+
+ def _call_xenhost(self, method, arg_dict):
+ """There will be several methods that will need this general
+ handling for interacting with the xenhost plugin, so this abstracts
+ out that behavior.
+ """
+ # Create a task ID as something that won't match any instance ID
+ task_id = random.randint(-80000, -70000)
+ try:
+ task = self._session.async_call_plugin("xenhost", method,
+ args=arg_dict)
+ #args={"params": arg_dict})
+ ret = self._session.wait_for_task(task, task_id)
+ except self.XenAPI.Failure as e:
+ ret = None
+ LOG.error(_("The call to %(method)s returned an error: %(e)s.")
+ % locals())
+ return ret
+
def inject_network_info(self, instance, network_info, vm_ref=None):
"""
Generate the network info and make calls to place it into the
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index cd4dc1b60..ec8c44c1c 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -336,6 +336,10 @@ class XenAPIConnection(driver.ComputeDriver):
True, run the update first."""
return self.HostState.get_host_stats(refresh=refresh)
+ def set_host_enabled(self, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ return self._vmops.set_host_enabled(host, enabled)
+
class XenAPISession(object):
"""The session to invoke XenAPI SDK calls"""
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost
index a8428e841..292bbce12 100644
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost
@@ -33,9 +33,10 @@ import tempfile
import time
import XenAPIPlugin
+import pluginlib_nova as pluginlib
-from pluginlib_nova import *
-configure_logging("xenhost")
+
+pluginlib.configure_logging("xenhost")
host_data_pattern = re.compile(r"\s*(\S+) \([^\)]+\) *: ?(.*)")
@@ -65,14 +66,49 @@ def _run_command(cmd):
return proc.stdout.read()
+def _get_host_uuid():
+ cmd = "xe host-list | grep uuid"
+ resp = _run_command(cmd)
+ return resp.split(":")[-1].strip()
+
+
+@jsonify
+def set_host_enabled(self, arg_dict):
+ """Sets this host's ability to accept new instances.
+ It will otherwise continue to operate normally.
+ """
+ enabled = arg_dict.get("enabled")
+ if enabled is None:
+ raise pluginlib.PluginError(
+ _("Missing 'enabled' argument to set_host_enabled"))
+ if enabled == "true":
+ result = _run_command("xe host-enable")
+ elif enabled == "false":
+ result = _run_command("xe host-disable")
+ else:
+ raise pluginlib.PluginError(_("Illegal enabled status: %s") % enabled)
+ # Should be empty string
+ if result:
+ raise pluginlib.PluginError(result)
+ # Return the current enabled status
+ host_uuid = _get_host_uuid()
+ cmd = "xe host-param-list uuid=%s | grep enabled" % host_uuid
+ resp = _run_command(cmd)
+ # Response should be in the format: "enabled ( RO): true"
+ host_enabled = resp.strip().split()[-1]
+ if host_enabled == "true":
+ status = "enabled"
+ else:
+ status = "disabled"
+ return {"status": status}
+
+
@jsonify
def host_data(self, arg_dict):
"""Runs the commands on the xenstore host to return the current status
information.
"""
- cmd = "xe host-list | grep uuid"
- resp = _run_command(cmd)
- host_uuid = resp.split(":")[-1].strip()
+ host_uuid = _get_host_uuid()
cmd = "xe host-param-list uuid=%s" % host_uuid
resp = _run_command(cmd)
parsed_data = parse_response(resp)
@@ -180,4 +216,5 @@ def cleanup(dct):
if __name__ == "__main__":
XenAPIPlugin.dispatch(
- {"host_data": host_data})
+ {"host_data": host_data,
+ "set_host_enabled": set_host_enabled})