Xen: NIC parameters
[ganeti-local] / lib / hypervisor / hv_xen.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 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 """Xen hypervisors
23
24 """
25
26 import os
27 import os.path
28 import time
29 import logging
30 from cStringIO import StringIO
31
32 from ganeti import constants
33 from ganeti import errors
34 from ganeti import utils
35 from ganeti.hypervisor import hv_base
36
37
38 class XenHypervisor(hv_base.BaseHypervisor):
39   """Xen generic hypervisor interface
40
41   This is the Xen base class used for both Xen PVM and HVM. It contains
42   all the functionality that is identical for both.
43
44   """
45   REBOOT_RETRY_COUNT = 60
46   REBOOT_RETRY_INTERVAL = 10
47
48   ANCILLARY_FILES = [
49     '/etc/xen/xend-config.sxp',
50     '/etc/xen/scripts/vif-bridge',
51     ]
52
53   @classmethod
54   def _WriteConfigFile(cls, instance, block_devices):
55     """Write the Xen config file for the instance.
56
57     """
58     raise NotImplementedError
59
60   @staticmethod
61   def _WriteConfigFileStatic(instance_name, data):
62     """Write the Xen config file for the instance.
63
64     This version of the function just writes the config file from static data.
65
66     """
67     utils.WriteFile("/etc/xen/%s" % instance_name, data=data)
68
69   @staticmethod
70   def _ReadConfigFile(instance_name):
71     """Returns the contents of the instance config file.
72
73     """
74     try:
75       file_content = utils.ReadFile("/etc/xen/%s" % instance_name)
76     except EnvironmentError, err:
77       raise errors.HypervisorError("Failed to load Xen config file: %s" % err)
78     return file_content
79
80   @staticmethod
81   def _RemoveConfigFile(instance_name):
82     """Remove the xen configuration file.
83
84     """
85     utils.RemoveFile("/etc/xen/%s" % instance_name)
86
87   @staticmethod
88   def _GetXMList(include_node):
89     """Return the list of running instances.
90
91     If the include_node argument is True, then we return information
92     for dom0 also, otherwise we filter that from the return value.
93
94     @return: list of (name, id, memory, vcpus, state, time spent)
95
96     """
97     for dummy in range(5):
98       result = utils.RunCmd(["xm", "list"])
99       if not result.failed:
100         break
101       logging.error("xm list failed (%s): %s", result.fail_reason,
102                     result.output)
103       time.sleep(1)
104
105     if result.failed:
106       raise errors.HypervisorError("xm list failed, retries"
107                                    " exceeded (%s): %s" %
108                                    (result.fail_reason, result.output))
109
110     # skip over the heading
111     lines = result.stdout.splitlines()[1:]
112     result = []
113     for line in lines:
114       # The format of lines is:
115       # Name      ID Mem(MiB) VCPUs State  Time(s)
116       # Domain-0   0  3418     4 r-----    266.2
117       data = line.split()
118       if len(data) != 6:
119         raise errors.HypervisorError("Can't parse output of xm list,"
120                                      " line: %s" % line)
121       try:
122         data[1] = int(data[1])
123         data[2] = int(data[2])
124         data[3] = int(data[3])
125         data[5] = float(data[5])
126       except ValueError, err:
127         raise errors.HypervisorError("Can't parse output of xm list,"
128                                      " line: %s, error: %s" % (line, err))
129
130       # skip the Domain-0 (optional)
131       if include_node or data[0] != 'Domain-0':
132         result.append(data)
133
134     return result
135
136   def ListInstances(self):
137     """Get the list of running instances.
138
139     """
140     xm_list = self._GetXMList(False)
141     names = [info[0] for info in xm_list]
142     return names
143
144   def GetInstanceInfo(self, instance_name):
145     """Get instance properties.
146
147     @param instance_name: the instance name
148
149     @return: tuple (name, id, memory, vcpus, stat, times)
150
151     """
152     xm_list = self._GetXMList(instance_name=="Domain-0")
153     result = None
154     for data in xm_list:
155       if data[0] == instance_name:
156         result = data
157         break
158     return result
159
160   def GetAllInstancesInfo(self):
161     """Get properties of all instances.
162
163     @return: list of tuples (name, id, memory, vcpus, stat, times)
164
165     """
166     xm_list = self._GetXMList(False)
167     return xm_list
168
169   def StartInstance(self, instance, block_devices):
170     """Start an instance.
171
172     """
173     self._WriteConfigFile(instance, block_devices)
174     result = utils.RunCmd(["xm", "create", instance.name])
175
176     if result.failed:
177       raise errors.HypervisorError("Failed to start instance %s: %s (%s)" %
178                                    (instance.name, result.fail_reason,
179                                     result.output))
180
181   def StopInstance(self, instance, force=False):
182     """Stop an instance.
183
184     """
185     self._RemoveConfigFile(instance.name)
186     if force:
187       command = ["xm", "destroy", instance.name]
188     else:
189       command = ["xm", "shutdown", instance.name]
190     result = utils.RunCmd(command)
191
192     if result.failed:
193       raise errors.HypervisorError("Failed to stop instance %s: %s, %s" %
194                                    (instance.name, result.fail_reason,
195                                     result.output))
196
197   def RebootInstance(self, instance):
198     """Reboot an instance.
199
200     """
201     ini_info = self.GetInstanceInfo(instance.name)
202     result = utils.RunCmd(["xm", "reboot", instance.name])
203
204     if result.failed:
205       raise errors.HypervisorError("Failed to reboot instance %s: %s, %s" %
206                                    (instance.name, result.fail_reason,
207                                     result.output))
208     done = False
209     retries = self.REBOOT_RETRY_COUNT
210     while retries > 0:
211       new_info = self.GetInstanceInfo(instance.name)
212       # check if the domain ID has changed or the run time has
213       # decreased
214       if new_info[1] != ini_info[1] or new_info[5] < ini_info[5]:
215         done = True
216         break
217       time.sleep(self.REBOOT_RETRY_INTERVAL)
218       retries -= 1
219
220     if not done:
221       raise errors.HypervisorError("Failed to reboot instance %s: instance"
222                                    " did not reboot in the expected interval" %
223                                    (instance.name, ))
224
225   def GetNodeInfo(self):
226     """Return information about the node.
227
228     @return: a dict with the following keys (memory values in MiB):
229           - memory_total: the total memory size on the node
230           - memory_free: the available memory on the node for instances
231           - memory_dom0: the memory used by the node itself, if available
232           - nr_cpus: total number of CPUs
233           - nr_nodes: in a NUMA system, the number of domains
234           - nr_sockets: the number of physical CPU sockets in the node
235
236     """
237     # note: in xen 3, memory has changed to total_memory
238     result = utils.RunCmd(["xm", "info"])
239     if result.failed:
240       logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
241                     result.output)
242       return None
243
244     xmoutput = result.stdout.splitlines()
245     result = {}
246     cores_per_socket = threads_per_core = nr_cpus = None
247     for line in xmoutput:
248       splitfields = line.split(":", 1)
249
250       if len(splitfields) > 1:
251         key = splitfields[0].strip()
252         val = splitfields[1].strip()
253         if key == 'memory' or key == 'total_memory':
254           result['memory_total'] = int(val)
255         elif key == 'free_memory':
256           result['memory_free'] = int(val)
257         elif key == 'nr_cpus':
258           nr_cpus = result['cpu_total'] = int(val)
259         elif key == 'nr_nodes':
260           result['cpu_nodes'] = int(val)
261         elif key == 'cores_per_socket':
262           cores_per_socket = int(val)
263         elif key == 'threads_per_core':
264           threads_per_core = int(val)
265
266     if (cores_per_socket is not None and
267         threads_per_core is not None and nr_cpus is not None):
268       result['cpu_sockets'] = nr_cpus / (cores_per_socket * threads_per_core)
269
270     dom0_info = self.GetInstanceInfo("Domain-0")
271     if dom0_info is not None:
272       result['memory_dom0'] = dom0_info[2]
273
274     return result
275
276   @classmethod
277   def GetShellCommandForConsole(cls, instance, hvparams, beparams):
278     """Return a command for connecting to the console of an instance.
279
280     """
281     return "xm console %s" % instance.name
282
283
284   def Verify(self):
285     """Verify the hypervisor.
286
287     For Xen, this verifies that the xend process is running.
288
289     """
290     result = utils.RunCmd(["xm", "info"])
291     if result.failed:
292       return "'xm info' failed: %s, %s" % (result.fail_reason, result.output)
293
294   @staticmethod
295   def _GetConfigFileDiskData(disk_template, block_devices):
296     """Get disk directive for xen config file.
297
298     This method builds the xen config disk directive according to the
299     given disk_template and block_devices.
300
301     @param disk_template: string containing instance disk template
302     @param block_devices: list of tuples (cfdev, rldev):
303         - cfdev: dict containing ganeti config disk part
304         - rldev: ganeti.bdev.BlockDev object
305
306     @return: string containing disk directive for xen instance config file
307
308     """
309     FILE_DRIVER_MAP = {
310       constants.FD_LOOP: "file",
311       constants.FD_BLKTAP: "tap:aio",
312       }
313     disk_data = []
314     if len(block_devices) > 24:
315       # 'z' - 'a' = 24
316       raise errors.HypervisorError("Too many disks")
317     # FIXME: instead of this hardcoding here, each of PVM/HVM should
318     # directly export their info (currently HVM will just sed this info)
319     namespace = ["sd" + chr(i + ord('a')) for i in range(24)]
320     for sd_name, (cfdev, dev_path) in zip(namespace, block_devices):
321       if cfdev.mode == constants.DISK_RDWR:
322         mode = "w"
323       else:
324         mode = "r"
325       if cfdev.dev_type == constants.LD_FILE:
326         line = "'%s:%s,%s,%s'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
327                                   dev_path, sd_name, mode)
328       else:
329         line = "'phy:%s,%s,%s'" % (dev_path, sd_name, mode)
330       disk_data.append(line)
331
332     return disk_data
333
334   def MigrationInfo(self, instance):
335     """Get instance information to perform a migration.
336
337     @type instance: L{objects.Instance}
338     @param instance: instance to be migrated
339     @rtype: string
340     @return: content of the xen config file
341
342     """
343     return self._ReadConfigFile(instance.name)
344
345   def AcceptInstance(self, instance, info, target):
346     """Prepare to accept an instance.
347
348     @type instance: L{objects.Instance}
349     @param instance: instance to be accepted
350     @type info: string
351     @param info: content of the xen config file on the source node
352     @type target: string
353     @param target: target host (usually ip), on this node
354
355     """
356     pass
357
358   def FinalizeMigration(self, instance, info, success):
359     """Finalize an instance migration.
360
361     After a successful migration we write the xen config file.
362     We do nothing on a failure, as we did not change anything at accept time.
363
364     @type instance: L{objects.Instance}
365     @param instance: instance whose migration is being aborted
366     @type info: string
367     @param info: content of the xen config file on the source node
368     @type success: boolean
369     @param success: whether the migration was a success or a failure
370
371     """
372     if success:
373       self._WriteConfigFileStatic(instance.name, info)
374
375   def MigrateInstance(self, instance, target, live):
376     """Migrate an instance to a target node.
377
378     The migration will not be attempted if the instance is not
379     currently running.
380
381     @type instance: string
382     @param instance: instance name
383     @type target: string
384     @param target: ip address of the target node
385     @type live: boolean
386     @param live: perform a live migration
387
388     """
389     if self.GetInstanceInfo(instance) is None:
390       raise errors.HypervisorError("Instance not running, cannot migrate")
391     args = ["xm", "migrate"]
392     if live:
393       args.append("-l")
394     args.extend([instance, target])
395     result = utils.RunCmd(args)
396     if result.failed:
397       raise errors.HypervisorError("Failed to migrate instance %s: %s" %
398                                    (instance, result.output))
399     # remove old xen file after migration succeeded
400     try:
401       self._RemoveConfigFile(instance)
402     except EnvironmentError:
403       logging.exception("Failure while removing instance config file")
404
405   @classmethod
406   def PowercycleNode(cls):
407     """Xen-specific powercycle.
408
409     This first does a Linux reboot (which triggers automatically a Xen
410     reboot), and if that fails it tries to do a Xen reboot. The reason
411     we don't try a Xen reboot first is that the xen reboot launches an
412     external command which connects to the Xen hypervisor, and that
413     won't work in case the root filesystem is broken and/or the xend
414     daemon is not working.
415
416     """
417     try:
418       cls.LinuxPowercycle()
419     finally:
420       utils.RunCmd(["xm", "debug", "R"])
421
422
423 class XenPvmHypervisor(XenHypervisor):
424   """Xen PVM hypervisor interface"""
425
426   PARAMETERS = {
427     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
428     constants.HV_INITRD_PATH: hv_base.OPT_FILE_CHECK,
429     constants.HV_ROOT_PATH: hv_base.REQUIRED_CHECK,
430     constants.HV_KERNEL_ARGS: hv_base.NO_CHECK,
431     }
432
433   @classmethod
434   def _WriteConfigFile(cls, instance, block_devices):
435     """Write the Xen config file for the instance.
436
437     """
438     hvp = instance.hvparams
439     config = StringIO()
440     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
441
442     # kernel handling
443     kpath = hvp[constants.HV_KERNEL_PATH]
444     config.write("kernel = '%s'\n" % kpath)
445
446     # initrd handling
447     initrd_path = hvp[constants.HV_INITRD_PATH]
448     if initrd_path:
449       config.write("ramdisk = '%s'\n" % initrd_path)
450
451     # rest of the settings
452     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
453     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
454     config.write("name = '%s'\n" % instance.name)
455
456     vif_data = []
457     for nic in instance.nics:
458       nic_str = "mac=%s" % (nic.mac)
459       ip = getattr(nic, "ip", None)
460       if ip is not None:
461         nic_str += ", ip=%s" % ip
462       vif_data.append("'%s'" % nic_str)
463       if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
464         nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
465
466     config.write("vif = [%s]\n" % ",".join(vif_data))
467     config.write("disk = [%s]\n" % ",".join(
468                  cls._GetConfigFileDiskData(instance.disk_template,
469                                             block_devices)))
470
471     config.write("root = '%s'\n" % hvp[constants.HV_ROOT_PATH])
472     config.write("on_poweroff = 'destroy'\n")
473     config.write("on_reboot = 'restart'\n")
474     config.write("on_crash = 'restart'\n")
475     config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
476     # just in case it exists
477     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
478     try:
479       utils.WriteFile("/etc/xen/%s" % instance.name, data=config.getvalue())
480     except EnvironmentError, err:
481       raise errors.HypervisorError("Cannot write Xen instance confile"
482                                    " file /etc/xen/%s: %s" %
483                                    (instance.name, err))
484
485     return True
486
487
488 class XenHvmHypervisor(XenHypervisor):
489   """Xen HVM hypervisor interface"""
490
491   ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + \
492     [constants.VNC_PASSWORD_FILE]
493
494   PARAMETERS = {
495     constants.HV_ACPI: hv_base.NO_CHECK,
496     constants.HV_BOOT_ORDER: (True, ) + \
497     (lambda x: x and len(x.strip("acdn")) == 0,
498      "Invalid boot order specified, must be one or more of [acdn]",
499      None, None),
500     constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
501     constants.HV_DISK_TYPE: \
502     hv_base.ParamInSet(True, constants.HT_HVM_VALID_DISK_TYPES),
503     constants.HV_NIC_TYPE: \
504     hv_base.ParamInSet(True, constants.HT_HVM_VALID_NIC_TYPES),
505     constants.HV_PAE: hv_base.NO_CHECK,
506     constants.HV_VNC_BIND_ADDRESS: \
507     (False, utils.IsValidIP,
508      "VNC bind address is not a valid IP address", None, None),
509     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
510     constants.HV_DEVICE_MODEL: hv_base.REQ_FILE_CHECK,
511     }
512
513   @classmethod
514   def _WriteConfigFile(cls, instance, block_devices):
515     """Create a Xen 3.1 HVM config file.
516
517     """
518     hvp = instance.hvparams
519
520     config = StringIO()
521     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
522
523     # kernel handling
524     kpath = hvp[constants.HV_KERNEL_PATH]
525     config.write("kernel = '%s'\n" % kpath)
526
527     config.write("builder = 'hvm'\n")
528     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
529     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
530     config.write("name = '%s'\n" % instance.name)
531     if hvp[constants.HV_PAE]:
532       config.write("pae = 1\n")
533     else:
534       config.write("pae = 0\n")
535     if hvp[constants.HV_ACPI]:
536       config.write("acpi = 1\n")
537     else:
538       config.write("acpi = 0\n")
539     config.write("apic = 1\n")
540     config.write("device_model = '%s'\n" % hvp[constants.HV_DEVICE_MODEL])
541     config.write("boot = '%s'\n" % hvp[constants.HV_BOOT_ORDER])
542     config.write("sdl = 0\n")
543     config.write("usb = 1\n")
544     config.write("usbdevice = 'tablet'\n")
545     config.write("vnc = 1\n")
546     if hvp[constants.HV_VNC_BIND_ADDRESS] is None:
547       config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
548     else:
549       config.write("vnclisten = '%s'\n" % hvp[constants.HV_VNC_BIND_ADDRESS])
550
551     if instance.network_port > constants.VNC_BASE_PORT:
552       display = instance.network_port - constants.VNC_BASE_PORT
553       config.write("vncdisplay = %s\n" % display)
554       config.write("vncunused = 0\n")
555     else:
556       config.write("# vncdisplay = 1\n")
557       config.write("vncunused = 1\n")
558
559     try:
560       password = utils.ReadFile(constants.VNC_PASSWORD_FILE)
561     except EnvironmentError, err:
562       raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
563                                    (constants.VNC_PASSWORD_FILE, err))
564
565     config.write("vncpasswd = '%s'\n" % password.rstrip())
566
567     config.write("serial = 'pty'\n")
568     config.write("localtime = 1\n")
569
570     vif_data = []
571     nic_type = hvp[constants.HV_NIC_TYPE]
572     if nic_type is None:
573       # ensure old instances don't change
574       nic_type_str = ", type=ioemu"
575     elif nic_type == constants.HT_NIC_PARAVIRTUAL:
576       nic_type_str = ", type=paravirtualized"
577     else:
578       nic_type_str = ", model=%s, type=ioemu" % nic_type
579     for nic in instance.nics:
580       nic_str = "mac=%s%s" % (nic.mac, nic_type_str)
581       ip = getattr(nic, "ip", None)
582       if ip is not None:
583         nic_str += ", ip=%s" % ip
584       vif_data.append("'%s'" % nic_str)
585       if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
586         nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
587
588     config.write("vif = [%s]\n" % ",".join(vif_data))
589     disk_data = cls._GetConfigFileDiskData(instance.disk_template,
590                                             block_devices)
591     disk_type = hvp[constants.HV_DISK_TYPE]
592     if disk_type in (None, constants.HT_DISK_IOEMU):
593       replacement = ",ioemu:hd"
594     else:
595       replacement = ",hd"
596     disk_data = [line.replace(",sd", replacement) for line in disk_data]
597     iso_path = hvp[constants.HV_CDROM_IMAGE_PATH]
598     if iso_path:
599       iso = "'file:%s,hdc:cdrom,r'" % iso_path
600       disk_data.append(iso)
601
602     config.write("disk = [%s]\n" % (",".join(disk_data)))
603
604     config.write("on_poweroff = 'destroy'\n")
605     config.write("on_reboot = 'restart'\n")
606     config.write("on_crash = 'restart'\n")
607     # just in case it exists
608     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
609     try:
610       utils.WriteFile("/etc/xen/%s" % instance.name,
611                       data=config.getvalue())
612     except EnvironmentError, err:
613       raise errors.HypervisorError("Cannot write Xen instance confile"
614                                    " file /etc/xen/%s: %s" %
615                                    (instance.name, err))
616
617     return True