From 39739158b0cf10a775fd3899e35df7f53b4c9336 Mon Sep 17 00:00:00 2001
From: Carlos Goncalves <mail@cgoncalves.pt>
Date: Wed, 27 May 2015 08:32:31 +0200
Subject: [PATCH] Support forcing service down

Extending Nova CLI to support forcing service to be set/unset as down,
as specified in blueprint mark-host-down.

Depends-On: I612582ba7b70bb6d167aa68bdfc47faa3b7b85ed
Depends-On: I39f1a84c100726f87a4dc464dd9922d66efdb53f
Implements: blueprint support-force-down-service
Change-Id: I2b80ac32a95fe80363b4ad95d8d89fff097935a3
---
 novaclient/__init__.py                    |  2 +-
 novaclient/tests/unit/v2/fakes.py         |  6 ++++++
 novaclient/tests/unit/v2/test_services.py | 26 +++++++++++++++++++++++
 novaclient/v2/services.py                 | 19 +++++++++++++++++
 novaclient/v2/shell.py                    | 15 +++++++++++++
 5 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/novaclient/__init__.py b/novaclient/__init__.py
index a52cedc8a..5e6ccb0bb 100644
--- a/novaclient/__init__.py
+++ b/novaclient/__init__.py
@@ -20,4 +20,4 @@ from novaclient import api_versions
 __version__ = pbr.version.VersionInfo('python-novaclient').version_string()
 
 API_MIN_VERSION = api_versions.APIVersion("2.1")
-API_MAX_VERSION = api_versions.APIVersion("2.2")
+API_MAX_VERSION = api_versions.APIVersion("2.11")
diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py
index 4a2cf8886..6814461d1 100644
--- a/novaclient/tests/unit/v2/fakes.py
+++ b/novaclient/tests/unit/v2/fakes.py
@@ -1677,6 +1677,12 @@ class FakeHTTPClient(base_client.HTTPClient):
     def delete_os_services_1(self, **kw):
         return (204, {}, None)
 
+    def put_os_services_force_down(self, body, **kw):
+        return (200, {}, {'service': {
+            'host': body['host'],
+            'binary': body['binary'],
+            'forced_down': False}})
+
     #
     # Fixed IPs
     #
diff --git a/novaclient/tests/unit/v2/test_services.py b/novaclient/tests/unit/v2/test_services.py
index 2724342d4..f7ab553a0 100644
--- a/novaclient/tests/unit/v2/test_services.py
+++ b/novaclient/tests/unit/v2/test_services.py
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from novaclient import api_versions
 from novaclient.tests.unit import utils
 from novaclient.tests.unit.v2 import fakes
 from novaclient.v2 import services
@@ -97,3 +98,28 @@ class ServicesTest(utils.TestCase):
         self.cs.assert_called('PUT', '/os-services/disable-log-reason', values)
         self.assertIsInstance(service, self._get_service_type())
         self.assertEqual('disabled', service.status)
+
+
+class ServicesV211TestCase(ServicesTest):
+    def setUp(self):
+        super(ServicesV211TestCase, self).setUp()
+        self.cs.api_version = api_versions.APIVersion("2.11")
+
+    def _update_body(self, host, binary, disabled_reason=None,
+                     force_down=None):
+        body = {"host": host,
+                "binary": binary}
+        if disabled_reason is not None:
+            body["disabled_reason"] = disabled_reason
+        if force_down is not None:
+            body["forced_down"] = force_down
+        return body
+
+    def test_services_force_down(self):
+        service = self.cs.services.force_down(
+            'compute1', 'nova-compute', False)
+        values = self._update_body("compute1", "nova-compute",
+                                   force_down=False)
+        self.cs.assert_called('PUT', '/os-services/force-down', values)
+        self.assertIsInstance(service, self._get_service_type())
+        self.assertEqual(False, service.forced_down)
diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py
index d51fa3ebf..fcf800938 100644
--- a/novaclient/v2/services.py
+++ b/novaclient/v2/services.py
@@ -16,6 +16,7 @@
 """
 service interface
 """
+from novaclient import api_versions
 from novaclient import base
 
 
@@ -48,6 +49,7 @@ class ServiceManager(base.ManagerWithFind):
             url = "%s?%s" % (url, "&".join(filters))
         return self._list(url, "services")
 
+    @api_versions.wraps("2.0", "2.10")
     def _update_body(self, host, binary, disabled_reason=None):
         body = {"host": host,
                 "binary": binary}
@@ -55,6 +57,17 @@ class ServiceManager(base.ManagerWithFind):
             body["disabled_reason"] = disabled_reason
         return body
 
+    @api_versions.wraps("2.11")
+    def _update_body(self, host, binary, disabled_reason=None,
+                     force_down=None):
+        body = {"host": host,
+                "binary": binary}
+        if disabled_reason is not None:
+            body["disabled_reason"] = disabled_reason
+        if force_down is not None:
+            body["forced_down"] = force_down
+        return body
+
     def enable(self, host, binary):
         """Enable the service specified by hostname and binary."""
         body = self._update_body(host, binary)
@@ -73,3 +86,9 @@ class ServiceManager(base.ManagerWithFind):
     def delete(self, service_id):
         """Delete a service."""
         return self._delete("/os-services/%s" % service_id)
+
+    @api_versions.wraps("2.11")
+    def force_down(self, host, binary, force_down=None):
+        """Force service state to down specified by hostname and binary."""
+        body = self._update_body(host, binary, force_down=force_down)
+        return self._update("/os-services/force-down", body, "service")
diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py
index 5ad0014d1..9e840a70d 100644
--- a/novaclient/v2/shell.py
+++ b/novaclient/v2/shell.py
@@ -3568,6 +3568,21 @@ def do_service_disable(cs, args):
         utils.print_list([result], ['Host', 'Binary', 'Status'])
 
 
+@api_versions.wraps("2.11")
+@cliutils.arg('host', metavar='<hostname>', help=_('Name of host.'))
+@cliutils.arg('binary', metavar='<binary>', help=_('Service binary.'))
+@cliutils.arg(
+    '--unset',
+    dest='force_down',
+    help=_("Unset the force state down of service"),
+    action='store_false',
+    default=True)
+def do_service_force_down(cs, args):
+    """Force service to down."""
+    result = cs.services.force_down(args.host, args.binary, args.force_down)
+    utils.print_list([result], ['Host', 'Binary', 'Forced down'])
+
+
 @cliutils.arg('id', metavar='<id>', help=_('Id of service.'))
 def do_service_delete(cs, args):
     """Delete the service."""