blob: 16b1e0ad29c566b99ace3308ef3d2dd66a2bbcac [file] [log] [blame]
#!/usr/bin/python
#
# Copyright (C) 2010, 2011 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Script for testing the hypervisor.hv_kvm module"""
import threading
import tempfile
import unittest
import socket
import os
import struct
import re
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 pathutils
from ganeti.hypervisor import hv_kvm
import ganeti.hypervisor.hv_kvm.netdev as netdev
import ganeti.hypervisor.hv_kvm.monitor as monitor
import mock
import testutils
from testutils.config_mock import ConfigMock
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": [],
}
_SUPPORTED_COMMANDS = {
"return": [
{"name": "command"},
{"name": "query-kvm"},
{"name": "eject"},
{"name": "query-status"},
{"name": "query-name"},
]
}
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))
# Expect query-commands and return the list of supported commands
conn.recv(4096)
conn.send(self.encode_string(self._SUPPORTED_COMMANDS))
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)
self.assertEqual(len(rebuilt_message), len(test_data))
def testDelete(self):
toDelete = "execute"
test_data = {
toDelete: "command",
"arguments": ["a", "b", "c"],
}
message = hv_kvm.QmpMessage(test_data)
oldLen = len(message)
del(message[toDelete])
newLen = len(message)
self.assertEqual(oldLen - 1, newLen)
class TestQmp(testutils.GanetiTestCase):
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 = [
{"enabled": True, "present": True},
{},
[{"name": "quit"}, {"name": "eject"}],
{"running": True, "singlestep": False},
]
def testQmp(self):
# Set up the stub
socket_file = tempfile.NamedTemporaryFile()
os.remove(socket_file.name)
qmp_stub = QmpStub(socket_file.name, self.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(self.REQUESTS,
self.EXPECTED_RESPONSES):
response = qmp_connection.Execute(request["execute"],
request["arguments"])
self.assertEqual(response, expected_response)
msg = hv_kvm.QmpMessage({"return": expected_response})
self.assertEqual(len(str(msg).splitlines()), 1,
msg="Got multi-line message")
self.assertRaises(monitor.QmpCommandNotSupported,
qmp_connection.Execute,
"unsupported-command")
def testQmpContextManager(self):
# Set up the stub
socket_file = tempfile.NamedTemporaryFile()
os.remove(socket_file.name)
qmp_stub = QmpStub(socket_file.name, self.SERVER_RESPONSES)
qmp_stub.start()
# Test the context manager functionality
with hv_kvm.QmpConnection(socket_file.name) as qmp:
for request, expected_response in zip(self.REQUESTS,
self.EXPECTED_RESPONSES):
response = qmp.Execute(request["execute"], request["arguments"])
self.assertEqual(response, expected_response)
class TestConsole(unittest.TestCase):
def MakeConsole(self, instance, node, group, hvparams):
cons = hv_kvm.KVMHypervisor.GetInstanceConsole(instance, node, group,
hvparams, {})
self.assertEqual(cons.Validate(), None)
return cons
def testSerial(self):
instance = objects.Instance(name="kvm.example.com",
primary_node="node6017-uuid")
node = objects.Node(name="node6017", uuid="node6017-uuid",
ndparams={})
group = objects.NodeGroup(name="group6134", ndparams={})
hvparams = {
constants.HV_SERIAL_CONSOLE: True,
constants.HV_VNC_BIND_ADDRESS: None,
constants.HV_KVM_SPICE_BIND: None,
}
cons = self.MakeConsole(instance, node, group, hvparams)
self.assertEqual(cons.kind, constants.CONS_SSH)
self.assertEqual(cons.host, node.name)
self.assertEqual(cons.command[0], pathutils.KVM_CONSOLE_WRAPPER)
self.assertEqual(cons.command[1], constants.SOCAT_PATH)
def testVnc(self):
instance = objects.Instance(name="kvm.example.com",
primary_node="node7235-uuid",
network_port=constants.VNC_BASE_PORT + 10)
node = objects.Node(name="node7235", uuid="node7235-uuid",
ndparams={})
group = objects.NodeGroup(name="group3632", ndparams={})
hvparams = {
constants.HV_SERIAL_CONSOLE: False,
constants.HV_VNC_BIND_ADDRESS: "192.0.2.1",
constants.HV_KVM_SPICE_BIND: None,
}
cons = self.MakeConsole(instance, node, group, hvparams)
self.assertEqual(cons.kind, constants.CONS_VNC)
self.assertEqual(cons.host, "192.0.2.1")
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)
node = objects.Node(name="node7235", uuid="node7235-uuid",
ndparams={})
group = objects.NodeGroup(name="group0132", ndparams={})
hvparams = {
constants.HV_SERIAL_CONSOLE: False,
constants.HV_VNC_BIND_ADDRESS: None,
constants.HV_KVM_SPICE_BIND: "192.0.2.1",
}
cons = self.MakeConsole(instance, node, group, 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",
network_port=0)
node = objects.Node(name="node24325", uuid="node24325-uuid",
ndparams={})
group = objects.NodeGroup(name="group9184", ndparams={})
hvparams = {
constants.HV_SERIAL_CONSOLE: False,
constants.HV_VNC_BIND_ADDRESS: None,
constants.HV_KVM_SPICE_BIND: None,
}
cons = self.MakeConsole(instance, node, group, hvparams)
self.assertEqual(cons.kind, constants.CONS_MESSAGE)
class TestVersionChecking(testutils.GanetiTestCase):
@staticmethod
def ParseTestData(name):
help = testutils.ReadTestData(name)
return hv_kvm.KVMHypervisor._ParseKVMVersion(help)
def testParseVersion112(self):
self.assertEqual(
self.ParseTestData("kvm_1.1.2_help.txt"), ("1.1.2", 1, 1, 2))
def testParseVersion10(self):
self.assertEqual(self.ParseTestData("kvm_1.0_help.txt"), ("1.0", 1, 0, 0))
def testParseVersion01590(self):
self.assertEqual(
self.ParseTestData("kvm_0.15.90_help.txt"), ("0.15.90", 0, 15, 90))
def testParseVersion0125(self):
self.assertEqual(
self.ParseTestData("kvm_0.12.5_help.txt"), ("0.12.5", 0, 12, 5))
def testParseVersion091(self):
self.assertEqual(
self.ParseTestData("kvm_0.9.1_help.txt"), ("0.9.1", 0, 9, 1))
class TestSpiceParameterList(unittest.TestCase):
def setUp(self):
self.defaults = constants.HVC_DEFAULTS[constants.HT_KVM]
def testAudioCompressionDefaultOn(self):
self.assertTrue(self.defaults[constants.HV_KVM_SPICE_AUDIO_COMPR])
def testVdAgentDefaultOn(self):
self.assertTrue(self.defaults[constants.HV_KVM_SPICE_USE_VDAGENT])
def testTlsCiphersDefaultOn(self):
self.assertTrue(self.defaults[constants.HV_KVM_SPICE_TLS_CIPHERS])
def testBindDefaultOff(self):
self.assertFalse(self.defaults[constants.HV_KVM_SPICE_BIND])
def testAdditionalParams(self):
params = compat.UniqueFrozenset(
getattr(constants, name)
for name in dir(constants)
if name.startswith("HV_KVM_SPICE_"))
fixed = set([
constants.HV_KVM_SPICE_BIND, constants.HV_KVM_SPICE_TLS_CIPHERS,
constants.HV_KVM_SPICE_USE_VDAGENT, constants.HV_KVM_SPICE_AUDIO_COMPR])
self.assertEqual(hv_kvm._SPICE_ADDITIONAL_PARAMS, params - fixed)
class TestHelpRegexps(testutils.GanetiTestCase):
"""Check _BOOT_RE
It has to match -drive.*boot=on|off except if there is another dash-option
at the beginning of the line.
"""
@staticmethod
def SearchTestData(name):
boot_re = hv_kvm.KVMHypervisor._BOOT_RE
help = testutils.ReadTestData(name)
return boot_re.search(help)
def testBootRe112(self):
self.assertFalse(self.SearchTestData("kvm_1.1.2_help.txt"))
def testBootRe10(self):
self.assertFalse(self.SearchTestData("kvm_1.0_help.txt"))
def testBootRe01590(self):
self.assertFalse(self.SearchTestData("kvm_0.15.90_help.txt"))
def testBootRe0125(self):
self.assertTrue(self.SearchTestData("kvm_0.12.5_help.txt"))
def testBootRe091(self):
self.assertTrue(self.SearchTestData("kvm_0.9.1_help.txt"))
def testBootRe091_fake(self):
self.assertFalse(self.SearchTestData("kvm_0.9.1_help_boot_test.txt"))
class TestGetTunFeatures(unittest.TestCase):
def testWrongIoctl(self):
tmpfile = tempfile.NamedTemporaryFile()
# A file does not have the right ioctls, so this must always fail
result = netdev._GetTunFeatures(tmpfile.fileno())
self.assertTrue(result is None)
def _FakeIoctl(self, features, fd, request, buf):
self.assertEqual(request, netdev.TUNGETFEATURES)
(reqno, ) = struct.unpack("I", buf)
self.assertEqual(reqno, 0)
return struct.pack("I", features)
def test(self):
tmpfile = tempfile.NamedTemporaryFile()
fd = tmpfile.fileno()
for features in [0, netdev.IFF_VNET_HDR]:
fn = compat.partial(self._FakeIoctl, features)
result = netdev._GetTunFeatures(fd, _ioctl=fn)
self.assertEqual(result, features)
class TestProbeTapVnetHdr(unittest.TestCase):
def _FakeTunFeatures(self, expected_fd, flags, fd):
self.assertEqual(fd, expected_fd)
return flags
def test(self):
tmpfile = tempfile.NamedTemporaryFile()
fd = tmpfile.fileno()
for flags in [0, netdev.IFF_VNET_HDR]:
fn = compat.partial(self._FakeTunFeatures, fd, flags)
result = netdev._ProbeTapVnetHdr(fd, _features_fn=fn)
if flags == 0:
self.assertFalse(result)
else:
self.assertTrue(result)
def testUnsupported(self):
tmpfile = tempfile.NamedTemporaryFile()
fd = tmpfile.fileno()
self.assertFalse(netdev._ProbeTapVnetHdr(fd, _features_fn=lambda _: None))
class TestGenerateDeviceKVMId(unittest.TestCase):
def test(self):
device = objects.NIC()
target = constants.HOTPLUG_TARGET_NIC
fn = hv_kvm._GenerateDeviceKVMId
device.uuid = "003fc157-66a8-4e6d-8b7e-ec4f69751396"
self.assertTrue(re.match("nic-003fc157-66a8-4e6d", fn(target, device)))
class TestGenerateDeviceHVInfo(testutils.GanetiTestCase):
def testPCI(self):
"""Test the placement of the first PCI device during startup."""
self.MockOut(mock.patch('ganeti.utils.EnsureDirs'))
hypervisor = hv_kvm.KVMHypervisor()
dev_type = constants.HOTPLUG_TARGET_NIC
kvm_devid = "nic-9e7c85f6-b6e5-4243"
hv_dev_type = constants.HT_NIC_PARAVIRTUAL
bus_slots = hypervisor._GetBusSlots()
hvinfo = hv_kvm._GenerateDeviceHVInfo(dev_type,
kvm_devid,
hv_dev_type,
bus_slots)
# NOTE: The PCI slot is zero-based, i.e. 13th slot has addr hex(12)
expected_hvinfo = {
"driver": "virtio-net-pci",
"id": kvm_devid,
"bus": "pci.0",
"addr": hex(constants.QEMU_DEFAULT_PCI_RESERVATIONS),
}
self.assertTrue(hvinfo == expected_hvinfo)
def testSCSI(self):
"""Test the placement of the first SCSI device during startup."""
self.MockOut(mock.patch('ganeti.utils.EnsureDirs'))
hypervisor = hv_kvm.KVMHypervisor()
dev_type = constants.HOTPLUG_TARGET_DISK
kvm_devid = "disk-932df160-7a22-4067"
hv_dev_type = constants.HT_DISK_SCSI_BLOCK
bus_slots = hypervisor._GetBusSlots()
hvinfo = hv_kvm._GenerateDeviceHVInfo(dev_type,
kvm_devid,
hv_dev_type,
bus_slots)
expected_hvinfo = {
"driver": "scsi-block",
"id": kvm_devid,
"bus": "scsi.0",
"channel": 0,
"scsi-id": 0,
"lun": 0,
}
self.assertTrue(hvinfo == expected_hvinfo)
class TestGetRuntimeInfo(unittest.TestCase):
@classmethod
def _GetRuntime(cls):
data = testutils.ReadTestData("kvm_runtime.json")
return hv_kvm._AnalyzeSerializedRuntime(data)
def _fail(self, target, device, runtime):
device.uuid = "aaaaaaaa-66a8-4e6d-8b7e-ec4f69751396"
self.assertRaises(errors.HotplugError,
hv_kvm._GetExistingDeviceInfo,
target, device, runtime)
def testNIC(self):
device = objects.NIC()
target = constants.HOTPLUG_TARGET_NIC
runtime = self._GetRuntime()
self._fail(target, device, runtime)
device.uuid = "003fc157-66a8-4e6d-8b7e-ec4f69751396"
devinfo = hv_kvm._GetExistingDeviceInfo(target, device, runtime)
self.assertTrue(devinfo.hvinfo["addr"] == "0x8")
def testDisk(self):
device = objects.Disk()
target = constants.HOTPLUG_TARGET_DISK
runtime = self._GetRuntime()
self._fail(target, device, runtime)
device.uuid = "9f5c5bd4-6f60-480b-acdc-9bb1a4b7df79"
(devinfo, _, __) = hv_kvm._GetExistingDeviceInfo(target, device, runtime)
self.assertTrue(devinfo.hvinfo["addr"] == "0xa")
class PostfixMatcher(object):
def __init__(self, string):
self.string = string
def __eq__(self, other):
return other.endswith(self.string)
def __repr__(self):
return "<Postfix %s>" % self.string
class TestKvmRuntime(testutils.GanetiTestCase):
"""The _ExecuteKvmRuntime is at the core of all KVM operations."""
def setUp(self):
super(TestKvmRuntime, self).setUp()
kvm_class = 'ganeti.hypervisor.hv_kvm.KVMHypervisor'
self.MockOut('qmp', mock.patch('ganeti.hypervisor.hv_kvm.QmpConnection'))
self.MockOut('run_cmd', mock.patch('ganeti.utils.RunCmd'))
self.MockOut('ensure_dirs', mock.patch('ganeti.utils.EnsureDirs'))
self.MockOut('write_file', mock.patch('ganeti.utils.WriteFile'))
self.MockOut(mock.patch(kvm_class + '.ValidateParameters'))
self.MockOut(mock.patch('ganeti.hypervisor.hv_kvm.OpenTap',
return_value=('test_nic', [], [])))
self.MockOut(mock.patch(kvm_class + '._ConfigureNIC'))
self.MockOut('pid_alive', mock.patch(kvm_class + '._InstancePidAlive',
return_value=('file', -1, False)))
self.MockOut(mock.patch(kvm_class + '._ExecuteCpuAffinity'))
self.MockOut(mock.patch(kvm_class + '._CallMonitorCommand'))
self.cfg = ConfigMock()
params = constants.HVC_DEFAULTS[constants.HT_KVM].copy()
beparams = constants.BEC_DEFAULTS.copy()
self.instance = self.cfg.AddNewInstance(name='name.example.com',
hypervisor='kvm',
hvparams=params,
beparams=beparams)
def testDirectoriesCreated(self):
hypervisor = hv_kvm.KVMHypervisor()
self.mocks['ensure_dirs'].assert_called_with([
(PostfixMatcher('/run/ganeti/kvm-hypervisor'), 0775),
(PostfixMatcher('/run/ganeti/kvm-hypervisor/pid'), 0775),
(PostfixMatcher('/run/ganeti/kvm-hypervisor/uid'), 0775),
(PostfixMatcher('/run/ganeti/kvm-hypervisor/ctrl'), 0775),
(PostfixMatcher('/run/ganeti/kvm-hypervisor/conf'), 0775),
(PostfixMatcher('/run/ganeti/kvm-hypervisor/nic'), 0775),
(PostfixMatcher('/run/ganeti/kvm-hypervisor/chroot'), 0775),
(PostfixMatcher('/run/ganeti/kvm-hypervisor/chroot-quarantine'), 0775),
(PostfixMatcher('/run/ganeti/kvm-hypervisor/keymap'), 0775)])
def testStartInstance(self):
hypervisor = hv_kvm.KVMHypervisor()
def RunCmd(cmd, **kwargs):
if '--help' in cmd:
return mock.Mock(
failed=False, output=testutils.ReadTestData("kvm_1.1.2_help.txt"))
if '-S' in cmd:
self.mocks['pid_alive'].return_value = ('file', -1, True)
return mock.Mock(failed=False)
elif '-M' in cmd:
return mock.Mock(failed=False, output='')
elif '-device' in cmd:
return mock.Mock(failed=False, output='name "virtio-blk-pci"')
else:
raise errors.ProgrammerError('Unexpected command: %s' % cmd)
self.mocks['run_cmd'].side_effect = RunCmd
hypervisor.StartInstance(self.instance, [], False)
if __name__ == "__main__":
testutils.GanetiTestProgram()