locking: Change locking order, move NAL after instances
[ganeti-local] / test / ganeti.hypervisor.hv_kvm_unittest.py
index 20c8489..230efff 100755 (executable)
 
 """Script for testing the hypervisor.hv_kvm module"""
 
 
 """Script for testing the hypervisor.hv_kvm module"""
 
+import threading
+import tempfile
 import unittest
 import unittest
+import socket
+import os
 
 
+from ganeti import serializer
 from ganeti import constants
 from ganeti import compat
 from ganeti import objects
 from ganeti import errors
 from ganeti import utils
 from ganeti import constants
 from ganeti import compat
 from ganeti import objects
 from ganeti import errors
 from ganeti import utils
+from ganeti import pathutils
 
 from ganeti.hypervisor import hv_kvm
 
 import testutils
 
 
 
 from ganeti.hypervisor import hv_kvm
 
 import testutils
 
 
+class QmpStub(threading.Thread):
+  """Stub for a QMP endpoint for a KVM instance
+
+  """
+  _QMP_BANNER_DATA = {
+    "QMP": {
+      "version": {
+        "package": "",
+        "qemu": {
+          "micro": 50,
+          "minor": 13,
+          "major": 0,
+          },
+        "capabilities": [],
+        },
+      }
+    }
+  _EMPTY_RESPONSE = {
+    "return": [],
+    }
+
+  def __init__(self, socket_filename, server_responses):
+    """Creates a QMP stub
+
+    @type socket_filename: string
+    @param socket_filename: filename of the UNIX socket that will be created
+                            this class and used for the communication
+    @type server_responses: list
+    @param server_responses: list of responses that the server sends in response
+                             to whatever it receives
+    """
+    threading.Thread.__init__(self)
+    self.socket_filename = socket_filename
+    self.script = server_responses
+
+    self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+    self.socket.bind(self.socket_filename)
+    self.socket.listen(1)
+
+  def run(self):
+    # Hypothesis: the messages we receive contain only a complete QMP message
+    # encoded in JSON.
+    conn, addr = self.socket.accept()
+
+    # Send the banner as the first thing
+    conn.send(self.encode_string(self._QMP_BANNER_DATA))
+
+    # Expect qmp_capabilities and return an empty response
+    conn.recv(4096)
+    conn.send(self.encode_string(self._EMPTY_RESPONSE))
+
+    while True:
+      # We ignore the expected message, as the purpose of this object is not
+      # to verify the correctness of the communication but to act as a
+      # partner for the SUT (System Under Test, that is QmpConnection)
+      msg = conn.recv(4096)
+      if not msg:
+        break
+
+      if not self.script:
+        break
+      response = self.script.pop(0)
+      if isinstance(response, str):
+        conn.send(response)
+      elif isinstance(response, list):
+        for chunk in response:
+          conn.send(chunk)
+      else:
+        raise errors.ProgrammerError("Unknown response type for %s" % response)
+
+    conn.close()
+
+  def encode_string(self, message):
+    return (serializer.DumpJson(message) +
+            hv_kvm.QmpConnection._MESSAGE_END_TOKEN)
+
+
+class TestQmpMessage(testutils.GanetiTestCase):
+  def testSerialization(self):
+    test_data = {
+      "execute": "command",
+      "arguments": ["a", "b", "c"],
+      }
+    message = hv_kvm.QmpMessage(test_data)
+
+    for k, v in test_data.items():
+      self.assertEqual(message[k], v)
+
+    serialized = str(message)
+    self.assertEqual(len(serialized.splitlines()), 1,
+                     msg="Got multi-line message")
+
+    rebuilt_message = hv_kvm.QmpMessage.BuildFromJsonString(serialized)
+    self.assertEqual(rebuilt_message, message)
+
+
+class TestQmp(testutils.GanetiTestCase):
+  def testQmp(self):
+    requests = [
+      {"execute": "query-kvm", "arguments": []},
+      {"execute": "eject", "arguments": {"device": "ide1-cd0"}},
+      {"execute": "query-status", "arguments": []},
+      {"execute": "query-name", "arguments": []},
+      ]
+
+    server_responses = [
+      # One message, one send()
+      '{"return": {"enabled": true, "present": true}}\r\n',
+
+      # Message sent using multiple send()
+      ['{"retur', 'n": {}}\r\n'],
+
+      # Multiple messages sent using one send()
+      '{"return": [{"name": "quit"}, {"name": "eject"}]}\r\n'
+      '{"return": {"running": true, "singlestep": false}}\r\n',
+      ]
+
+    expected_responses = [
+      {"return": {"enabled": True, "present": True}},
+      {"return": {}},
+      {"return": [{"name": "quit"}, {"name": "eject"}]},
+      {"return": {"running": True, "singlestep": False}},
+      ]
+
+    # Set up the stub
+    socket_file = tempfile.NamedTemporaryFile()
+    os.remove(socket_file.name)
+    qmp_stub = QmpStub(socket_file.name, server_responses)
+    qmp_stub.start()
+
+    # Set up the QMP connection
+    qmp_connection = hv_kvm.QmpConnection(socket_file.name)
+    qmp_connection.connect()
+
+    # Format the script
+    for request, expected_response in zip(requests, expected_responses):
+      response = qmp_connection.Execute(request)
+      msg = hv_kvm.QmpMessage(expected_response)
+      self.assertEqual(len(str(msg).splitlines()), 1,
+                       msg="Got multi-line message")
+      self.assertEqual(response, msg)
+
+
 class TestConsole(unittest.TestCase):
   def _Test(self, instance, hvparams):
     cons = hv_kvm.KVMHypervisor.GetInstanceConsole(instance, hvparams, {})
 class TestConsole(unittest.TestCase):
   def _Test(self, instance, hvparams):
     cons = hv_kvm.KVMHypervisor.GetInstanceConsole(instance, hvparams, {})
@@ -46,11 +195,12 @@ class TestConsole(unittest.TestCase):
     hvparams = {
       constants.HV_SERIAL_CONSOLE: True,
       constants.HV_VNC_BIND_ADDRESS: None,
     hvparams = {
       constants.HV_SERIAL_CONSOLE: True,
       constants.HV_VNC_BIND_ADDRESS: None,
+      constants.HV_KVM_SPICE_BIND: None,
       }
     cons = self._Test(instance, hvparams)
     self.assertEqual(cons.kind, constants.CONS_SSH)
     self.assertEqual(cons.host, instance.primary_node)
       }
     cons = self._Test(instance, hvparams)
     self.assertEqual(cons.kind, constants.CONS_SSH)
     self.assertEqual(cons.host, instance.primary_node)
-    self.assertEqual(cons.command[0], constants.KVM_CONSOLE_WRAPPER)
+    self.assertEqual(cons.command[0], pathutils.KVM_CONSOLE_WRAPPER)
     self.assertEqual(cons.command[1], constants.SOCAT_PATH)
 
   def testVnc(self):
     self.assertEqual(cons.command[1], constants.SOCAT_PATH)
 
   def testVnc(self):
@@ -60,6 +210,7 @@ class TestConsole(unittest.TestCase):
     hvparams = {
       constants.HV_SERIAL_CONSOLE: False,
       constants.HV_VNC_BIND_ADDRESS: "192.0.2.1",
     hvparams = {
       constants.HV_SERIAL_CONSOLE: False,
       constants.HV_VNC_BIND_ADDRESS: "192.0.2.1",
+      constants.HV_KVM_SPICE_BIND: None,
       }
     cons = self._Test(instance, hvparams)
     self.assertEqual(cons.kind, constants.CONS_VNC)
       }
     cons = self._Test(instance, hvparams)
     self.assertEqual(cons.kind, constants.CONS_VNC)
@@ -67,6 +218,20 @@ class TestConsole(unittest.TestCase):
     self.assertEqual(cons.port, constants.VNC_BASE_PORT + 10)
     self.assertEqual(cons.display, 10)
 
     self.assertEqual(cons.port, constants.VNC_BASE_PORT + 10)
     self.assertEqual(cons.display, 10)
 
+  def testSpice(self):
+    instance = objects.Instance(name="kvm.example.com",
+                                primary_node="node7235",
+                                network_port=11000)
+    hvparams = {
+      constants.HV_SERIAL_CONSOLE: False,
+      constants.HV_VNC_BIND_ADDRESS: None,
+      constants.HV_KVM_SPICE_BIND: "192.0.2.1",
+      }
+    cons = self._Test(instance, hvparams)
+    self.assertEqual(cons.kind, constants.CONS_SPICE)
+    self.assertEqual(cons.host, "192.0.2.1")
+    self.assertEqual(cons.port, 11000)
+
   def testNoConsole(self):
     instance = objects.Instance(name="kvm.example.com",
                                 primary_node="node24325",
   def testNoConsole(self):
     instance = objects.Instance(name="kvm.example.com",
                                 primary_node="node24325",
@@ -74,6 +239,7 @@ class TestConsole(unittest.TestCase):
     hvparams = {
       constants.HV_SERIAL_CONSOLE: False,
       constants.HV_VNC_BIND_ADDRESS: None,
     hvparams = {
       constants.HV_SERIAL_CONSOLE: False,
       constants.HV_VNC_BIND_ADDRESS: None,
+      constants.HV_KVM_SPICE_BIND: None,
       }
     cons = self._Test(instance, hvparams)
     self.assertEqual(cons.kind, constants.CONS_MESSAGE)
       }
     cons = self._Test(instance, hvparams)
     self.assertEqual(cons.kind, constants.CONS_MESSAGE)