diff --git a/common-powervc/powervc/common/client/extensions/nova.py b/common-powervc/powervc/common/client/extensions/nova.py index 5ae978c..25e077e 100644 --- a/common-powervc/powervc/common/client/extensions/nova.py +++ b/common-powervc/powervc/common/client/extensions/nova.py @@ -1,4 +1,4 @@ -# Copyright 2013 IBM Corp. +# Copyright 2014 IBM Corp. import six import urllib @@ -13,6 +13,7 @@ from novaclient.v1_1.volume_types import VolumeType from powervc.common.client.extensions import base from powervc.common.gettextutils import _ from powervc.common import utils +from webob import exc import logging LOG = logging.getLogger(__name__) @@ -48,11 +49,13 @@ class PVCHypervisorManager(hypervisors.HypervisorManager): hypervisors = self.search(hostname) if not hypervisors[0] or not self.get(hypervisors[0]): - raise exceptions.NotFound(_("No hypervisor matching '%s' could be" - " found.") - % hostname) + raise exc.HTTPNotFound(_("No hypervisor matching '%s' could be" + " found.") % hostname) - hypervisor = self.get(hypervisors[0]) + try: + hypervisor = self.get(hypervisors[0]) + except Exception as ex: + raise exc.HTTPNotFound(explanation=six.text_type(ex)) # Either "ok" (maintenance off), "entering", "on" or "error" # compatible with previous powervc version, if no such property @@ -68,7 +71,8 @@ class PVCHypervisorManager(hypervisors.HypervisorManager): return {"maintenance_status": maintenance_status, "maintenance_migration_action": maintenance_migration_action} - def update_host_maintenance_mode(self, hostname, enabled, migrate): + def update_host_maintenance_mode(self, hostname, enabled, migrate=None, + target_host=None): """Update host maintenance mode status. :hostname: The hostname of the hypervisor :enabled: should be "enable" or "disable" @@ -79,11 +83,22 @@ class PVCHypervisorManager(hypervisors.HypervisorManager): """ # Refer to PowerVC HLD host maintenance mode chapter url = "/ego/prs/hypervisor_maintenance/%s" % hostname - body = {"status": enabled, - "migrate": migrate} + if not migrate: + body = {"status": enabled} + else: + if target_host: + body = {"status": enabled, + "migrate": migrate, + "target_host": target_host} + else: + body = {"status": enabled, + "migrate": migrate} # send set maintenance mode request by put http method - _resp, resp_body = self.api.client.put(url, body=body) + try: + _resp, resp_body = self.api.client.put(url, body=body) + except Exception as ex: + raise exc.HTTPBadRequest(explanation=six.text_type(ex)) # check response content if "hypervisor_maintenance" not in resp_body: diff --git a/nova-powervc/hostmaintenanceclient/__init__.py b/nova-powervc/hostmaintenanceclient/__init__.py new file mode 100644 index 0000000..4653ab6 --- /dev/null +++ b/nova-powervc/hostmaintenanceclient/__init__.py @@ -0,0 +1 @@ +# Copyright 2014 IBM Corp. diff --git a/nova-powervc/hostmaintenanceclient/setup.py b/nova-powervc/hostmaintenanceclient/setup.py new file mode 100644 index 0000000..b76fb58 --- /dev/null +++ b/nova-powervc/hostmaintenanceclient/setup.py @@ -0,0 +1,15 @@ +# Copyright 2014 IBM Corp. + +from setuptools import setup, find_packages + +setup( + name="hostmaintenance-client", + version="0.1", + packages=find_packages(exclude=['*.tests', 'tests', + 'tests.*', '*.tests.*']), + entry_points={ + 'novaclient.extension': [ + 'host_maintenance = v1_1.host_maintenance', + ], + } +) diff --git a/nova-powervc/hostmaintenanceclient/v1_1/__init__.py b/nova-powervc/hostmaintenanceclient/v1_1/__init__.py new file mode 100644 index 0000000..4653ab6 --- /dev/null +++ b/nova-powervc/hostmaintenanceclient/v1_1/__init__.py @@ -0,0 +1 @@ +# Copyright 2014 IBM Corp. diff --git a/nova-powervc/hostmaintenanceclient/v1_1/host_maintenance.py b/nova-powervc/hostmaintenanceclient/v1_1/host_maintenance.py new file mode 100644 index 0000000..ffbf8d3 --- /dev/null +++ b/nova-powervc/hostmaintenanceclient/v1_1/host_maintenance.py @@ -0,0 +1,89 @@ +# Copyright 2014 IBM Corp. + +from novaclient import base +from novaclient import exceptions +from powervc.common.gettextutils import _ +from novaclient import utils + + +class HostMaintenanceResource(base.Resource): + def __repr__(self): + return "" % self.hypervisor_hostname + + +class HostMaintenanceManager(base.Manager): + resource_class = HostMaintenanceResource + + def update(self, host_name, status, migrate=None, target_host=None): + """ + Update status, migrate or target host when put a hypervisor + into maintenance mode. + """ + if "enable" == status: + body = {'status': status, 'migrate': migrate, + 'target_host': target_host} + else: + body = {'status': status} + return self._update("/os-host-maintenance-mode/%s" % host_name, + body) + + def get(self, host_name): + url = "/os-host-maintenance-mode/%s" % host_name + _resp, body = self.api.client.get(url) + # set host name for response body to form a object + if body: + body["hypervisor_hostname"] = host_name + source_obj = {} + source_obj['hypervisor_maintenance'] = body + source_obj['hypervisor_hostname'] = host_name + obj = self.resource_class(self, source_obj, loaded=True) + self._write_object_to_completion_cache(obj) + return obj + + +@utils.arg('host', + metavar='', + help='Name of a host.') +@utils.arg('--set-status', choices=["enable", "disable"], + metavar="", + help='To enable or disable the host maintenance mode.') +@utils.arg('--migrate', choices=["active-only", "all", "none"], + metavar="", + help='Which kinds of instances to migrate.') +@utils.arg('--target-host', + metavar='', + help='Which service host instances would be migrated to.') +def do_host_maintenance(cs, args): + """Enable maintenance mode for a hypervisor.""" + if not args.set_status: + if args.migrate or args.target_host: + raise exceptions.CommandError(_("Need to set --set-status " + "to 'enable' when --migrate " + "or --target-host specified.")) + else: + return _show_maintenance_status(cs, args.host) + + if "disable" == args.set_status.lower(): + if args.migrate or args.target_host: + raise exceptions.CommandError(_("No need to specify migrate or " + "target-host when disabling the " + "host maintenance mode.")) + + hv = cs.host_maintenance.update(args.host, + args.set_status, + args.migrate, + args.target_host) + + host = HostMaintenanceResource(HostMaintenanceManager, + hv.hypervisor_maintenance) + utils.print_list([host], ['hypervisor_hostname', 'status', + 'migrate', 'target-host']) + + +def _show_maintenance_status(cs, host): + hv = cs.host_maintenance.get(host) + host = HostMaintenanceResource(HostMaintenanceManager, + hv.hypervisor_maintenance) + + utils.print_list([host], ['hypervisor_hostname', 'maintenance_status', + 'maintenance_migration_action']) diff --git a/nova-powervc/powervc/nova/extension/host_maintenance_mode.py b/nova-powervc/powervc/nova/extension/host_maintenance_mode.py index 20f6f18..137e3fe 100644 --- a/nova-powervc/powervc/nova/extension/host_maintenance_mode.py +++ b/nova-powervc/powervc/nova/extension/host_maintenance_mode.py @@ -53,15 +53,16 @@ class Controller(wsgi.Controller): "'enable' or 'disable'")) migrate_candidate = ["none", "active-only", "all"] - migrate = body.get("migrate", "none") - if migrate.lower() not in migrate_candidate: + migrate = body.get("migrate") + if migrate and migrate.lower() not in migrate_candidate: raise exc.HTTPBadRequest(_("Malformed request body, migrate wrong " "in request body, should be 'none'," "active-only, all or empty")) + target_host = body.get("target_host") # Set maintenance mode from powervc client maintenance_update_status = self.pvcclient.hypervisors.\ update_host_maintenance_mode(host_name, maintenance_status, - migrate) + migrate, target_host) return maintenance_update_status diff --git a/nova-powervc/test/hostmaintenanceclient/__init__.py b/nova-powervc/test/hostmaintenanceclient/__init__.py new file mode 100644 index 0000000..4653ab6 --- /dev/null +++ b/nova-powervc/test/hostmaintenanceclient/__init__.py @@ -0,0 +1 @@ +# Copyright 2014 IBM Corp. diff --git a/nova-powervc/test/hostmaintenanceclient/v1_1/__init__.py b/nova-powervc/test/hostmaintenanceclient/v1_1/__init__.py new file mode 100644 index 0000000..4653ab6 --- /dev/null +++ b/nova-powervc/test/hostmaintenanceclient/v1_1/__init__.py @@ -0,0 +1 @@ +# Copyright 2014 IBM Corp. diff --git a/nova-powervc/test/hostmaintenanceclient/v1_1/test_host_maintenance.py b/nova-powervc/test/hostmaintenanceclient/v1_1/test_host_maintenance.py new file mode 100644 index 0000000..1cb4f82 --- /dev/null +++ b/nova-powervc/test/hostmaintenanceclient/v1_1/test_host_maintenance.py @@ -0,0 +1,142 @@ +# Copyright 2014 IBM Corp. + +from webob import exc + +from mock import MagicMock +from novaclient import exceptions +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import hypervisors as data +from novaclient.tests import utils +from hostmaintenanceclient.v1_1 import host_maintenance + + +class HostMaintenanceTest(utils.FixturedTestCase): + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + def compare_to_expected(self, expected, hyper): + for key, value in expected.items(): + self.assertEqual(getattr(hyper, key), value) + + def test_host_maintenance_get(self): + restAPI = MagicMock() + args = MagicMock() + # generate cs parameter + get_return_value = {"maintenance_status": "on", + "maintenance_migration_action": "none"} + + host_manager = host_maintenance.HostMaintenanceManager(restAPI) + host_manager.api.client.get = MagicMock( + return_value=(200, get_return_value)) + self.cs.host_maintenance = host_manager + args.set_status = None + args.migrate = None + args.target_host = None + args.host = "789522X_067E30B" + + # generate args parameter + host_maintenance.do_host_maintenance(self.cs, args) + restAPI.client.get.assert_called_once_with( + '/os-host-maintenance-mode/789522X_067E30B') + + def test_host_maintenance_get_not_found(self): + restAPI = MagicMock() + args = MagicMock() + # generate cs parameter + host_manager = host_maintenance.HostMaintenanceManager(restAPI) + host_manager.api.client.get = MagicMock( + side_effect=exc.HTTPNotFound('The specified host cannot be found')) + self.cs.host_maintenance = host_manager + args.set_status = None + args.migrate = None + args.target_host = None + args.host = "789522X_067E30B" + + # generate args parameter + self.assertRaises(exc.HTTPNotFound, + host_maintenance.do_host_maintenance, + self.cs, args) + restAPI.client.get.assert_called_once_with( + '/os-host-maintenance-mode/789522X_067E30B') + + def test_host_maintenance_update_enable_success(self): + restAPI = MagicMock() + args = MagicMock() + # generate cs parameter + update_return_value = {"hypervisor_maintenance": + {"status": "enable", + "migrate": "none", + "target-host": "none", + "hypervisor_hostname": "789522X_067E30B"} + } + + host_manager = host_maintenance.HostMaintenanceManager(restAPI) + host_manager.api.client.put = MagicMock( + return_value=(200, update_return_value)) + self.cs.host_maintenance = host_manager + args.set_status = 'enable' + args.migrate = None + args.target_host = None + args.host = "789522X_067E30B" + + # generate args parameter + host_maintenance.do_host_maintenance(self.cs, args) + restAPI.client.put.assert_called_once_with( + '/os-host-maintenance-mode/789522X_067E30B', + body={'status': 'enable', 'migrate': None, 'target_host': None}) + + def test_host_maintenance_update_disable_success(self): + restAPI = MagicMock() + args = MagicMock() + # generate cs parameter + update_return_value = {"hypervisor_maintenance": + {"status": "disable", + "migrate": "none", + "target-host": "none", + "hypervisor_hostname": "789522X_067E30B"} + } + + host_manager = host_maintenance.HostMaintenanceManager(restAPI) + host_manager.api.client.put = MagicMock( + return_value=(200, update_return_value)) + self.cs.host_maintenance = host_manager + args.set_status = 'disable' + args.migrate = None + args.target_host = None + args.host = "789522X_067E30B" + + # generate args parameter + host_maintenance.do_host_maintenance(self.cs, args) + restAPI.client.put.assert_called_once_with( + '/os-host-maintenance-mode/789522X_067E30B', + body={'status': 'disable'}) + + def test_host_maintenance_get_parameter_error1(self): + args = MagicMock() + args.set_status = None + args.migrate = 'active-only' + args.target_host = None + args.host = "789522X_067E30B" + + self.assertRaises(exceptions.CommandError, + host_maintenance.do_host_maintenance, self.cs, args) + + def test_host_maintenance_get_parameter_error2(self): + args = MagicMock() + args.set_status = None + args.migrate = None + args.target_host = "789522X_067E31B" + args.host = "789522X_067E30B" + + self.assertRaises(exceptions.CommandError, + host_maintenance.do_host_maintenance, self.cs, args) + + def test_host_maintenance_get_parameter_error3(self): + args = MagicMock() + args.set_status = 'disable' + args.migrate = None + args.target_host = "789522X_067E31B" + args.host = "789522X_067E30B" + + self.assertRaises(exceptions.CommandError, + host_maintenance.do_host_maintenance, self.cs, args)