Statistics
| Branch: | Tag: | Revision:

root / test / py / ganeti.hypervisor.hv_kvm_unittest.py @ ea2bcb82

History | View | Annotate | Download (11.8 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2010, 2011 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Script for testing the hypervisor.hv_kvm module"""
23

    
24
import threading
25
import tempfile
26
import unittest
27
import socket
28
import os
29
import struct
30

    
31
from ganeti import serializer
32
from ganeti import constants
33
from ganeti import compat
34
from ganeti import objects
35
from ganeti import errors
36
from ganeti import utils
37
from ganeti import pathutils
38

    
39
from ganeti.hypervisor import hv_kvm
40

    
41
import testutils
42

    
43

    
44
class QmpStub(threading.Thread):
45
  """Stub for a QMP endpoint for a KVM instance
46

47
  """
48
  _QMP_BANNER_DATA = {
49
    "QMP": {
50
      "version": {
51
        "package": "",
52
        "qemu": {
53
          "micro": 50,
54
          "minor": 13,
55
          "major": 0,
56
          },
57
        "capabilities": [],
58
        },
59
      }
60
    }
61
  _EMPTY_RESPONSE = {
62
    "return": [],
63
    }
64

    
65
  def __init__(self, socket_filename, server_responses):
66
    """Creates a QMP stub
67

68
    @type socket_filename: string
69
    @param socket_filename: filename of the UNIX socket that will be created
70
                            this class and used for the communication
71
    @type server_responses: list
72
    @param server_responses: list of responses that the server sends in response
73
                             to whatever it receives
74
    """
75
    threading.Thread.__init__(self)
76
    self.socket_filename = socket_filename
77
    self.script = server_responses
78

    
79
    self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
80
    self.socket.bind(self.socket_filename)
81
    self.socket.listen(1)
82

    
83
  def run(self):
84
    # Hypothesis: the messages we receive contain only a complete QMP message
85
    # encoded in JSON.
86
    conn, addr = self.socket.accept()
87

    
88
    # Send the banner as the first thing
89
    conn.send(self.encode_string(self._QMP_BANNER_DATA))
90

    
91
    # Expect qmp_capabilities and return an empty response
92
    conn.recv(4096)
93
    conn.send(self.encode_string(self._EMPTY_RESPONSE))
94

    
95
    while True:
96
      # We ignore the expected message, as the purpose of this object is not
97
      # to verify the correctness of the communication but to act as a
98
      # partner for the SUT (System Under Test, that is QmpConnection)
99
      msg = conn.recv(4096)
100
      if not msg:
101
        break
102

    
103
      if not self.script:
104
        break
105
      response = self.script.pop(0)
106
      if isinstance(response, str):
107
        conn.send(response)
108
      elif isinstance(response, list):
109
        for chunk in response:
110
          conn.send(chunk)
111
      else:
112
        raise errors.ProgrammerError("Unknown response type for %s" % response)
113

    
114
    conn.close()
115

    
116
  def encode_string(self, message):
117
    return (serializer.DumpJson(message) +
118
            hv_kvm.QmpConnection._MESSAGE_END_TOKEN)
119

    
120

    
121
class TestQmpMessage(testutils.GanetiTestCase):
122
  def testSerialization(self):
123
    test_data = {
124
      "execute": "command",
125
      "arguments": ["a", "b", "c"],
126
      }
127
    message = hv_kvm.QmpMessage(test_data)
128

    
129
    for k, v in test_data.items():
130
      self.assertEqual(message[k], v)
131

    
132
    serialized = str(message)
133
    self.assertEqual(len(serialized.splitlines()), 1,
134
                     msg="Got multi-line message")
135

    
136
    rebuilt_message = hv_kvm.QmpMessage.BuildFromJsonString(serialized)
137
    self.assertEqual(rebuilt_message, message)
138

    
139

    
140
class TestQmp(testutils.GanetiTestCase):
141
  def testQmp(self):
142
    requests = [
143
      {"execute": "query-kvm", "arguments": []},
144
      {"execute": "eject", "arguments": {"device": "ide1-cd0"}},
145
      {"execute": "query-status", "arguments": []},
146
      {"execute": "query-name", "arguments": []},
147
      ]
148

    
149
    server_responses = [
150
      # One message, one send()
151
      '{"return": {"enabled": true, "present": true}}\r\n',
152

    
153
      # Message sent using multiple send()
154
      ['{"retur', 'n": {}}\r\n'],
155

    
156
      # Multiple messages sent using one send()
157
      '{"return": [{"name": "quit"}, {"name": "eject"}]}\r\n'
158
      '{"return": {"running": true, "singlestep": false}}\r\n',
159
      ]
160

    
161
    expected_responses = [
162
      {"return": {"enabled": True, "present": True}},
163
      {"return": {}},
164
      {"return": [{"name": "quit"}, {"name": "eject"}]},
165
      {"return": {"running": True, "singlestep": False}},
166
      ]
167

    
168
    # Set up the stub
169
    socket_file = tempfile.NamedTemporaryFile()
170
    os.remove(socket_file.name)
171
    qmp_stub = QmpStub(socket_file.name, server_responses)
172
    qmp_stub.start()
173

    
174
    # Set up the QMP connection
175
    qmp_connection = hv_kvm.QmpConnection(socket_file.name)
176
    qmp_connection.connect()
177

    
178
    # Format the script
179
    for request, expected_response in zip(requests, expected_responses):
180
      response = qmp_connection.Execute(request)
181
      msg = hv_kvm.QmpMessage(expected_response)
182
      self.assertEqual(len(str(msg).splitlines()), 1,
183
                       msg="Got multi-line message")
184
      self.assertEqual(response, msg)
185

    
186

    
187
class TestConsole(unittest.TestCase):
188
  def _Test(self, instance, hvparams):
189
    cons = hv_kvm.KVMHypervisor.GetInstanceConsole(instance, hvparams, {})
190
    self.assertTrue(cons.Validate())
191
    return cons
192

    
193
  def testSerial(self):
194
    instance = objects.Instance(name="kvm.example.com",
195
                                primary_node="node6017")
196
    hvparams = {
197
      constants.HV_SERIAL_CONSOLE: True,
198
      constants.HV_VNC_BIND_ADDRESS: None,
199
      constants.HV_KVM_SPICE_BIND: None,
200
      }
201
    cons = self._Test(instance, hvparams)
202
    self.assertEqual(cons.kind, constants.CONS_SSH)
203
    self.assertEqual(cons.host, instance.primary_node)
204
    self.assertEqual(cons.command[0], pathutils.KVM_CONSOLE_WRAPPER)
205
    self.assertEqual(cons.command[1], constants.SOCAT_PATH)
206

    
207
  def testVnc(self):
208
    instance = objects.Instance(name="kvm.example.com",
209
                                primary_node="node7235",
210
                                network_port=constants.VNC_BASE_PORT + 10)
211
    hvparams = {
212
      constants.HV_SERIAL_CONSOLE: False,
213
      constants.HV_VNC_BIND_ADDRESS: "192.0.2.1",
214
      constants.HV_KVM_SPICE_BIND: None,
215
      }
216
    cons = self._Test(instance, hvparams)
217
    self.assertEqual(cons.kind, constants.CONS_VNC)
218
    self.assertEqual(cons.host, "192.0.2.1")
219
    self.assertEqual(cons.port, constants.VNC_BASE_PORT + 10)
220
    self.assertEqual(cons.display, 10)
221

    
222
  def testSpice(self):
223
    instance = objects.Instance(name="kvm.example.com",
224
                                primary_node="node7235",
225
                                network_port=11000)
226
    hvparams = {
227
      constants.HV_SERIAL_CONSOLE: False,
228
      constants.HV_VNC_BIND_ADDRESS: None,
229
      constants.HV_KVM_SPICE_BIND: "192.0.2.1",
230
      }
231
    cons = self._Test(instance, hvparams)
232
    self.assertEqual(cons.kind, constants.CONS_SPICE)
233
    self.assertEqual(cons.host, "192.0.2.1")
234
    self.assertEqual(cons.port, 11000)
235

    
236
  def testNoConsole(self):
237
    instance = objects.Instance(name="kvm.example.com",
238
                                primary_node="node24325",
239
                                network_port=0)
240
    hvparams = {
241
      constants.HV_SERIAL_CONSOLE: False,
242
      constants.HV_VNC_BIND_ADDRESS: None,
243
      constants.HV_KVM_SPICE_BIND: None,
244
      }
245
    cons = self._Test(instance, hvparams)
246
    self.assertEqual(cons.kind, constants.CONS_MESSAGE)
247

    
248

    
249
class TestVersionChecking(testutils.GanetiTestCase):
250
  def testParseVersion(self):
251
    parse = hv_kvm.KVMHypervisor._ParseKVMVersion
252
    help_112 = testutils.ReadTestData("kvm_1.1.2_help.txt")
253
    help_10 = testutils.ReadTestData("kvm_1.0_help.txt")
254
    help_01590 = testutils.ReadTestData("kvm_0.15.90_help.txt")
255
    help_0125 = testutils.ReadTestData("kvm_0.12.5_help.txt")
256
    help_091 = testutils.ReadTestData("kvm_0.9.1_help.txt")
257
    self.assertEqual(parse(help_112), ("1.1.2", 1, 1, 2))
258
    self.assertEqual(parse(help_10), ("1.0", 1, 0, 0))
259
    self.assertEqual(parse(help_01590), ("0.15.90", 0, 15, 90))
260
    self.assertEqual(parse(help_0125), ("0.12.5", 0, 12, 5))
261
    self.assertEqual(parse(help_091), ("0.9.1", 0, 9, 1))
262

    
263

    
264
class TestSpiceParameterList(unittest.TestCase):
265
  def test(self):
266
    defaults = constants.HVC_DEFAULTS[constants.HT_KVM]
267

    
268
    params = \
269
      compat.UniqueFrozenset(getattr(constants, name)
270
                             for name in dir(constants)
271
                             if name.startswith("HV_KVM_SPICE_"))
272

    
273
    # Parameters whose default value evaluates to True and don't need to be set
274
    defaults_true = frozenset(filter(defaults.__getitem__, params))
275

    
276
    self.assertEqual(defaults_true, frozenset([
277
      constants.HV_KVM_SPICE_AUDIO_COMPR,
278
      constants.HV_KVM_SPICE_USE_VDAGENT,
279
      constants.HV_KVM_SPICE_TLS_CIPHERS,
280
      ]))
281

    
282
    # HV_KVM_SPICE_BIND decides whether the other parameters must be set if
283
    # their default evaluates to False
284
    assert constants.HV_KVM_SPICE_BIND in params
285
    assert constants.HV_KVM_SPICE_BIND not in defaults_true
286

    
287
    # Exclude some parameters
288
    params -= defaults_true | frozenset([
289
      constants.HV_KVM_SPICE_BIND,
290
      ])
291

    
292
    self.assertEqual(hv_kvm._SPICE_ADDITIONAL_PARAMS, params)
293

    
294

    
295
class TestHelpRegexps(testutils.GanetiTestCase):
296
  def testBootRe(self):
297
    """Check _BOOT_RE
298

299
    It has too match -drive.*boot=on|off except if there is another dash-option
300
    at the beginning of the line.
301

302
    """
303
    boot_re = hv_kvm.KVMHypervisor._BOOT_RE
304
    help_112 = testutils.ReadTestData("kvm_1.1.2_help.txt")
305
    help_10 = testutils.ReadTestData("kvm_1.0_help.txt")
306
    help_01590 = testutils.ReadTestData("kvm_0.15.90_help.txt")
307
    help_0125 = testutils.ReadTestData("kvm_0.12.5_help.txt")
308
    help_091 = testutils.ReadTestData("kvm_0.9.1_help.txt")
309
    help_091_fake = testutils.ReadTestData("kvm_0.9.1_help_boot_test.txt")
310

    
311
    self.assertTrue(boot_re.search(help_091))
312
    self.assertTrue(boot_re.search(help_0125))
313
    self.assertFalse(boot_re.search(help_091_fake))
314
    self.assertFalse(boot_re.search(help_112))
315
    self.assertFalse(boot_re.search(help_10))
316
    self.assertFalse(boot_re.search(help_01590))
317

    
318

    
319
class TestGetTunFeatures(unittest.TestCase):
320
  def testWrongIoctl(self):
321
    tmpfile = tempfile.NamedTemporaryFile()
322
    # A file does not have the right ioctls, so this must always fail
323
    result = hv_kvm._GetTunFeatures(tmpfile.fileno())
324
    self.assertTrue(result is None)
325

    
326
  def _FakeIoctl(self, features, fd, request, buf):
327
    self.assertEqual(request, hv_kvm.TUNGETFEATURES)
328

    
329
    (reqno, ) = struct.unpack("I", buf)
330
    self.assertEqual(reqno, 0)
331

    
332
    return struct.pack("I", features)
333

    
334
  def test(self):
335
    tmpfile = tempfile.NamedTemporaryFile()
336
    fd = tmpfile.fileno()
337

    
338
    for features in [0, hv_kvm.IFF_VNET_HDR]:
339
      fn = compat.partial(self._FakeIoctl, features)
340
      result = hv_kvm._GetTunFeatures(fd, _ioctl=fn)
341
      self.assertEqual(result, features)
342

    
343

    
344
class TestProbeTapVnetHdr(unittest.TestCase):
345
  def _FakeTunFeatures(self, expected_fd, flags, fd):
346
    self.assertEqual(fd, expected_fd)
347
    return flags
348

    
349
  def test(self):
350
    tmpfile = tempfile.NamedTemporaryFile()
351
    fd = tmpfile.fileno()
352

    
353
    for flags in [0, hv_kvm.IFF_VNET_HDR]:
354
      fn = compat.partial(self._FakeTunFeatures, fd, flags)
355

    
356
      result = hv_kvm._ProbeTapVnetHdr(fd, _features_fn=fn)
357
      if flags == 0:
358
        self.assertFalse(result)
359
      else:
360
        self.assertTrue(result)
361

    
362
  def testUnsupported(self):
363
    tmpfile = tempfile.NamedTemporaryFile()
364
    fd = tmpfile.fileno()
365

    
366
    self.assertFalse(hv_kvm._ProbeTapVnetHdr(fd, _features_fn=lambda _: None))
367

    
368

    
369
if __name__ == "__main__":
370
  testutils.GanetiTestProgram()