Implement basic automatic KVM postcopy migration (#1262)
[ganeti-github.git] / test / py / ganeti.hypervisor.hv_kvm_unittest.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2010, 2011 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30
31 """Script for testing the hypervisor.hv_kvm module"""
32
33 import threading
34 import tempfile
35 import unittest
36 import socket
37 import os
38 import struct
39 import re
40 from contextlib import nested
41
42 from ganeti import serializer
43 from ganeti import constants
44 from ganeti import compat
45 from ganeti import objects
46 from ganeti import errors
47 from ganeti import utils
48 from ganeti import pathutils
49
50 from ganeti.hypervisor import hv_kvm
51 import ganeti.hypervisor.hv_kvm.netdev as netdev
52 import ganeti.hypervisor.hv_kvm.monitor as monitor
53
54 import mock
55 import testutils
56
57 from testutils.config_mock import ConfigMock
58
59
60 class QmpStub(threading.Thread):
61 """Stub for a QMP endpoint for a KVM instance
62
63 """
64 _QMP_BANNER_DATA = {
65 "QMP": {
66 "version": {
67 "package": "",
68 "qemu": {
69 "micro": 50,
70 "minor": 13,
71 "major": 0,
72 },
73 "capabilities": [],
74 },
75 }
76 }
77 _EMPTY_RESPONSE = {
78 "return": [],
79 }
80 _SUPPORTED_COMMANDS = {
81 "return": [
82 {"name": "command"},
83 {"name": "query-kvm"},
84 {"name": "eject"},
85 {"name": "query-status"},
86 {"name": "query-name"},
87 ]
88 }
89
90 def __init__(self, socket_filename, server_responses):
91 """Creates a QMP stub
92
93 @type socket_filename: string
94 @param socket_filename: filename of the UNIX socket that will be created
95 this class and used for the communication
96 @type server_responses: list
97 @param server_responses: list of responses that the server sends in response
98 to whatever it receives
99 """
100 threading.Thread.__init__(self)
101 self.socket_filename = socket_filename
102 self.script = server_responses[:]
103
104 self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
105 self.socket.bind(self.socket_filename)
106 self.socket.listen(1)
107
108 def run(self):
109 # Hypothesis: the messages we receive contain only a complete QMP message
110 # encoded in JSON.
111 conn, addr = self.socket.accept()
112
113 # Send the banner as the first thing
114 conn.send(self.encode_string(self._QMP_BANNER_DATA))
115
116 # Expect qmp_capabilities and return an empty response
117 conn.recv(4096)
118 conn.send(self.encode_string(self._EMPTY_RESPONSE))
119
120 # Expect query-commands and return the list of supported commands
121 conn.recv(4096)
122 conn.send(self.encode_string(self._SUPPORTED_COMMANDS))
123
124 while True:
125 # We ignore the expected message, as the purpose of this object is not
126 # to verify the correctness of the communication but to act as a
127 # partner for the SUT (System Under Test, that is QmpConnection)
128 msg = conn.recv(4096)
129 if not msg:
130 break
131
132 if not self.script:
133 break
134 response = self.script.pop(0)
135 if isinstance(response, str):
136 conn.send(response)
137 elif isinstance(response, list):
138 for chunk in response:
139 conn.send(chunk)
140 else:
141 raise errors.ProgrammerError("Unknown response type for %s" % response)
142
143 conn.close()
144
145 def encode_string(self, message):
146 return (serializer.DumpJson(message) +
147 hv_kvm.QmpConnection._MESSAGE_END_TOKEN)
148
149
150 class TestQmpMessage(testutils.GanetiTestCase):
151 def testSerialization(self):
152 test_data = {
153 "execute": "command",
154 "arguments": ["a", "b", "c"],
155 }
156 message = hv_kvm.QmpMessage(test_data)
157
158 for k, v in test_data.items():
159 self.assertEqual(message[k], v)
160
161 serialized = str(message)
162 self.assertEqual(len(serialized.splitlines()), 1,
163 msg="Got multi-line message")
164
165 rebuilt_message = hv_kvm.QmpMessage.BuildFromJsonString(serialized)
166 self.assertEqual(rebuilt_message, message)
167 self.assertEqual(len(rebuilt_message), len(test_data))
168
169 def testDelete(self):
170 toDelete = "execute"
171 test_data = {
172 toDelete: "command",
173 "arguments": ["a", "b", "c"],
174 }
175 message = hv_kvm.QmpMessage(test_data)
176
177 oldLen = len(message)
178 del message[toDelete]
179 newLen = len(message)
180 self.assertEqual(oldLen - 1, newLen)
181
182
183 class TestQmp(testutils.GanetiTestCase):
184 REQUESTS = [
185 {"execute": "query-kvm", "arguments": []},
186 {"execute": "eject", "arguments": {"device": "ide1-cd0"}},
187 {"execute": "query-status", "arguments": []},
188 {"execute": "query-name", "arguments": []},
189 ]
190
191 SERVER_RESPONSES = [
192 # One message, one send()
193 '{"return": {"enabled": true, "present": true}}\r\n',
194
195 # Message sent using multiple send()
196 ['{"retur', 'n": {}}\r\n'],
197
198 # Multiple messages sent using one send()
199 '{"return": [{"name": "quit"}, {"name": "eject"}]}\r\n'
200 '{"return": {"running": true, "singlestep": false}}\r\n',
201 ]
202
203 EXPECTED_RESPONSES = [
204 {"enabled": True, "present": True},
205 {},
206 [{"name": "quit"}, {"name": "eject"}],
207 {"running": True, "singlestep": False},
208 ]
209
210 def testQmp(self):
211 # Set up the stub
212 socket_file = tempfile.NamedTemporaryFile()
213 os.remove(socket_file.name)
214 qmp_stub = QmpStub(socket_file.name, self.SERVER_RESPONSES)
215 qmp_stub.start()
216
217 # Set up the QMP connection
218 qmp_connection = hv_kvm.QmpConnection(socket_file.name)
219 qmp_connection.connect()
220
221 # Format the script
222 for request, expected_response in zip(self.REQUESTS,
223 self.EXPECTED_RESPONSES):
224 response = qmp_connection.Execute(request["execute"],
225 request["arguments"])
226 self.assertEqual(response, expected_response)
227 msg = hv_kvm.QmpMessage({"return": expected_response})
228 self.assertEqual(len(str(msg).splitlines()), 1,
229 msg="Got multi-line message")
230
231 self.assertRaises(monitor.QmpCommandNotSupported,
232 qmp_connection.Execute,
233 "unsupported-command")
234
235 def testQmpContextManager(self):
236 # Set up the stub
237 socket_file = tempfile.NamedTemporaryFile()
238 os.remove(socket_file.name)
239 qmp_stub = QmpStub(socket_file.name, self.SERVER_RESPONSES)
240 qmp_stub.start()
241
242 # Test the context manager functionality
243 with hv_kvm.QmpConnection(socket_file.name) as qmp:
244 for request, expected_response in zip(self.REQUESTS,
245 self.EXPECTED_RESPONSES):
246 response = qmp.Execute(request["execute"], request["arguments"])
247 self.assertEqual(response, expected_response)
248
249
250 class TestConsole(unittest.TestCase):
251 def MakeConsole(self, instance, node, group, hvparams):
252 cons = hv_kvm.KVMHypervisor.GetInstanceConsole(instance, node, group,
253 hvparams, {})
254 self.assertEqual(cons.Validate(), None)
255 return cons
256
257 def testSerial(self):
258 instance = objects.Instance(name="kvm.example.com",
259 primary_node="node6017-uuid")
260 node = objects.Node(name="node6017", uuid="node6017-uuid",
261 ndparams={})
262 group = objects.NodeGroup(name="group6134", ndparams={})
263 hvparams = {
264 constants.HV_SERIAL_CONSOLE: True,
265 constants.HV_VNC_BIND_ADDRESS: None,
266 constants.HV_KVM_SPICE_BIND: None,
267 }
268 cons = self.MakeConsole(instance, node, group, hvparams)
269 self.assertEqual(cons.kind, constants.CONS_SSH)
270 self.assertEqual(cons.host, node.name)
271 self.assertEqual(cons.command[0], pathutils.KVM_CONSOLE_WRAPPER)
272 self.assertEqual(cons.command[1], constants.SOCAT_PATH)
273
274 def testVnc(self):
275 instance = objects.Instance(name="kvm.example.com",
276 primary_node="node7235-uuid",
277 network_port=constants.VNC_BASE_PORT + 10)
278 node = objects.Node(name="node7235", uuid="node7235-uuid",
279 ndparams={})
280 group = objects.NodeGroup(name="group3632", ndparams={})
281 hvparams = {
282 constants.HV_SERIAL_CONSOLE: False,
283 constants.HV_VNC_BIND_ADDRESS: "192.0.2.1",
284 constants.HV_KVM_SPICE_BIND: None,
285 }
286 cons = self.MakeConsole(instance, node, group, hvparams)
287 self.assertEqual(cons.kind, constants.CONS_VNC)
288 self.assertEqual(cons.host, "192.0.2.1")
289 self.assertEqual(cons.port, constants.VNC_BASE_PORT + 10)
290 self.assertEqual(cons.display, 10)
291
292 def testSpice(self):
293 instance = objects.Instance(name="kvm.example.com",
294 primary_node="node7235",
295 network_port=11000)
296 node = objects.Node(name="node7235", uuid="node7235-uuid",
297 ndparams={})
298 group = objects.NodeGroup(name="group0132", ndparams={})
299 hvparams = {
300 constants.HV_SERIAL_CONSOLE: False,
301 constants.HV_VNC_BIND_ADDRESS: None,
302 constants.HV_KVM_SPICE_BIND: "192.0.2.1",
303 }
304 cons = self.MakeConsole(instance, node, group, hvparams)
305 self.assertEqual(cons.kind, constants.CONS_SPICE)
306 self.assertEqual(cons.host, "192.0.2.1")
307 self.assertEqual(cons.port, 11000)
308
309 def testNoConsole(self):
310 instance = objects.Instance(name="kvm.example.com",
311 primary_node="node24325",
312 network_port=0)
313 node = objects.Node(name="node24325", uuid="node24325-uuid",
314 ndparams={})
315 group = objects.NodeGroup(name="group9184", ndparams={})
316 hvparams = {
317 constants.HV_SERIAL_CONSOLE: False,
318 constants.HV_VNC_BIND_ADDRESS: None,
319 constants.HV_KVM_SPICE_BIND: None,
320 }
321 cons = self.MakeConsole(instance, node, group, hvparams)
322 self.assertEqual(cons.kind, constants.CONS_MESSAGE)
323
324
325 class TestVersionChecking(testutils.GanetiTestCase):
326 @staticmethod
327 def ParseTestData(name):
328 help = testutils.ReadTestData(name)
329 return hv_kvm.KVMHypervisor._ParseKVMVersion(help)
330
331 def testParseVersion112(self):
332 self.assertEqual(
333 self.ParseTestData("kvm_1.1.2_help.txt"), ("1.1.2", 1, 1, 2))
334
335 def testParseVersion10(self):
336 self.assertEqual(self.ParseTestData("kvm_1.0_help.txt"), ("1.0", 1, 0, 0))
337
338 def testParseVersion01590(self):
339 self.assertEqual(
340 self.ParseTestData("kvm_0.15.90_help.txt"), ("0.15.90", 0, 15, 90))
341
342 def testParseVersion0125(self):
343 self.assertEqual(
344 self.ParseTestData("kvm_0.12.5_help.txt"), ("0.12.5", 0, 12, 5))
345
346 def testParseVersion091(self):
347 self.assertEqual(
348 self.ParseTestData("kvm_0.9.1_help.txt"), ("0.9.1", 0, 9, 1))
349
350
351 class TestSpiceParameterList(unittest.TestCase):
352 def setUp(self):
353 self.defaults = constants.HVC_DEFAULTS[constants.HT_KVM]
354
355 def testAudioCompressionDefaultOn(self):
356 self.assertTrue(self.defaults[constants.HV_KVM_SPICE_AUDIO_COMPR])
357
358 def testVdAgentDefaultOn(self):
359 self.assertTrue(self.defaults[constants.HV_KVM_SPICE_USE_VDAGENT])
360
361 def testTlsCiphersDefaultOn(self):
362 self.assertTrue(self.defaults[constants.HV_KVM_SPICE_TLS_CIPHERS])
363
364 def testBindDefaultOff(self):
365 self.assertFalse(self.defaults[constants.HV_KVM_SPICE_BIND])
366
367 def testAdditionalParams(self):
368 params = compat.UniqueFrozenset(
369 getattr(constants, name)
370 for name in dir(constants)
371 if name.startswith("HV_KVM_SPICE_"))
372 fixed = set([
373 constants.HV_KVM_SPICE_BIND, constants.HV_KVM_SPICE_TLS_CIPHERS,
374 constants.HV_KVM_SPICE_USE_VDAGENT, constants.HV_KVM_SPICE_AUDIO_COMPR])
375 self.assertEqual(hv_kvm._SPICE_ADDITIONAL_PARAMS, params - fixed)
376
377
378 class TestHelpRegexps(testutils.GanetiTestCase):
379 """Check _BOOT_RE
380
381 It has to match -drive.*boot=on|off except if there is another dash-option
382 at the beginning of the line.
383
384 """
385
386 @staticmethod
387 def SearchTestData(name):
388 boot_re = hv_kvm.KVMHypervisor._BOOT_RE
389 help = testutils.ReadTestData(name)
390 return boot_re.search(help)
391
392 def testBootRe112(self):
393 self.assertFalse(self.SearchTestData("kvm_1.1.2_help.txt"))
394
395 def testBootRe10(self):
396 self.assertFalse(self.SearchTestData("kvm_1.0_help.txt"))
397
398 def testBootRe01590(self):
399 self.assertFalse(self.SearchTestData("kvm_0.15.90_help.txt"))
400
401 def testBootRe0125(self):
402 self.assertTrue(self.SearchTestData("kvm_0.12.5_help.txt"))
403
404 def testBootRe091(self):
405 self.assertTrue(self.SearchTestData("kvm_0.9.1_help.txt"))
406
407 def testBootRe091_fake(self):
408 self.assertFalse(self.SearchTestData("kvm_0.9.1_help_boot_test.txt"))
409
410
411 class TestGetTunFeatures(unittest.TestCase):
412 def testWrongIoctl(self):
413 tmpfile = tempfile.NamedTemporaryFile()
414 # A file does not have the right ioctls, so this must always fail
415 result = netdev._GetTunFeatures(tmpfile.fileno())
416 self.assertTrue(result is None)
417
418 def _FakeIoctl(self, features, fd, request, buf):
419 self.assertEqual(request, netdev.TUNGETFEATURES)
420
421 (reqno, ) = struct.unpack("I", buf)
422 self.assertEqual(reqno, 0)
423
424 return struct.pack("I", features)
425
426 def test(self):
427 tmpfile = tempfile.NamedTemporaryFile()
428 fd = tmpfile.fileno()
429
430 for features in [0, netdev.IFF_VNET_HDR]:
431 fn = compat.partial(self._FakeIoctl, features)
432 result = netdev._GetTunFeatures(fd, _ioctl=fn)
433 self.assertEqual(result, features)
434
435
436 class TestProbeTapVnetHdr(unittest.TestCase):
437 def _FakeTunFeatures(self, expected_fd, flags, fd):
438 self.assertEqual(fd, expected_fd)
439 return flags
440
441 def test(self):
442 tmpfile = tempfile.NamedTemporaryFile()
443 fd = tmpfile.fileno()
444
445 for flags in [0, netdev.IFF_VNET_HDR]:
446 fn = compat.partial(self._FakeTunFeatures, fd, flags)
447
448 result = netdev._ProbeTapVnetHdr(fd, _features_fn=fn)
449 if flags == 0:
450 self.assertFalse(result)
451 else:
452 self.assertTrue(result)
453
454 def testUnsupported(self):
455 tmpfile = tempfile.NamedTemporaryFile()
456 fd = tmpfile.fileno()
457
458 self.assertFalse(netdev._ProbeTapVnetHdr(fd, _features_fn=lambda _: None))
459
460
461 class TestGenerateDeviceKVMId(unittest.TestCase):
462 def test(self):
463 device = objects.NIC()
464 target = constants.HOTPLUG_TARGET_NIC
465 fn = hv_kvm._GenerateDeviceKVMId
466 device.uuid = "003fc157-66a8-4e6d-8b7e-ec4f69751396"
467 self.assertTrue(re.match("nic-003fc157-66a8-4e6d", fn(target, device)))
468
469
470 class TestGenerateDeviceHVInfo(testutils.GanetiTestCase):
471 def testPCI(self):
472 """Test the placement of the first PCI device during startup."""
473 self.MockOut(mock.patch('ganeti.utils.EnsureDirs'))
474 hypervisor = hv_kvm.KVMHypervisor()
475 dev_type = constants.HOTPLUG_TARGET_NIC
476 kvm_devid = "nic-9e7c85f6-b6e5-4243"
477 hv_dev_type = constants.HT_NIC_PARAVIRTUAL
478 bus_slots = hypervisor._GetBusSlots()
479 hvinfo = hv_kvm._GenerateDeviceHVInfo(dev_type,
480 kvm_devid,
481 hv_dev_type,
482 bus_slots)
483 # NOTE: The PCI slot is zero-based, i.e. 13th slot has addr hex(12)
484 expected_hvinfo = {
485 "driver": "virtio-net-pci",
486 "id": kvm_devid,
487 "bus": "pci.0",
488 "addr": hex(constants.QEMU_DEFAULT_PCI_RESERVATIONS),
489 }
490
491 self.assertTrue(hvinfo == expected_hvinfo)
492
493 def testSCSI(self):
494 """Test the placement of the first SCSI device during startup."""
495 self.MockOut(mock.patch('ganeti.utils.EnsureDirs'))
496 hypervisor = hv_kvm.KVMHypervisor()
497 dev_type = constants.HOTPLUG_TARGET_DISK
498 kvm_devid = "disk-932df160-7a22-4067"
499 hv_dev_type = constants.HT_DISK_SCSI_BLOCK
500 bus_slots = hypervisor._GetBusSlots()
501 hvinfo = hv_kvm._GenerateDeviceHVInfo(dev_type,
502 kvm_devid,
503 hv_dev_type,
504 bus_slots)
505 expected_hvinfo = {
506 "driver": "scsi-block",
507 "id": kvm_devid,
508 "bus": "scsi.0",
509 "channel": 0,
510 "scsi-id": 0,
511 "lun": 0,
512 }
513
514 self.assertTrue(hvinfo == expected_hvinfo)
515
516
517 class TestGetRuntimeInfo(unittest.TestCase):
518 @classmethod
519 def _GetRuntime(cls):
520 data = testutils.ReadTestData("kvm_runtime.json")
521 return hv_kvm._AnalyzeSerializedRuntime(data)
522
523 def _fail(self, target, device, runtime):
524 device.uuid = "aaaaaaaa-66a8-4e6d-8b7e-ec4f69751396"
525 self.assertRaises(errors.HotplugError,
526 hv_kvm._GetExistingDeviceInfo,
527 target, device, runtime)
528
529 def testNIC(self):
530 device = objects.NIC()
531 target = constants.HOTPLUG_TARGET_NIC
532 runtime = self._GetRuntime()
533
534 self._fail(target, device, runtime)
535
536 device.uuid = "003fc157-66a8-4e6d-8b7e-ec4f69751396"
537 devinfo = hv_kvm._GetExistingDeviceInfo(target, device, runtime)
538 self.assertTrue(devinfo.hvinfo["addr"] == "0x8")
539
540 def testDisk(self):
541 device = objects.Disk()
542 target = constants.HOTPLUG_TARGET_DISK
543 runtime = self._GetRuntime()
544
545 self._fail(target, device, runtime)
546
547 device.uuid = "9f5c5bd4-6f60-480b-acdc-9bb1a4b7df79"
548 (devinfo, _, __) = hv_kvm._GetExistingDeviceInfo(target, device, runtime)
549 self.assertTrue(devinfo.hvinfo["addr"] == "0xa")
550
551
552 class PostfixMatcher(object):
553 def __init__(self, string):
554 self.string = string
555
556 def __eq__(self, other):
557 return other.endswith(self.string)
558
559 def __repr__(self):
560 return "<Postfix %s>" % self.string
561
562 class TestKvmRuntime(testutils.GanetiTestCase):
563 """The _ExecuteKvmRuntime is at the core of all KVM operations."""
564
565 def setUp(self):
566 super(TestKvmRuntime, self).setUp()
567 kvm_class = 'ganeti.hypervisor.hv_kvm.KVMHypervisor'
568 self.MockOut('qmp', mock.patch('ganeti.hypervisor.hv_kvm.QmpConnection'))
569 self.MockOut('run_cmd', mock.patch('ganeti.utils.RunCmd'))
570 self.MockOut('ensure_dirs', mock.patch('ganeti.utils.EnsureDirs'))
571 self.MockOut('write_file', mock.patch('ganeti.utils.WriteFile'))
572 self.MockOut(mock.patch(kvm_class + '.ValidateParameters'))
573 self.MockOut(mock.patch('ganeti.hypervisor.hv_kvm.OpenTap',
574 return_value=('test_nic', [], [])))
575 self.MockOut(mock.patch(kvm_class + '._ConfigureNIC'))
576 self.MockOut('pid_alive', mock.patch(kvm_class + '._InstancePidAlive',
577 return_value=('file', -1, False)))
578 self.MockOut(mock.patch(kvm_class + '._ExecuteCpuAffinity'))
579 self.MockOut(mock.patch(kvm_class + '._CallMonitorCommand'))
580
581 self.cfg = ConfigMock()
582 self.params = constants.HVC_DEFAULTS[constants.HT_KVM].copy()
583 self.beparams = constants.BEC_DEFAULTS.copy()
584 self.instance = self.cfg.AddNewInstance(name='name.example.com',
585 hypervisor='kvm',
586 hvparams=self.params,
587 beparams=self.beparams)
588
589 def testDirectoriesCreated(self):
590 hypervisor = hv_kvm.KVMHypervisor()
591 self.mocks['ensure_dirs'].assert_called_with([
592 (PostfixMatcher('/run/ganeti/kvm-hypervisor'), 0775),
593 (PostfixMatcher('/run/ganeti/kvm-hypervisor/pid'), 0775),
594 (PostfixMatcher('/run/ganeti/kvm-hypervisor/uid'), 0775),
595 (PostfixMatcher('/run/ganeti/kvm-hypervisor/ctrl'), 0775),
596 (PostfixMatcher('/run/ganeti/kvm-hypervisor/conf'), 0775),
597 (PostfixMatcher('/run/ganeti/kvm-hypervisor/nic'), 0775),
598 (PostfixMatcher('/run/ganeti/kvm-hypervisor/chroot'), 0775),
599 (PostfixMatcher('/run/ganeti/kvm-hypervisor/chroot-quarantine'), 0775),
600 (PostfixMatcher('/run/ganeti/kvm-hypervisor/keymap'), 0775)])
601
602 def testStartInstance(self):
603 hypervisor = hv_kvm.KVMHypervisor()
604 def RunCmd(cmd, **kwargs):
605 if '--help' in cmd:
606 return mock.Mock(
607 failed=False, output=testutils.ReadTestData("kvm_1.1.2_help.txt"))
608 if '-S' in cmd:
609 self.mocks['pid_alive'].return_value = ('file', -1, True)
610 return mock.Mock(failed=False)
611 elif '-M' in cmd:
612 return mock.Mock(failed=False, output='')
613 elif '-device' in cmd:
614 return mock.Mock(failed=False, output='name "virtio-blk-pci"')
615 else:
616 raise errors.ProgrammerError('Unexpected command: %s' % cmd)
617 self.mocks['run_cmd'].side_effect = RunCmd
618 hypervisor.StartInstance(self.instance, [], False)
619
620
621 class TestKvmCpuPinning(testutils.GanetiTestCase):
622 def setUp(self):
623 super(TestKvmCpuPinning, self).setUp()
624 kvm_class = 'ganeti.hypervisor.hv_kvm.KVMHypervisor'
625 self.MockOut('qmp', mock.patch('ganeti.hypervisor.hv_kvm.QmpConnection'))
626 self.MockOut('run_cmd', mock.patch('ganeti.utils.RunCmd'))
627 self.MockOut('ensure_dirs', mock.patch('ganeti.utils.EnsureDirs'))
628 self.MockOut('write_file', mock.patch('ganeti.utils.WriteFile'))
629 self.MockOut(mock.patch(kvm_class + '._InstancePidAlive',
630 return_value=(True, 1371, True)))
631 self.MockOut(mock.patch(kvm_class + '._GetVcpuThreadIds',
632 return_value=[1, 3, 5, 2, 4, 0 ]))
633 self.params = constants.HVC_DEFAULTS[constants.HT_KVM].copy()
634
635 def testCpuPinningDefault(self):
636 if hv_kvm.psutil is None:
637 # FIXME: switch to unittest.skip once python 2.6 is deprecated
638 print "skipped 'psutil Python package not found'"
639 return
640 mock_process = mock.MagicMock()
641 cpu_mask = self.params['cpu_mask']
642 worker_cpu_mask = self.params['worker_cpu_mask']
643 hypervisor = hv_kvm.KVMHypervisor()
644 with nested(mock.patch('psutil.Process', return_value=mock_process),
645 mock.patch('psutil.cpu_count', return_value=1237)):
646 hypervisor._ExecuteCpuAffinity('test_instance', cpu_mask, worker_cpu_mask)
647
648 self.assertEqual(mock_process.set_cpu_affinity.call_count, 1)
649 self.assertEqual(mock_process.set_cpu_affinity.call_args_list[0],
650 mock.call(range(0,1237)))
651
652 def testCpuPinningPerVcpu(self):
653 if hv_kvm.psutil is None:
654 # FIXME: switch to unittest.skip once python 2.6 is deprecated
655 print "skipped 'psutil Python package not found'"
656 return
657 mock_process = mock.MagicMock()
658 mock_process.set_cpu_affinity = mock.MagicMock()
659 mock_process.set_cpu_affinity().return_value = True
660 mock_process.get_children.return_value = []
661 mock_process.reset_mock()
662
663 cpu_mask = "1:2:4:5:10:15-17"
664 worker_cpu_mask = self.params['worker_cpu_mask']
665 hypervisor = hv_kvm.KVMHypervisor()
666
667 # This is necessary so that it provides the same object each time instead of
668 # overwriting it each time.
669 def get_mock_process(unused_pid):
670 return mock_process
671
672 with nested(mock.patch('psutil.Process', get_mock_process),
673 mock.patch('psutil.cpu_count', return_value=1237)):
674 hypervisor._ExecuteCpuAffinity('test_instance', cpu_mask, worker_cpu_mask)
675 self.assertEqual(mock_process.set_cpu_affinity.call_count, 7)
676 self.assertEqual(mock_process.set_cpu_affinity.call_args_list[0],
677 mock.call(range(0,1237)))
678 self.assertEqual(mock_process.set_cpu_affinity.call_args_list[6],
679 mock.call([15, 16, 17]))
680
681 def testCpuPinningEntireInstance(self):
682 if hv_kvm.psutil is None:
683 # FIXME: switch to unittest.skip once python 2.6 is deprecated
684 print "skipped 'psutil Python package not found'"
685 return
686 mock_process = mock.MagicMock()
687 mock_process.set_cpu_affinity = mock.MagicMock()
688 mock_process.set_cpu_affinity().return_value = True
689 mock_process.get_children.return_value = []
690 mock_process.reset_mock()
691
692 cpu_mask = "4"
693 worker_cpu_mask = "5"
694 hypervisor = hv_kvm.KVMHypervisor()
695
696 def get_mock_process(unused_pid):
697 return mock_process
698
699 with mock.patch('psutil.Process', get_mock_process):
700 hypervisor._ExecuteCpuAffinity('test_instance', cpu_mask, worker_cpu_mask)
701 self.assertEqual(mock_process.set_cpu_affinity.call_count, 7)
702 self.assertEqual(mock_process.set_cpu_affinity.call_args_list[0],
703 mock.call([5]))
704 self.assertEqual(mock_process.set_cpu_affinity.call_args_list[1],
705 mock.call([4]))
706
707 class TestPostcopyAfterPrecopy(testutils.GanetiTestCase):
708 def setUp(self):
709 super(TestPostcopyAfterPrecopy, self).setUp()
710 kvm_class = 'ganeti.hypervisor.hv_kvm.KVMHypervisor'
711 self.MockOut('qmp', mock.patch('ganeti.hypervisor.hv_kvm.QmpConnection'))
712 self.MockOut('run_cmd', mock.patch('ganeti.utils.RunCmd'))
713 self.MockOut('ensure_dirs', mock.patch('ganeti.utils.EnsureDirs'))
714 self.MockOut('write_file', mock.patch('ganeti.utils.WriteFile'))
715 self.params = constants.HVC_DEFAULTS[constants.HT_KVM].copy()
716
717 def _TestPostcopyAfterPrecopy(self, runcmd, postcopy_started_goal):
718 hypervisor = hv_kvm.KVMHypervisor()
719 self.iteration = 0
720 self.postcopy_started = False
721
722 def runcmd_mock(cmd, env=None, output=None, cwd="/", reset_env=False,
723 interactive=False, timeout=None, noclose_fds=None,
724 input_fd=None, postfork_fn=None):
725 res = utils.RunResult(0, None, '', '', cmd, None, None)
726 if not self.postcopy_started and cmd.find('migrate_start_postcopy') != -1:
727 self.postcopy_started = True
728 res.stdout = ('migrate_postcopy_start\n'
729 '(qemu) ')
730 return runcmd(cmd, res)
731
732 with mock.patch('ganeti.utils.RunCmd', runcmd_mock):
733 instance = mock.MagicMock()
734 instance.name = 'example.instance'
735 hypervisor._PostcopyAfterPrecopy(instance)
736 self.assertEqual(self.postcopy_started, postcopy_started_goal)
737
738 def testNormal(self):
739 def runcmd_normal(cmd, res):
740 res = utils.RunResult(0, None, '', '', cmd, None, None)
741 if cmd.find('info migrate') != -1:
742 self.iteration += 1
743 res.stdout = (
744 'QEMU 2.5.0 monitor - type \'help\' for more information\n'
745 '(qemu) info migrate\n'
746 'capabilities: xbzrle: off rdma-pin-all: off auto-converge: on'
747 'zero-blocks: off compress: off events: off x-postcopy-ram: on \n'
748 'Migration status: active\n'
749 'skipped: 0 pages\n'
750 'dirty sync count: %i\n'
751 '(qemu) ' % self.iteration
752 )
753 return res
754
755 self._TestPostcopyAfterPrecopy(runcmd_normal, True)
756
757 def testEmptyResponses(self):
758 def runcmd_empty_responses(cmd, res):
759 res = utils.RunResult(0, None, '', '', cmd, None, None)
760 if cmd.find('info migrate') != -1:
761 self.iteration += 1
762 if self.iteration < 3:
763 res.stdout = (
764 'QEMU 2.5.0 monitor - type \'help\' for more information\n'
765 '(qemu) info migrate\n'
766 '(qemu) '
767 )
768 else:
769 res.stdout = (
770 'QEMU 2.5.0 monitor - type \'help\' for more information\n'
771 '(qemu) info migrate\n'
772 'capabilities: xbzrle: off rdma-pin-all: off auto-converge: on'
773 'zero-blocks: off compress: off events: off x-postcopy-ram: on \n'
774 'Migration status: active\n'
775 'skipped: 0 pages\n'
776 'dirty sync count: %i\n'
777 '(qemu) ' % self.iteration
778 )
779 return res
780 self._TestPostcopyAfterPrecopy(runcmd_empty_responses, True)
781
782 def testMonitorRemoved(self):
783 def runcmd_monitor_removed(cmd, res):
784 res = utils.RunResult(0, None, '', '', cmd, None, None)
785 if cmd.find('info migrate') != -1:
786 self.iteration += 1
787 if self.iteration < 3:
788 res.stdout = (
789 'QEMU 2.5.0 monitor - type \'help\' for more information\n'
790 '(qemu) info migrate\n'
791 'capabilities: xbzrle: off rdma-pin-all: off auto-converge: on'
792 'zero-blocks: off compress: off events: off x-postcopy-ram: on \n'
793 'Migration status: active\n'
794 'skipped: 0 pages\n'
795 'dirty sync count: %i\n'
796 '(qemu) '
797 )
798 else:
799 res.stderr = ('2017/07/26 15:49:52 socat[105703] E connect(3, AF=1 '
800 '"/var/run/ganeti/kvm-hypervisor/ctrl/example.instanc'
801 'e.monitor", 85): No such file or directory')
802 return res
803 self._TestPostcopyAfterPrecopy(runcmd_monitor_removed, False)
804
805 def testMigrationFailed(self):
806 def runcmd_migration_failed(cmd, res):
807 res = utils.RunResult(0, None, '', '', cmd, None, None)
808 if cmd.find('info migrate') != -1:
809 self.iteration += 1
810 if self.iteration < 3:
811 res.stdout = (
812 'QEMU 2.5.0 monitor - type \'help\' for more information\n'
813 '(qemu) info migrate\n'
814 'capabilities: xbzrle: off rdma-pin-all: off auto-converge: on'
815 'zero-blocks: off compress: off events: off x-postcopy-ram: on \n'
816 'Migration status: active\n'
817 'skipped: 0 pages\n'
818 'dirty sync count: %i\n'
819 '(qemu) '
820 )
821 else:
822 res.stdout = (
823 'QEMU 2.5.0 monitor - type \'help\' for more information\n'
824 '(qemu) info migrate\n'
825 'capabilities: xbzrle: off rdma-pin-all: off auto-converge: on'
826 'zero-blocks: off compress: off events: off x-postcopy-ram: on \n'
827 'Migration status: failed\n'
828 'skipped: 0 pages\n'
829 'dirty sync count: %i\n'
830 '(qemu) '
831 )
832 return res
833 self._TestPostcopyAfterPrecopy(runcmd_migration_failed, False)
834
835 def testAlreadyInPostcopy(self):
836 def runcmd_already_in_postcopy(cmd, res):
837 res = utils.RunResult(0, None, '', '', cmd, None, None)
838 if cmd.find('info migrate') != -1:
839 res.stdout = (
840 'QEMU 2.5.0 monitor - type \'help\' for more information\n'
841 '(qemu) info migrate\n'
842 'capabilities: xbzrle: off rdma-pin-all: off auto-converge: on'
843 'zero-blocks: off compress: off events: off x-postcopy-ram: on \n'
844 'Migration status: postcopy-active\n'
845 'skipped: 0 pages\n'
846 'dirty sync count: %i\n'
847 '(qemu) '
848 )
849 return res
850 self._TestPostcopyAfterPrecopy(runcmd_already_in_postcopy, False)
851
852 if __name__ == "__main__":
853 testutils.GanetiTestProgram()