Statistics
| Branch: | Tag: | Revision:

root / test / py / ganeti.hypervisor.hv_kvm_unittest.py @ 560ef132

History | View | Annotate | Download (14.4 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
import re
31

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

    
40
from ganeti.hypervisor import hv_kvm
41

    
42
import testutils
43

    
44

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

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

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

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

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

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

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

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

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

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

    
115
    conn.close()
116

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

    
121

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

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

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

    
137
    rebuilt_message = hv_kvm.QmpMessage.BuildFromJsonString(serialized)
138
    self.assertEqual(rebuilt_message, message)
139
    self.assertEqual(len(rebuilt_message), len(test_data))
140

    
141
  def testDelete(self):
142
    toDelete = "execute"
143
    test_data = {
144
      toDelete: "command",
145
      "arguments": ["a", "b", "c"],
146
      }
147
    message = hv_kvm.QmpMessage(test_data)
148

    
149
    oldLen = len(message)
150
    del(message[toDelete])
151
    newLen = len(message)
152
    self.assertEqual(oldLen - 1, newLen)
153

    
154

    
155
class TestQmp(testutils.GanetiTestCase):
156
  def testQmp(self):
157
    requests = [
158
      {"execute": "query-kvm", "arguments": []},
159
      {"execute": "eject", "arguments": {"device": "ide1-cd0"}},
160
      {"execute": "query-status", "arguments": []},
161
      {"execute": "query-name", "arguments": []},
162
      ]
163

    
164
    server_responses = [
165
      # One message, one send()
166
      '{"return": {"enabled": true, "present": true}}\r\n',
167

    
168
      # Message sent using multiple send()
169
      ['{"retur', 'n": {}}\r\n'],
170

    
171
      # Multiple messages sent using one send()
172
      '{"return": [{"name": "quit"}, {"name": "eject"}]}\r\n'
173
      '{"return": {"running": true, "singlestep": false}}\r\n',
174
      ]
175

    
176
    expected_responses = [
177
      {"return": {"enabled": True, "present": True}},
178
      {"return": {}},
179
      {"return": [{"name": "quit"}, {"name": "eject"}]},
180
      {"return": {"running": True, "singlestep": False}},
181
      ]
182

    
183
    # Set up the stub
184
    socket_file = tempfile.NamedTemporaryFile()
185
    os.remove(socket_file.name)
186
    qmp_stub = QmpStub(socket_file.name, server_responses)
187
    qmp_stub.start()
188

    
189
    # Set up the QMP connection
190
    qmp_connection = hv_kvm.QmpConnection(socket_file.name)
191
    qmp_connection.connect()
192

    
193
    # Format the script
194
    for request, expected_response in zip(requests, expected_responses):
195
      response = qmp_connection.Execute(request)
196
      msg = hv_kvm.QmpMessage(expected_response)
197
      self.assertEqual(len(str(msg).splitlines()), 1,
198
                       msg="Got multi-line message")
199
      self.assertEqual(response, msg)
200

    
201

    
202
class TestConsole(unittest.TestCase):
203
  def _Test(self, instance, node, group, hvparams):
204
    cons = hv_kvm.KVMHypervisor.GetInstanceConsole(instance, node, group,
205
                                                   hvparams, {})
206
    self.assertTrue(cons.Validate())
207
    return cons
208

    
209
  def testSerial(self):
210
    instance = objects.Instance(name="kvm.example.com",
211
                                primary_node="node6017-uuid")
212
    node = objects.Node(name="node6017", uuid="node6017-uuid",
213
                        ndparams={})
214
    group = objects.NodeGroup(name="group6134", ndparams={})
215
    hvparams = {
216
      constants.HV_SERIAL_CONSOLE: True,
217
      constants.HV_VNC_BIND_ADDRESS: None,
218
      constants.HV_KVM_SPICE_BIND: None,
219
      }
220
    cons = self._Test(instance, node, group, hvparams)
221
    self.assertEqual(cons.kind, constants.CONS_SSH)
222
    self.assertEqual(cons.host, node.name)
223
    self.assertEqual(cons.command[0], pathutils.KVM_CONSOLE_WRAPPER)
224
    self.assertEqual(cons.command[1], constants.SOCAT_PATH)
225

    
226
  def testVnc(self):
227
    instance = objects.Instance(name="kvm.example.com",
228
                                primary_node="node7235-uuid",
229
                                network_port=constants.VNC_BASE_PORT + 10)
230
    node = objects.Node(name="node7235", uuid="node7235-uuid",
231
                        ndparams={})
232
    group = objects.NodeGroup(name="group3632", ndparams={})
233
    hvparams = {
234
      constants.HV_SERIAL_CONSOLE: False,
235
      constants.HV_VNC_BIND_ADDRESS: "192.0.2.1",
236
      constants.HV_KVM_SPICE_BIND: None,
237
      }
238
    cons = self._Test(instance, node, group, hvparams)
239
    self.assertEqual(cons.kind, constants.CONS_VNC)
240
    self.assertEqual(cons.host, "192.0.2.1")
241
    self.assertEqual(cons.port, constants.VNC_BASE_PORT + 10)
242
    self.assertEqual(cons.display, 10)
243

    
244
  def testSpice(self):
245
    instance = objects.Instance(name="kvm.example.com",
246
                                primary_node="node7235",
247
                                network_port=11000)
248
    node = objects.Node(name="node7235", uuid="node7235-uuid",
249
                        ndparams={})
250
    group = objects.NodeGroup(name="group0132", ndparams={})
251
    hvparams = {
252
      constants.HV_SERIAL_CONSOLE: False,
253
      constants.HV_VNC_BIND_ADDRESS: None,
254
      constants.HV_KVM_SPICE_BIND: "192.0.2.1",
255
      }
256
    cons = self._Test(instance, node, group, hvparams)
257
    self.assertEqual(cons.kind, constants.CONS_SPICE)
258
    self.assertEqual(cons.host, "192.0.2.1")
259
    self.assertEqual(cons.port, 11000)
260

    
261
  def testNoConsole(self):
262
    instance = objects.Instance(name="kvm.example.com",
263
                                primary_node="node24325",
264
                                network_port=0)
265
    node = objects.Node(name="node24325", uuid="node24325-uuid",
266
                        ndparams={})
267
    group = objects.NodeGroup(name="group9184", ndparams={})
268
    hvparams = {
269
      constants.HV_SERIAL_CONSOLE: False,
270
      constants.HV_VNC_BIND_ADDRESS: None,
271
      constants.HV_KVM_SPICE_BIND: None,
272
      }
273
    cons = self._Test(instance, node, group, hvparams)
274
    self.assertEqual(cons.kind, constants.CONS_MESSAGE)
275

    
276

    
277
class TestVersionChecking(testutils.GanetiTestCase):
278
  def testParseVersion(self):
279
    parse = hv_kvm.KVMHypervisor._ParseKVMVersion
280
    help_112 = testutils.ReadTestData("kvm_1.1.2_help.txt")
281
    help_10 = testutils.ReadTestData("kvm_1.0_help.txt")
282
    help_01590 = testutils.ReadTestData("kvm_0.15.90_help.txt")
283
    help_0125 = testutils.ReadTestData("kvm_0.12.5_help.txt")
284
    help_091 = testutils.ReadTestData("kvm_0.9.1_help.txt")
285
    self.assertEqual(parse(help_112), ("1.1.2", 1, 1, 2))
286
    self.assertEqual(parse(help_10), ("1.0", 1, 0, 0))
287
    self.assertEqual(parse(help_01590), ("0.15.90", 0, 15, 90))
288
    self.assertEqual(parse(help_0125), ("0.12.5", 0, 12, 5))
289
    self.assertEqual(parse(help_091), ("0.9.1", 0, 9, 1))
290

    
291

    
292
class TestSpiceParameterList(unittest.TestCase):
293
  def test(self):
294
    defaults = constants.HVC_DEFAULTS[constants.HT_KVM]
295

    
296
    params = \
297
      compat.UniqueFrozenset(getattr(constants, name)
298
                             for name in dir(constants)
299
                             if name.startswith("HV_KVM_SPICE_"))
300

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

    
304
    self.assertEqual(defaults_true, frozenset([
305
      constants.HV_KVM_SPICE_AUDIO_COMPR,
306
      constants.HV_KVM_SPICE_USE_VDAGENT,
307
      constants.HV_KVM_SPICE_TLS_CIPHERS,
308
      ]))
309

    
310
    # HV_KVM_SPICE_BIND decides whether the other parameters must be set if
311
    # their default evaluates to False
312
    assert constants.HV_KVM_SPICE_BIND in params
313
    assert constants.HV_KVM_SPICE_BIND not in defaults_true
314

    
315
    # Exclude some parameters
316
    params -= defaults_true | frozenset([
317
      constants.HV_KVM_SPICE_BIND,
318
      ])
319

    
320
    self.assertEqual(hv_kvm._SPICE_ADDITIONAL_PARAMS, params)
321

    
322

    
323
class TestHelpRegexps(testutils.GanetiTestCase):
324
  def testBootRe(self):
325
    """Check _BOOT_RE
326

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

330
    """
331
    boot_re = hv_kvm.KVMHypervisor._BOOT_RE
332
    help_112 = testutils.ReadTestData("kvm_1.1.2_help.txt")
333
    help_10 = testutils.ReadTestData("kvm_1.0_help.txt")
334
    help_01590 = testutils.ReadTestData("kvm_0.15.90_help.txt")
335
    help_0125 = testutils.ReadTestData("kvm_0.12.5_help.txt")
336
    help_091 = testutils.ReadTestData("kvm_0.9.1_help.txt")
337
    help_091_fake = testutils.ReadTestData("kvm_0.9.1_help_boot_test.txt")
338

    
339
    self.assertTrue(boot_re.search(help_091))
340
    self.assertTrue(boot_re.search(help_0125))
341
    self.assertFalse(boot_re.search(help_091_fake))
342
    self.assertFalse(boot_re.search(help_112))
343
    self.assertFalse(boot_re.search(help_10))
344
    self.assertFalse(boot_re.search(help_01590))
345

    
346

    
347
class TestGetTunFeatures(unittest.TestCase):
348
  def testWrongIoctl(self):
349
    tmpfile = tempfile.NamedTemporaryFile()
350
    # A file does not have the right ioctls, so this must always fail
351
    result = hv_kvm._GetTunFeatures(tmpfile.fileno())
352
    self.assertTrue(result is None)
353

    
354
  def _FakeIoctl(self, features, fd, request, buf):
355
    self.assertEqual(request, hv_kvm.TUNGETFEATURES)
356

    
357
    (reqno, ) = struct.unpack("I", buf)
358
    self.assertEqual(reqno, 0)
359

    
360
    return struct.pack("I", features)
361

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

    
366
    for features in [0, hv_kvm.IFF_VNET_HDR]:
367
      fn = compat.partial(self._FakeIoctl, features)
368
      result = hv_kvm._GetTunFeatures(fd, _ioctl=fn)
369
      self.assertEqual(result, features)
370

    
371

    
372
class TestProbeTapVnetHdr(unittest.TestCase):
373
  def _FakeTunFeatures(self, expected_fd, flags, fd):
374
    self.assertEqual(fd, expected_fd)
375
    return flags
376

    
377
  def test(self):
378
    tmpfile = tempfile.NamedTemporaryFile()
379
    fd = tmpfile.fileno()
380

    
381
    for flags in [0, hv_kvm.IFF_VNET_HDR]:
382
      fn = compat.partial(self._FakeTunFeatures, fd, flags)
383

    
384
      result = hv_kvm._ProbeTapVnetHdr(fd, _features_fn=fn)
385
      if flags == 0:
386
        self.assertFalse(result)
387
      else:
388
        self.assertTrue(result)
389

    
390
  def testUnsupported(self):
391
    tmpfile = tempfile.NamedTemporaryFile()
392
    fd = tmpfile.fileno()
393

    
394
    self.assertFalse(hv_kvm._ProbeTapVnetHdr(fd, _features_fn=lambda _: None))
395

    
396

    
397
class TestGenerateDeviceKVMId(unittest.TestCase):
398
  def test(self):
399
    device = objects.NIC()
400
    target = constants.HOTPLUG_TARGET_NIC
401
    fn = hv_kvm._GenerateDeviceKVMId
402
    self.assertRaises(errors.HotplugError, fn, target, device)
403

    
404
    device.pci = 5
405
    device.uuid = "003fc157-66a8-4e6d-8b7e-ec4f69751396"
406
    self.assertTrue(re.match("hotnic-003fc157-pci-5", fn(target, device)))
407

    
408

    
409
class TestGetRuntimeInfo(unittest.TestCase):
410
  @classmethod
411
  def _GetRuntime(cls):
412
    data = testutils.ReadTestData("kvm_runtime.json")
413
    return hv_kvm._AnalyzeSerializedRuntime(data)
414

    
415
  def _fail(self, target, device, runtime):
416
    device.uuid = "aaaaaaaa-66a8-4e6d-8b7e-ec4f69751396"
417
    self.assertRaises(errors.HotplugError,
418
                      hv_kvm._GetExistingDeviceInfo,
419
                      target, device, runtime)
420

    
421
  def testNIC(self):
422
    device = objects.NIC()
423
    target = constants.HOTPLUG_TARGET_NIC
424
    runtime = self._GetRuntime()
425

    
426
    self._fail(target, device, runtime)
427

    
428
    device.uuid = "003fc157-66a8-4e6d-8b7e-ec4f69751396"
429
    devinfo = hv_kvm._GetExistingDeviceInfo(target, device, runtime)
430
    self.assertTrue(devinfo.pci==6)
431

    
432
  def testDisk(self):
433
    device = objects.Disk()
434
    target = constants.HOTPLUG_TARGET_DISK
435
    runtime = self._GetRuntime()
436

    
437
    self._fail(target, device, runtime)
438

    
439
    device.uuid = "9f5c5bd4-6f60-480b-acdc-9bb1a4b7df79"
440
    (devinfo, _, __) = hv_kvm._GetExistingDeviceInfo(target, device, runtime)
441
    self.assertTrue(devinfo.pci==5)
442

    
443

    
444
if __name__ == "__main__":
445
  testutils.GanetiTestProgram()