LUSetInstanceParams: save cluster
[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, bridge=%s" % (nic.mac, nic.bridge)
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
464     config.write("vif = [%s]\n" % ",".join(vif_data))
465     config.write("disk = [%s]\n" % ",".join(
466                  cls._GetConfigFileDiskData(instance.disk_template,
467                                             block_devices)))
468
469     config.write("root = '%s'\n" % hvp[constants.HV_ROOT_PATH])
470     config.write("on_poweroff = 'destroy'\n")
471     config.write("on_reboot = 'restart'\n")
472     config.write("on_crash = 'restart'\n")
473     config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
474     # just in case it exists
475     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
476     try:
477       utils.WriteFile("/etc/xen/%s" % instance.name, data=config.getvalue())
478     except EnvironmentError, err:
479       raise errors.HypervisorError("Cannot write Xen instance confile"
480                                    " file /etc/xen/%s: %s" %
481                                    (instance.name, err))
482
483     return True
484
485
486 class XenHvmHypervisor(XenHypervisor):
487   """Xen HVM hypervisor interface"""
488
489   ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + \
490     [constants.VNC_PASSWORD_FILE]
491
492   PARAMETERS = {
493     constants.HV_ACPI: hv_base.NO_CHECK,
494     constants.HV_BOOT_ORDER: (True, ) + \
495     (lambda x: x and len(x.strip("acdn")) == 0,
496      "Invalid boot order specified, must be one or more of [acdn]",
497      None, None),
498     constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
499     constants.HV_DISK_TYPE: \
500     hv_base.ParamInSet(True, constants.HT_HVM_VALID_DISK_TYPES),
501     constants.HV_NIC_TYPE: \
502     hv_base.ParamInSet(True, constants.HT_HVM_VALID_NIC_TYPES),
503     constants.HV_PAE: hv_base.NO_CHECK,
504     constants.HV_VNC_BIND_ADDRESS: \
505     (False, utils.IsValidIP,
506      "VNC bind address is not a valid IP address", None, None),
507     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
508     constants.HV_DEVICE_MODEL: hv_base.REQ_FILE_CHECK,
509     }
510
511   @classmethod
512   def _WriteConfigFile(cls, instance, block_devices):
513     """Create a Xen 3.1 HVM config file.
514
515     """
516     hvp = instance.hvparams
517
518     config = StringIO()
519     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
520
521     # kernel handling
522     kpath = hvp[constants.HV_KERNEL_PATH]
523     config.write("kernel = '%s'\n" % kpath)
524
525     config.write("builder = 'hvm'\n")
526     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
527     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
528     config.write("name = '%s'\n" % instance.name)
529     if hvp[constants.HV_PAE]:
530       config.write("pae = 1\n")
531     else:
532       config.write("pae = 0\n")
533     if hvp[constants.HV_ACPI]:
534       config.write("acpi = 1\n")
535     else:
536       config.write("acpi = 0\n")
537     config.write("apic = 1\n")
538     config.write("device_model = '%s'\n" % hvp[constants.HV_DEVICE_MODEL])
539     config.write("boot = '%s'\n" % hvp[constants.HV_BOOT_ORDER])
540     config.write("sdl = 0\n")
541     config.write("usb = 1\n")
542     config.write("usbdevice = 'tablet'\n")
543     config.write("vnc = 1\n")
544     if hvp[constants.HV_VNC_BIND_ADDRESS] is None:
545       config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
546     else:
547       config.write("vnclisten = '%s'\n" % hvp[constants.HV_VNC_BIND_ADDRESS])
548
549     if instance.network_port > constants.VNC_BASE_PORT:
550       display = instance.network_port - constants.VNC_BASE_PORT
551       config.write("vncdisplay = %s\n" % display)
552       config.write("vncunused = 0\n")
553     else:
554       config.write("# vncdisplay = 1\n")
555       config.write("vncunused = 1\n")
556
557     try:
558       password = utils.ReadFile(constants.VNC_PASSWORD_FILE)
559     except EnvironmentError, err:
560       raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
561                                    (constants.VNC_PASSWORD_FILE, err))
562
563     config.write("vncpasswd = '%s'\n" % password.rstrip())
564
565     config.write("serial = 'pty'\n")
566     config.write("localtime = 1\n")
567
568     vif_data = []
569     nic_type = hvp[constants.HV_NIC_TYPE]
570     if nic_type is None:
571       # ensure old instances don't change
572       nic_type_str = ", type=ioemu"
573     elif nic_type == constants.HT_NIC_PARAVIRTUAL:
574       nic_type_str = ", type=paravirtualized"
575     else:
576       nic_type_str = ", model=%s, type=ioemu" % nic_type
577     for nic in instance.nics:
578       nic_str = "mac=%s, bridge=%s%s" % (nic.mac, nic.bridge, nic_type_str)
579       ip = getattr(nic, "ip", None)
580       if ip is not None:
581         nic_str += ", ip=%s" % ip
582       vif_data.append("'%s'" % nic_str)
583
584     config.write("vif = [%s]\n" % ",".join(vif_data))
585     disk_data = cls._GetConfigFileDiskData(instance.disk_template,
586                                             block_devices)
587     disk_type = hvp[constants.HV_DISK_TYPE]
588     if disk_type in (None, constants.HT_DISK_IOEMU):
589       replacement = ",ioemu:hd"
590     else:
591       replacement = ",hd"
592     disk_data = [line.replace(",sd", replacement) for line in disk_data]
593     iso_path = hvp[constants.HV_CDROM_IMAGE_PATH]
594     if iso_path:
595       iso = "'file:%s,hdc:cdrom,r'" % iso_path
596       disk_data.append(iso)
597
598     config.write("disk = [%s]\n" % (",".join(disk_data)))
599
600     config.write("on_poweroff = 'destroy'\n")
601     config.write("on_reboot = 'restart'\n")
602     config.write("on_crash = 'restart'\n")
603     # just in case it exists
604     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
605     try:
606       utils.WriteFile("/etc/xen/%s" % instance.name,
607                       data=config.getvalue())
608     except EnvironmentError, err:
609       raise errors.HypervisorError("Cannot write Xen instance confile"
610                                    " file /etc/xen/%s: %s" %
611                                    (instance.name, err))
612
613     return True