Fix an error handling case in instance info
[ganeti-local] / lib / hypervisor / hv_kvm.py
1 #
2 #
3
4 # Copyright (C) 2008 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 """KVM hypervisor
23
24 """
25
26 import os
27 import os.path
28 import re
29 import tempfile
30 from cStringIO import StringIO
31
32 from ganeti import utils
33 from ganeti import constants
34 from ganeti import errors
35 from ganeti.hypervisor import hv_base
36
37
38 class KVMHypervisor(hv_base.BaseHypervisor):
39   """Fake hypervisor interface.
40
41   This can be used for testing the ganeti code without having to have
42   a real virtualisation software installed.
43
44   """
45   _ROOT_DIR = constants.RUN_GANETI_DIR + "/kvm-hypervisor"
46   _PIDS_DIR = _ROOT_DIR + "/pid"
47   _CTRL_DIR = _ROOT_DIR + "/ctrl"
48   _DIRS = [_ROOT_DIR, _PIDS_DIR, _CTRL_DIR]
49
50   PARAMETERS = [
51     constants.HV_KERNEL_PATH,
52     constants.HV_INITRD_PATH,
53     constants.HV_ACPI,
54     ]
55
56   def __init__(self):
57     hv_base.BaseHypervisor.__init__(self)
58     # Let's make sure the directories we need exist, even if the RUN_DIR lives
59     # in a tmpfs filesystem or has been otherwise wiped out.
60     for mydir in self._DIRS:
61       if not os.path.exists(mydir):
62         os.mkdir(mydir)
63
64   def _WriteNetScript(self, instance, seq, nic):
65     """Write a script to connect a net interface to the proper bridge.
66
67     This can be used by any qemu-type hypervisor.
68
69     @param instance: instance we're acting on
70     @type instance: instance object
71     @param seq: nic sequence number
72     @type seq: int
73     @param nic: nic we're acting on
74     @type nic: nic object
75     @return: netscript file name
76     @rtype: string
77
78     """
79     script = StringIO()
80     script.write("#!/bin/sh\n")
81     script.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
82     script.write("export INSTANCE=%s\n" % instance.name)
83     script.write("export MAC=%s\n" % nic.mac)
84     script.write("export IP=%s\n" % nic.ip)
85     script.write("export BRIDGE=%s\n" % nic.bridge)
86     script.write("export INTERFACE=$1\n")
87     # TODO: make this configurable at ./configure time
88     script.write("if [ -x /etc/ganeti/kvm-vif-bridge ]; then\n")
89     script.write("  # Execute the user-specific vif file\n")
90     script.write("  /etc/ganeti/kvm-vif-bridge\n")
91     script.write("else\n")
92     script.write("  # Connect the interface to the bridge\n")
93     script.write("  /sbin/ifconfig $INTERFACE 0.0.0.0 up\n")
94     script.write("  /usr/sbin/brctl addif $BRIDGE $INTERFACE\n")
95     script.write("fi\n\n")
96     # As much as we'd like to put this in our _ROOT_DIR, that will happen to be
97     # mounted noexec sometimes, so we'll have to find another place.
98     (tmpfd, tmpfile_name) = tempfile.mkstemp()
99     tmpfile = os.fdopen(tmpfd, 'w')
100     tmpfile.write(script.getvalue())
101     tmpfile.close()
102     os.chmod(tmpfile_name, 0755)
103     return tmpfile_name
104
105   def ListInstances(self):
106     """Get the list of running instances.
107
108     We can do this by listing our live instances directory and
109     checking whether the associated kvm process is still alive.
110
111     """
112     result = []
113     for name in os.listdir(self._PIDS_DIR):
114       filename = "%s/%s" % (self._PIDS_DIR, name)
115       if utils.IsProcessAlive(utils.ReadPidFile(filename)):
116         result.append(name)
117     return result
118
119   def GetInstanceInfo(self, instance_name):
120     """Get instance properties.
121
122     @param instance_name: the instance name
123
124     @return: tuple (name, id, memory, vcpus, stat, times)
125
126     """
127     pidfile = "%s/%s" % (self._PIDS_DIR, instance_name)
128     pid = utils.ReadPidFile(pidfile)
129     if not utils.IsProcessAlive(pid):
130       return None
131
132     cmdline_file = "/proc/%s/cmdline" % pid
133     try:
134       fh = open(cmdline_file, 'r')
135       try:
136         cmdline = fh.read()
137       finally:
138         fh.close()
139     except IOError, err:
140       raise errors.HypervisorError("Failed to list instance %s: %s" %
141                                    (instance_name, err))
142
143     memory = 0
144     vcpus = 0
145     stat = "---b-"
146     times = "0"
147
148     arg_list = cmdline.split('\x00')
149     while arg_list:
150       arg =  arg_list.pop(0)
151       if arg == '-m':
152         memory = arg_list.pop(0)
153       elif arg == '-smp':
154         vcpus = arg_list.pop(0)
155
156     return (instance_name, pid, memory, vcpus, stat, times)
157
158   def GetAllInstancesInfo(self):
159     """Get properties of all instances.
160
161     @return: list of tuples (name, id, memory, vcpus, stat, times)
162
163     """
164     data = []
165     for name in os.listdir(self._PIDS_DIR):
166       filename = "%s/%s" % (self._PIDS_DIR, name)
167       if utils.IsProcessAlive(utils.ReadPidFile(filename)):
168         data.append(self.GetInstanceInfo(name))
169
170     return data
171
172   def StartInstance(self, instance, block_devices, extra_args):
173     """Start an instance.
174
175     """
176     temp_files = []
177     pidfile = self._PIDS_DIR + "/%s" % instance.name
178     if utils.IsProcessAlive(utils.ReadPidFile(pidfile)):
179       raise errors.HypervisorError("Failed to start instance %s: %s" %
180                                    (instance.name, "already running"))
181
182     kvm = constants.KVM_PATH
183     kvm_cmd = [kvm]
184     kvm_cmd.extend(['-m', instance.beparams[constants.BE_MEMORY]])
185     kvm_cmd.extend(['-smp', instance.beparams[constants.BE_VCPUS]])
186     kvm_cmd.extend(['-pidfile', pidfile])
187     # used just by the vnc server, if enabled
188     kvm_cmd.extend(['-name', instance.name])
189     kvm_cmd.extend(['-daemonize'])
190     if not instance.hvparams[constants.HV_ACPI]:
191       kvm_cmd.extend(['-no-acpi'])
192     if not instance.nics:
193       kvm_cmd.extend(['-net', 'none'])
194     else:
195       nic_seq = 0
196       for nic in instance.nics:
197         script = self._WriteNetScript(instance, nic_seq, nic)
198         # FIXME: handle other models
199         nic_val = "nic,macaddr=%s,model=virtio" % nic.mac
200         kvm_cmd.extend(['-net', nic_val])
201         kvm_cmd.extend(['-net', 'tap,script=%s' % script])
202         temp_files.append(script)
203         nic_seq += 1
204
205     boot_drive = True
206     for cfdev, dev_path in block_devices:
207       # TODO: handle FD_LOOP and FD_BLKTAP (?)
208       if boot_drive:
209         boot_val = ',boot=on'
210         boot_drive = False
211       else:
212         boot_val = ''
213
214       # TODO: handle different if= types
215       if_val = ',if=virtio'
216
217       drive_val = 'file=%s,format=raw%s%s' % (dev_path, if_val, boot_val)
218       kvm_cmd.extend(['-drive', drive_val])
219
220     kvm_cmd.extend(['-kernel', instance.hvparams[constants.HV_KERNEL_PATH]])
221
222     initrd_path = instance.hvparams[constants.HV_INITRD_PATH]
223     if initrd_path:
224       kvm_cmd.extend(['-initrd', initrd_path])
225
226     kvm_cmd.extend(['-append', 'console=ttyS0,38400 root=/dev/vda'])
227
228     #"hvm_boot_order",
229     #"hvm_cdrom_image_path",
230
231     kvm_cmd.extend(['-nographic'])
232     # FIXME: handle vnc, if needed
233     # How do we decide whether to have it or not?? :(
234     #"vnc_bind_address",
235     #"network_port"
236     base_control = '%s/%s' % (self._CTRL_DIR, instance.name)
237     monitor_dev = 'unix:%s.monitor,server,nowait' % base_control
238     kvm_cmd.extend(['-monitor', monitor_dev])
239     serial_dev = 'unix:%s.serial,server,nowait' % base_control
240     kvm_cmd.extend(['-serial', serial_dev])
241
242     result = utils.RunCmd(kvm_cmd)
243     if result.failed:
244       raise errors.HypervisorError("Failed to start instance %s: %s (%s)" %
245                                    (instance.name, result.fail_reason,
246                                     result.output))
247
248     if not utils.IsProcessAlive(utils.ReadPidFile(pidfile)):
249       raise errors.HypervisorError("Failed to start instance %s: %s" %
250                                    (instance.name))
251
252     for filename in temp_files:
253       utils.RemoveFile(filename)
254
255   def StopInstance(self, instance, force=False):
256     """Stop an instance.
257
258     """
259     socat_bin = constants.SOCAT_PATH
260     pid_file = self._PIDS_DIR + "/%s" % instance.name
261     pid = utils.ReadPidFile(pid_file)
262     if pid > 0 and utils.IsProcessAlive(pid):
263       if force or not instance.hvparams[constants.HV_ACPI]:
264         utils.KillProcess(pid)
265       else:
266         # This only works if the instance os has acpi support
267         monitor_socket = '%s/%s.monitor'  % (self._CTRL_DIR, instance.name)
268         socat = '%s -u STDIN UNIX-CONNECT:%s' % (socat_bin, monitor_socket)
269         command = "echo 'system_powerdown' | %s" % socat
270         result = utils.RunCmd(command)
271         if result.failed:
272           raise errors.HypervisorError("Failed to stop instance %s: %s" %
273                                        (instance.name, result.fail_reason))
274
275     if not utils.IsProcessAlive(pid):
276       utils.RemoveFile(pid_file)
277
278   def RebootInstance(self, instance):
279     """Reboot an instance.
280
281     """
282     # For some reason if we do a 'send-key ctrl-alt-delete' to the control
283     # socket the instance will stop, but now power up again. So we'll resort
284     # to shutdown and restart.
285     self.StopInstance(instance)
286     self.StartInstance(instance)
287
288   def GetNodeInfo(self):
289     """Return information about the node.
290
291     @return: a dict with the following keys (values in MiB):
292           - memory_total: the total memory size on the node
293           - memory_free: the available memory on the node for instances
294           - memory_dom0: the memory used by the node itself, if available
295
296     """
297     # global ram usage from the xm info command
298     # memory                 : 3583
299     # free_memory            : 747
300     # note: in xen 3, memory has changed to total_memory
301     try:
302       fh = file("/proc/meminfo")
303       try:
304         data = fh.readlines()
305       finally:
306         fh.close()
307     except IOError, err:
308       raise errors.HypervisorError("Failed to list node info: %s" % err)
309
310     result = {}
311     sum_free = 0
312     for line in data:
313       splitfields = line.split(":", 1)
314
315       if len(splitfields) > 1:
316         key = splitfields[0].strip()
317         val = splitfields[1].strip()
318         if key == 'MemTotal':
319           result['memory_total'] = int(val.split()[0])/1024
320         elif key in ('MemFree', 'Buffers', 'Cached'):
321           sum_free += int(val.split()[0])/1024
322         elif key == 'Active':
323           result['memory_dom0'] = int(val.split()[0])/1024
324     result['memory_free'] = sum_free
325
326     cpu_total = 0
327     try:
328       fh = open("/proc/cpuinfo")
329       try:
330         cpu_total = len(re.findall("(?m)^processor\s*:\s*[0-9]+\s*$",
331                                    fh.read()))
332       finally:
333         fh.close()
334     except EnvironmentError, err:
335       raise errors.HypervisorError("Failed to list node info: %s" % err)
336     result['cpu_total'] = cpu_total
337
338     return result
339
340   @staticmethod
341   def GetShellCommandForConsole(instance):
342     """Return a command for connecting to the console of an instance.
343
344     """
345     # TODO: we can either try the serial socket or suggest vnc
346     return "echo Console not available for the kvm hypervisor yet"
347
348   def Verify(self):
349     """Verify the hypervisor.
350
351     Check that the binary exists.
352
353     """
354     if not os.path.exists(constants.KVM_PATH):
355       return "The kvm binary ('%s') does not exist." % constants.KVM_PATH
356     if not os.path.exists(constants.SOCAT_PATH):
357       return "The socat binary ('%s') does not exist." % constants.SOCAT_PATH
358
359
360   @classmethod
361   def CheckParameterSyntax(cls, hvparams):
362     """Check the given parameters for validity.
363
364     For the KVM hypervisor, this only check the existence of the
365     kernel.
366
367     @type hvparams:  dict
368     @param hvparams: dictionary with parameter names/value
369     @raise errors.HypervisorError: when a parameter is not valid
370
371     """
372     super(KVMHypervisor, cls).CheckParameterSyntax(hvparams)
373
374     if not hvparams[constants.HV_KERNEL_PATH]:
375       raise errors.HypervisorError("Need a kernel for the instance")
376
377     if not os.path.isabs(hvparams[constants.HV_KERNEL_PATH]):
378       raise errors.HypervisorError("The kernel path must an absolute path")
379
380     if hvparams[constants.HV_INITRD_PATH]:
381       if not os.path.isabs(hvparams[constants.HV_INITRD_PATH]):
382         raise errors.HypervisorError("The initrd path must an absolute path"
383                                      ", if defined")
384
385   def ValidateParameters(self, hvparams):
386     """Check the given parameters for validity.
387
388     For the KVM hypervisor, this checks the existence of the
389     kernel.
390
391     """
392     super(KVMHypervisor, self).ValidateParameters(hvparams)
393
394     kernel_path = hvparams[constants.HV_KERNEL_PATH]
395     if not os.path.isfile(kernel_path):
396       raise errors.HypervisorError("Instance kernel '%s' not found or"
397                                    " not a file" % kernel_path)
398     initrd_path = hvparams[constants.HV_INITRD_PATH]
399     if initrd_path and not os.path.isfile(initrd_path):
400       raise errors.HypervisorError("Instance initrd '%s' not found or"
401                                    " not a file" % initrd_path)