From 42d4d8b93a9df8db378f293e3bf068d76f032468 Mon Sep 17 00:00:00 2001
From: Michael Hanselmann <hansmi@google.com>
Date: Fri, 16 Sep 2011 11:38:15 +0200
Subject: [PATCH] RAPI: Add resource to powercycle node

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>
---
 doc/rapi.rst                        | 11 +++++++++++
 lib/rapi/client.py                  | 20 ++++++++++++++++++++
 lib/rapi/connector.py               |  2 ++
 lib/rapi/rlib2.py                   | 16 ++++++++++++++++
 test/docs_unittest.py               |  1 -
 test/ganeti.rapi.client_unittest.py | 10 ++++++++++
 test/ganeti.rapi.rlib2_unittest.py  | 20 ++++++++++++++++++++
 7 files changed, 79 insertions(+), 1 deletion(-)

diff --git a/doc/rapi.rst b/doc/rapi.rst
index 20602b052..23805d6f7 100644
--- a/doc/rapi.rst
+++ b/doc/rapi.rst
@@ -1241,6 +1241,17 @@ It supports the following commands: ``GET``.
 
 Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.N_FIELDS))`
 
+``/2/nodes/[node_name]/powercycle``
++++++++++++++++++++++++++++++++++++
+
+Powercycles a node. Supports the following commands: ``POST``.
+
+``POST``
+~~~~~~~~
+
+Returns a job ID.
+
+
 ``/2/nodes/[node_name]/evacuate``
 +++++++++++++++++++++++++++++++++
 
diff --git a/lib/rapi/client.py b/lib/rapi/client.py
index 8ad0d95a1..9f3002a80 100644
--- a/lib/rapi/client.py
+++ b/lib/rapi/client.py
@@ -1474,6 +1474,26 @@ class GanetiRapiClient(object): # pylint: disable=R0904
                              ("/%s/nodes/%s/role" %
                               (GANETI_RAPI_VERSION, node)), query, role)
 
+  def PowercycleNode(self, node, force=False):
+    """Powercycles a node.
+
+    @type node: string
+    @param node: Node name
+    @type force: bool
+    @param force: Whether to force the operation
+
+    @rtype: string
+    @return: job id
+
+    """
+    query = [
+      ("force", force),
+      ]
+
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/nodes/%s/powercycle" %
+                              (GANETI_RAPI_VERSION, node)), query, None)
+
   def GetNodeStorageUnits(self, node, storage_type, output_fields):
     """Gets the storage units for a node.
 
diff --git a/lib/rapi/connector.py b/lib/rapi/connector.py
index 8d0d3951a..a20027bee 100644
--- a/lib/rapi/connector.py
+++ b/lib/rapi/connector.py
@@ -107,6 +107,8 @@ def GetHandlers(node_name_pattern, instance_name_pattern,
     "/2/nodes": rlib2.R_2_nodes,
     re.compile(r"^/2/nodes/(%s)$" % node_name_pattern):
       rlib2.R_2_nodes_name,
+    re.compile(r"^/2/nodes/(%s)/powercycle$" % node_name_pattern):
+      rlib2.R_2_nodes_name_powercycle,
     re.compile(r"^/2/nodes/(%s)/tags$" % node_name_pattern):
       rlib2.R_2_nodes_name_tags,
     re.compile(r"^/2/nodes/(%s)/role$" % node_name_pattern):
diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py
index 0f3f008e4..1ecf732a1 100644
--- a/lib/rapi/rlib2.py
+++ b/lib/rapi/rlib2.py
@@ -397,6 +397,22 @@ class R_2_nodes_name(baserlib.OpcodeResource):
     return baserlib.MapFields(N_FIELDS, result[0])
 
 
+class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
+  """/2/nodes/[node_name]/powercycle resource.
+
+  """
+  POST_OPCODE = opcodes.OpNodePowercycle
+
+  def GetPostOpInput(self):
+    """Tries to powercycle a node.
+
+    """
+    return (self.request_body, {
+      "node_name": self.items[0],
+      "force": self.useForce(),
+      })
+
+
 class R_2_nodes_name_role(baserlib.OpcodeResource):
   """/2/nodes/[node_name]/role resource.
 
diff --git a/test/docs_unittest.py b/test/docs_unittest.py
index a695280a7..0db111f2f 100755
--- a/test/docs_unittest.py
+++ b/test/docs_unittest.py
@@ -53,7 +53,6 @@ RAPI_OPCODE_EXCLUDE = frozenset([
   opcodes.OpClusterVerifyDisks,
   opcodes.OpInstanceChangeGroup,
   opcodes.OpInstanceMove,
-  opcodes.OpNodePowercycle,
   opcodes.OpNodeQueryvols,
   opcodes.OpOobCommand,
   opcodes.OpTagsSearch,
diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py
index 81d6ea6da..758490c22 100755
--- a/test/ganeti.rapi.client_unittest.py
+++ b/test/ganeti.rapi.client_unittest.py
@@ -973,6 +973,16 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertQuery("force", ["1"])
     self.assertEqual("\"master-candidate\"", self.rapi.GetLastRequestData())
 
+  def testPowercycleNode(self):
+    self.rapi.AddResponse("23051")
+    self.assertEqual(23051,
+        self.client.PowercycleNode("node5468", force=True))
+    self.assertHandler(rlib2.R_2_nodes_name_powercycle)
+    self.assertItems(["node5468"])
+    self.assertQuery("force", ["1"])
+    self.assertFalse(self.rapi.GetLastRequestData())
+    self.assertEqual(self.rapi.CountPending(), 0)
+
   def testGetNodeStorageUnits(self):
     self.rapi.AddResponse("42")
     self.assertEqual(42,
diff --git a/test/ganeti.rapi.rlib2_unittest.py b/test/ganeti.rapi.rlib2_unittest.py
index 74176adc1..88a20a0aa 100755
--- a/test/ganeti.rapi.rlib2_unittest.py
+++ b/test/ganeti.rapi.rlib2_unittest.py
@@ -272,6 +272,26 @@ class TestNodeEvacuate(unittest.TestCase):
     self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
 
+class TestNodePowercycle(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_nodes_name_powercycle, ["node20744"], {
+      "force": ["1"],
+      }, None, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpNodePowercycle))
+    self.assertEqual(op.node_name, "node20744")
+    self.assertTrue(op.force)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
 class TestGroupAssignNodes(unittest.TestCase):
   def test(self):
     clfactory = _FakeClientFactory(_FakeClient)
-- 
GitLab