Fix heading to the one of epydoc
[ganeti-local] / lib / backend.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 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 """Functions used by the node daemon
23
24 @var _ALLOWED_UPLOAD_FILES: denotes which files are accepted in
25      the L{UploadFile} function
26 @var _ALLOWED_CLEAN_DIRS: denotes which directories are accepted
27      in the L{_CleanDirectory} function
28
29 """
30
31 # pylint: disable-msg=E1103
32
33 # E1103: %s %r has no %r member (but some types could not be
34 # inferred), because the _TryOSFromDisk returns either (True, os_obj)
35 # or (False, "string") which confuses pylint
36
37
38 import os
39 import os.path
40 import shutil
41 import time
42 import stat
43 import errno
44 import re
45 import random
46 import logging
47 import tempfile
48 import zlib
49 import base64
50
51 from ganeti import errors
52 from ganeti import utils
53 from ganeti import ssh
54 from ganeti import hypervisor
55 from ganeti import constants
56 from ganeti import bdev
57 from ganeti import objects
58 from ganeti import ssconf
59
60
61 _BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id"
62 _ALLOWED_CLEAN_DIRS = frozenset([
63   constants.DATA_DIR,
64   constants.JOB_QUEUE_ARCHIVE_DIR,
65   constants.QUEUE_DIR,
66   ])
67
68
69 class RPCFail(Exception):
70   """Class denoting RPC failure.
71
72   Its argument is the error message.
73
74   """
75
76
77 def _Fail(msg, *args, **kwargs):
78   """Log an error and the raise an RPCFail exception.
79
80   This exception is then handled specially in the ganeti daemon and
81   turned into a 'failed' return type. As such, this function is a
82   useful shortcut for logging the error and returning it to the master
83   daemon.
84
85   @type msg: string
86   @param msg: the text of the exception
87   @raise RPCFail
88
89   """
90   if args:
91     msg = msg % args
92   if "log" not in kwargs or kwargs["log"]: # if we should log this error
93     if "exc" in kwargs and kwargs["exc"]:
94       logging.exception(msg)
95     else:
96       logging.error(msg)
97   raise RPCFail(msg)
98
99
100 def _GetConfig():
101   """Simple wrapper to return a SimpleStore.
102
103   @rtype: L{ssconf.SimpleStore}
104   @return: a SimpleStore instance
105
106   """
107   return ssconf.SimpleStore()
108
109
110 def _GetSshRunner(cluster_name):
111   """Simple wrapper to return an SshRunner.
112
113   @type cluster_name: str
114   @param cluster_name: the cluster name, which is needed
115       by the SshRunner constructor
116   @rtype: L{ssh.SshRunner}
117   @return: an SshRunner instance
118
119   """
120   return ssh.SshRunner(cluster_name)
121
122
123 def _Decompress(data):
124   """Unpacks data compressed by the RPC client.
125
126   @type data: list or tuple
127   @param data: Data sent by RPC client
128   @rtype: str
129   @return: Decompressed data
130
131   """
132   assert isinstance(data, (list, tuple))
133   assert len(data) == 2
134   (encoding, content) = data
135   if encoding == constants.RPC_ENCODING_NONE:
136     return content
137   elif encoding == constants.RPC_ENCODING_ZLIB_BASE64:
138     return zlib.decompress(base64.b64decode(content))
139   else:
140     raise AssertionError("Unknown data encoding")
141
142
143 def _CleanDirectory(path, exclude=None):
144   """Removes all regular files in a directory.
145
146   @type path: str
147   @param path: the directory to clean
148   @type exclude: list
149   @param exclude: list of files to be excluded, defaults
150       to the empty list
151
152   """
153   if path not in _ALLOWED_CLEAN_DIRS:
154     _Fail("Path passed to _CleanDirectory not in allowed clean targets: '%s'",
155           path)
156
157   if not os.path.isdir(path):
158     return
159   if exclude is None:
160     exclude = []
161   else:
162     # Normalize excluded paths
163     exclude = [os.path.normpath(i) for i in exclude]
164
165   for rel_name in utils.ListVisibleFiles(path):
166     full_name = utils.PathJoin(path, rel_name)
167     if full_name in exclude:
168       continue
169     if os.path.isfile(full_name) and not os.path.islink(full_name):
170       utils.RemoveFile(full_name)
171
172
173 def _BuildUploadFileList():
174   """Build the list of allowed upload files.
175
176   This is abstracted so that it's built only once at module import time.
177
178   """
179   allowed_files = set([
180     constants.CLUSTER_CONF_FILE,
181     constants.ETC_HOSTS,
182     constants.SSH_KNOWN_HOSTS_FILE,
183     constants.VNC_PASSWORD_FILE,
184     constants.RAPI_CERT_FILE,
185     constants.RAPI_USERS_FILE,
186     constants.CONFD_HMAC_KEY,
187     ])
188
189   for hv_name in constants.HYPER_TYPES:
190     hv_class = hypervisor.GetHypervisorClass(hv_name)
191     allowed_files.update(hv_class.GetAncillaryFiles())
192
193   return frozenset(allowed_files)
194
195
196 _ALLOWED_UPLOAD_FILES = _BuildUploadFileList()
197
198
199 def JobQueuePurge():
200   """Removes job queue files and archived jobs.
201
202   @rtype: tuple
203   @return: True, None
204
205   """
206   _CleanDirectory(constants.QUEUE_DIR, exclude=[constants.JOB_QUEUE_LOCK_FILE])
207   _CleanDirectory(constants.JOB_QUEUE_ARCHIVE_DIR)
208
209
210 def GetMasterInfo():
211   """Returns master information.
212
213   This is an utility function to compute master information, either
214   for consumption here or from the node daemon.
215
216   @rtype: tuple
217   @return: master_netdev, master_ip, master_name
218   @raise RPCFail: in case of errors
219
220   """
221   try:
222     cfg = _GetConfig()
223     master_netdev = cfg.GetMasterNetdev()
224     master_ip = cfg.GetMasterIP()
225     master_node = cfg.GetMasterNode()
226   except errors.ConfigurationError, err:
227     _Fail("Cluster configuration incomplete: %s", err, exc=True)
228   return (master_netdev, master_ip, master_node)
229
230
231 def StartMaster(start_daemons, no_voting):
232   """Activate local node as master node.
233
234   The function will always try activate the IP address of the master
235   (unless someone else has it). It will also start the master daemons,
236   based on the start_daemons parameter.
237
238   @type start_daemons: boolean
239   @param start_daemons: whether to also start the master
240       daemons (ganeti-masterd and ganeti-rapi)
241   @type no_voting: boolean
242   @param no_voting: whether to start ganeti-masterd without a node vote
243       (if start_daemons is True), but still non-interactively
244   @rtype: None
245
246   """
247   # GetMasterInfo will raise an exception if not able to return data
248   master_netdev, master_ip, _ = GetMasterInfo()
249
250   err_msgs = []
251   if utils.TcpPing(master_ip, constants.DEFAULT_NODED_PORT):
252     if utils.OwnIpAddress(master_ip):
253       # we already have the ip:
254       logging.debug("Master IP already configured, doing nothing")
255     else:
256       msg = "Someone else has the master ip, not activating"
257       logging.error(msg)
258       err_msgs.append(msg)
259   else:
260     result = utils.RunCmd(["ip", "address", "add", "%s/32" % master_ip,
261                            "dev", master_netdev, "label",
262                            "%s:0" % master_netdev])
263     if result.failed:
264       msg = "Can't activate master IP: %s" % result.output
265       logging.error(msg)
266       err_msgs.append(msg)
267
268     result = utils.RunCmd(["arping", "-q", "-U", "-c 3", "-I", master_netdev,
269                            "-s", master_ip, master_ip])
270     # we'll ignore the exit code of arping
271
272   # and now start the master and rapi daemons
273   if start_daemons:
274     if no_voting:
275       masterd_args = "--no-voting --yes-do-it"
276     else:
277       masterd_args = ""
278
279     env = {
280       "EXTRA_MASTERD_ARGS": masterd_args,
281       }
282
283     result = utils.RunCmd([constants.DAEMON_UTIL, "start-master"], env=env)
284     if result.failed:
285       msg = "Can't start Ganeti master: %s" % result.output
286       logging.error(msg)
287       err_msgs.append(msg)
288
289   if err_msgs:
290     _Fail("; ".join(err_msgs))
291
292
293 def StopMaster(stop_daemons):
294   """Deactivate this node as master.
295
296   The function will always try to deactivate the IP address of the
297   master. It will also stop the master daemons depending on the
298   stop_daemons parameter.
299
300   @type stop_daemons: boolean
301   @param stop_daemons: whether to also stop the master daemons
302       (ganeti-masterd and ganeti-rapi)
303   @rtype: None
304
305   """
306   # TODO: log and report back to the caller the error failures; we
307   # need to decide in which case we fail the RPC for this
308
309   # GetMasterInfo will raise an exception if not able to return data
310   master_netdev, master_ip, _ = GetMasterInfo()
311
312   result = utils.RunCmd(["ip", "address", "del", "%s/32" % master_ip,
313                          "dev", master_netdev])
314   if result.failed:
315     logging.error("Can't remove the master IP, error: %s", result.output)
316     # but otherwise ignore the failure
317
318   if stop_daemons:
319     result = utils.RunCmd([constants.DAEMON_UTIL, "stop-master"])
320     if result.failed:
321       logging.error("Could not stop Ganeti master, command %s had exitcode %s"
322                     " and error %s",
323                     result.cmd, result.exit_code, result.output)
324
325
326 def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
327   """Joins this node to the cluster.
328
329   This does the following:
330       - updates the hostkeys of the machine (rsa and dsa)
331       - adds the ssh private key to the user
332       - adds the ssh public key to the users' authorized_keys file
333
334   @type dsa: str
335   @param dsa: the DSA private key to write
336   @type dsapub: str
337   @param dsapub: the DSA public key to write
338   @type rsa: str
339   @param rsa: the RSA private key to write
340   @type rsapub: str
341   @param rsapub: the RSA public key to write
342   @type sshkey: str
343   @param sshkey: the SSH private key to write
344   @type sshpub: str
345   @param sshpub: the SSH public key to write
346   @rtype: boolean
347   @return: the success of the operation
348
349   """
350   sshd_keys =  [(constants.SSH_HOST_RSA_PRIV, rsa, 0600),
351                 (constants.SSH_HOST_RSA_PUB, rsapub, 0644),
352                 (constants.SSH_HOST_DSA_PRIV, dsa, 0600),
353                 (constants.SSH_HOST_DSA_PUB, dsapub, 0644)]
354   for name, content, mode in sshd_keys:
355     utils.WriteFile(name, data=content, mode=mode)
356
357   try:
358     priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS,
359                                                     mkdir=True)
360   except errors.OpExecError, err:
361     _Fail("Error while processing user ssh files: %s", err, exc=True)
362
363   for name, content in [(priv_key, sshkey), (pub_key, sshpub)]:
364     utils.WriteFile(name, data=content, mode=0600)
365
366   utils.AddAuthorizedKey(auth_keys, sshpub)
367
368   result = utils.RunCmd([constants.DAEMON_UTIL, "reload-ssh-keys"])
369   if result.failed:
370     _Fail("Unable to reload SSH keys (command %r, exit code %s, output %r)",
371           result.cmd, result.exit_code, result.output)
372
373
374 def LeaveCluster(modify_ssh_setup):
375   """Cleans up and remove the current node.
376
377   This function cleans up and prepares the current node to be removed
378   from the cluster.
379
380   If processing is successful, then it raises an
381   L{errors.QuitGanetiException} which is used as a special case to
382   shutdown the node daemon.
383
384   @param modify_ssh_setup: boolean
385
386   """
387   _CleanDirectory(constants.DATA_DIR)
388   JobQueuePurge()
389
390   if modify_ssh_setup:
391     try:
392       priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
393
394       utils.RemoveAuthorizedKey(auth_keys, utils.ReadFile(pub_key))
395
396       utils.RemoveFile(priv_key)
397       utils.RemoveFile(pub_key)
398     except errors.OpExecError:
399       logging.exception("Error while processing ssh files")
400
401   try:
402     utils.RemoveFile(constants.CONFD_HMAC_KEY)
403     utils.RemoveFile(constants.RAPI_CERT_FILE)
404     utils.RemoveFile(constants.NODED_CERT_FILE)
405   except: # pylint: disable-msg=W0702
406     logging.exception("Error while removing cluster secrets")
407
408   result = utils.RunCmd([constants.DAEMON_UTIL, "stop", constants.CONFD])
409   if result.failed:
410     logging.error("Command %s failed with exitcode %s and error %s",
411                   result.cmd, result.exit_code, result.output)
412
413   # Raise a custom exception (handled in ganeti-noded)
414   raise errors.QuitGanetiException(True, 'Shutdown scheduled')
415
416
417 def GetNodeInfo(vgname, hypervisor_type):
418   """Gives back a hash with different information about the node.
419
420   @type vgname: C{string}
421   @param vgname: the name of the volume group to ask for disk space information
422   @type hypervisor_type: C{str}
423   @param hypervisor_type: the name of the hypervisor to ask for
424       memory information
425   @rtype: C{dict}
426   @return: dictionary with the following keys:
427       - vg_size is the size of the configured volume group in MiB
428       - vg_free is the free size of the volume group in MiB
429       - memory_dom0 is the memory allocated for domain0 in MiB
430       - memory_free is the currently available (free) ram in MiB
431       - memory_total is the total number of ram in MiB
432
433   """
434   outputarray = {}
435   vginfo = _GetVGInfo(vgname)
436   outputarray['vg_size'] = vginfo['vg_size']
437   outputarray['vg_free'] = vginfo['vg_free']
438
439   hyper = hypervisor.GetHypervisor(hypervisor_type)
440   hyp_info = hyper.GetNodeInfo()
441   if hyp_info is not None:
442     outputarray.update(hyp_info)
443
444   outputarray["bootid"] = utils.ReadFile(_BOOT_ID_PATH, size=128).rstrip("\n")
445
446   return outputarray
447
448
449 def VerifyNode(what, cluster_name):
450   """Verify the status of the local node.
451
452   Based on the input L{what} parameter, various checks are done on the
453   local node.
454
455   If the I{filelist} key is present, this list of
456   files is checksummed and the file/checksum pairs are returned.
457
458   If the I{nodelist} key is present, we check that we have
459   connectivity via ssh with the target nodes (and check the hostname
460   report).
461
462   If the I{node-net-test} key is present, we check that we have
463   connectivity to the given nodes via both primary IP and, if
464   applicable, secondary IPs.
465
466   @type what: C{dict}
467   @param what: a dictionary of things to check:
468       - filelist: list of files for which to compute checksums
469       - nodelist: list of nodes we should check ssh communication with
470       - node-net-test: list of nodes we should check node daemon port
471         connectivity with
472       - hypervisor: list with hypervisors to run the verify for
473   @rtype: dict
474   @return: a dictionary with the same keys as the input dict, and
475       values representing the result of the checks
476
477   """
478   result = {}
479
480   if constants.NV_HYPERVISOR in what:
481     result[constants.NV_HYPERVISOR] = tmp = {}
482     for hv_name in what[constants.NV_HYPERVISOR]:
483       try:
484         val = hypervisor.GetHypervisor(hv_name).Verify()
485       except errors.HypervisorError, err:
486         val = "Error while checking hypervisor: %s" % str(err)
487       tmp[hv_name] = val
488
489   if constants.NV_FILELIST in what:
490     result[constants.NV_FILELIST] = utils.FingerprintFiles(
491       what[constants.NV_FILELIST])
492
493   if constants.NV_NODELIST in what:
494     result[constants.NV_NODELIST] = tmp = {}
495     random.shuffle(what[constants.NV_NODELIST])
496     for node in what[constants.NV_NODELIST]:
497       success, message = _GetSshRunner(cluster_name).VerifyNodeHostname(node)
498       if not success:
499         tmp[node] = message
500
501   if constants.NV_NODENETTEST in what:
502     result[constants.NV_NODENETTEST] = tmp = {}
503     my_name = utils.HostInfo().name
504     my_pip = my_sip = None
505     for name, pip, sip in what[constants.NV_NODENETTEST]:
506       if name == my_name:
507         my_pip = pip
508         my_sip = sip
509         break
510     if not my_pip:
511       tmp[my_name] = ("Can't find my own primary/secondary IP"
512                       " in the node list")
513     else:
514       port = utils.GetDaemonPort(constants.NODED)
515       for name, pip, sip in what[constants.NV_NODENETTEST]:
516         fail = []
517         if not utils.TcpPing(pip, port, source=my_pip):
518           fail.append("primary")
519         if sip != pip:
520           if not utils.TcpPing(sip, port, source=my_sip):
521             fail.append("secondary")
522         if fail:
523           tmp[name] = ("failure using the %s interface(s)" %
524                        " and ".join(fail))
525
526   if constants.NV_LVLIST in what:
527     try:
528       val = GetVolumeList(what[constants.NV_LVLIST])
529     except RPCFail, err:
530       val = str(err)
531     result[constants.NV_LVLIST] = val
532
533   if constants.NV_INSTANCELIST in what:
534     # GetInstanceList can fail
535     try:
536       val = GetInstanceList(what[constants.NV_INSTANCELIST])
537     except RPCFail, err:
538       val = str(err)
539     result[constants.NV_INSTANCELIST] = val
540
541   if constants.NV_VGLIST in what:
542     result[constants.NV_VGLIST] = utils.ListVolumeGroups()
543
544   if constants.NV_PVLIST in what:
545     result[constants.NV_PVLIST] = \
546       bdev.LogicalVolume.GetPVInfo(what[constants.NV_PVLIST],
547                                    filter_allocatable=False)
548
549   if constants.NV_VERSION in what:
550     result[constants.NV_VERSION] = (constants.PROTOCOL_VERSION,
551                                     constants.RELEASE_VERSION)
552
553   if constants.NV_HVINFO in what:
554     hyper = hypervisor.GetHypervisor(what[constants.NV_HVINFO])
555     result[constants.NV_HVINFO] = hyper.GetNodeInfo()
556
557   if constants.NV_DRBDLIST in what:
558     try:
559       used_minors = bdev.DRBD8.GetUsedDevs().keys()
560     except errors.BlockDeviceError, err:
561       logging.warning("Can't get used minors list", exc_info=True)
562       used_minors = str(err)
563     result[constants.NV_DRBDLIST] = used_minors
564
565   if constants.NV_NODESETUP in what:
566     result[constants.NV_NODESETUP] = tmpr = []
567     if not os.path.isdir("/sys/block") or not os.path.isdir("/sys/class/net"):
568       tmpr.append("The sysfs filesytem doesn't seem to be mounted"
569                   " under /sys, missing required directories /sys/block"
570                   " and /sys/class/net")
571     if (not os.path.isdir("/proc/sys") or
572         not os.path.isfile("/proc/sysrq-trigger")):
573       tmpr.append("The procfs filesystem doesn't seem to be mounted"
574                   " under /proc, missing required directory /proc/sys and"
575                   " the file /proc/sysrq-trigger")
576
577   if constants.NV_TIME in what:
578     result[constants.NV_TIME] = utils.SplitTime(time.time())
579
580   return result
581
582
583 def GetVolumeList(vg_name):
584   """Compute list of logical volumes and their size.
585
586   @type vg_name: str
587   @param vg_name: the volume group whose LVs we should list
588   @rtype: dict
589   @return:
590       dictionary of all partions (key) with value being a tuple of
591       their size (in MiB), inactive and online status::
592
593         {'test1': ('20.06', True, True)}
594
595       in case of errors, a string is returned with the error
596       details.
597
598   """
599   lvs = {}
600   sep = '|'
601   result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
602                          "--separator=%s" % sep,
603                          "-olv_name,lv_size,lv_attr", vg_name])
604   if result.failed:
605     _Fail("Failed to list logical volumes, lvs output: %s", result.output)
606
607   valid_line_re = re.compile("^ *([^|]+)\|([0-9.]+)\|([^|]{6})\|?$")
608   for line in result.stdout.splitlines():
609     line = line.strip()
610     match = valid_line_re.match(line)
611     if not match:
612       logging.error("Invalid line returned from lvs output: '%s'", line)
613       continue
614     name, size, attr = match.groups()
615     inactive = attr[4] == '-'
616     online = attr[5] == 'o'
617     virtual = attr[0] == 'v'
618     if virtual:
619       # we don't want to report such volumes as existing, since they
620       # don't really hold data
621       continue
622     lvs[name] = (size, inactive, online)
623
624   return lvs
625
626
627 def ListVolumeGroups():
628   """List the volume groups and their size.
629
630   @rtype: dict
631   @return: dictionary with keys volume name and values the
632       size of the volume
633
634   """
635   return utils.ListVolumeGroups()
636
637
638 def NodeVolumes():
639   """List all volumes on this node.
640
641   @rtype: list
642   @return:
643     A list of dictionaries, each having four keys:
644       - name: the logical volume name,
645       - size: the size of the logical volume
646       - dev: the physical device on which the LV lives
647       - vg: the volume group to which it belongs
648
649     In case of errors, we return an empty list and log the
650     error.
651
652     Note that since a logical volume can live on multiple physical
653     volumes, the resulting list might include a logical volume
654     multiple times.
655
656   """
657   result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
658                          "--separator=|",
659                          "--options=lv_name,lv_size,devices,vg_name"])
660   if result.failed:
661     _Fail("Failed to list logical volumes, lvs output: %s",
662           result.output)
663
664   def parse_dev(dev):
665     return dev.split('(')[0]
666
667   def handle_dev(dev):
668     return [parse_dev(x) for x in dev.split(",")]
669
670   def map_line(line):
671     line = [v.strip() for v in line]
672     return [{'name': line[0], 'size': line[1],
673              'dev': dev, 'vg': line[3]} for dev in handle_dev(line[2])]
674
675   all_devs = []
676   for line in result.stdout.splitlines():
677     if line.count('|') >= 3:
678       all_devs.extend(map_line(line.split('|')))
679     else:
680       logging.warning("Strange line in the output from lvs: '%s'", line)
681   return all_devs
682
683
684 def BridgesExist(bridges_list):
685   """Check if a list of bridges exist on the current node.
686
687   @rtype: boolean
688   @return: C{True} if all of them exist, C{False} otherwise
689
690   """
691   missing = []
692   for bridge in bridges_list:
693     if not utils.BridgeExists(bridge):
694       missing.append(bridge)
695
696   if missing:
697     _Fail("Missing bridges %s", utils.CommaJoin(missing))
698
699
700 def GetInstanceList(hypervisor_list):
701   """Provides a list of instances.
702
703   @type hypervisor_list: list
704   @param hypervisor_list: the list of hypervisors to query information
705
706   @rtype: list
707   @return: a list of all running instances on the current node
708     - instance1.example.com
709     - instance2.example.com
710
711   """
712   results = []
713   for hname in hypervisor_list:
714     try:
715       names = hypervisor.GetHypervisor(hname).ListInstances()
716       results.extend(names)
717     except errors.HypervisorError, err:
718       _Fail("Error enumerating instances (hypervisor %s): %s",
719             hname, err, exc=True)
720
721   return results
722
723
724 def GetInstanceInfo(instance, hname):
725   """Gives back the information about an instance as a dictionary.
726
727   @type instance: string
728   @param instance: the instance name
729   @type hname: string
730   @param hname: the hypervisor type of the instance
731
732   @rtype: dict
733   @return: dictionary with the following keys:
734       - memory: memory size of instance (int)
735       - state: xen state of instance (string)
736       - time: cpu time of instance (float)
737
738   """
739   output = {}
740
741   iinfo = hypervisor.GetHypervisor(hname).GetInstanceInfo(instance)
742   if iinfo is not None:
743     output['memory'] = iinfo[2]
744     output['state'] = iinfo[4]
745     output['time'] = iinfo[5]
746
747   return output
748
749
750 def GetInstanceMigratable(instance):
751   """Gives whether an instance can be migrated.
752
753   @type instance: L{objects.Instance}
754   @param instance: object representing the instance to be checked.
755
756   @rtype: tuple
757   @return: tuple of (result, description) where:
758       - result: whether the instance can be migrated or not
759       - description: a description of the issue, if relevant
760
761   """
762   hyper = hypervisor.GetHypervisor(instance.hypervisor)
763   iname = instance.name
764   if iname not in hyper.ListInstances():
765     _Fail("Instance %s is not running", iname)
766
767   for idx in range(len(instance.disks)):
768     link_name = _GetBlockDevSymlinkPath(iname, idx)
769     if not os.path.islink(link_name):
770       _Fail("Instance %s was not restarted since ganeti 1.2.5", iname)
771
772
773 def GetAllInstancesInfo(hypervisor_list):
774   """Gather data about all instances.
775
776   This is the equivalent of L{GetInstanceInfo}, except that it
777   computes data for all instances at once, thus being faster if one
778   needs data about more than one instance.
779
780   @type hypervisor_list: list
781   @param hypervisor_list: list of hypervisors to query for instance data
782
783   @rtype: dict
784   @return: dictionary of instance: data, with data having the following keys:
785       - memory: memory size of instance (int)
786       - state: xen state of instance (string)
787       - time: cpu time of instance (float)
788       - vcpus: the number of vcpus
789
790   """
791   output = {}
792
793   for hname in hypervisor_list:
794     iinfo = hypervisor.GetHypervisor(hname).GetAllInstancesInfo()
795     if iinfo:
796       for name, _, memory, vcpus, state, times in iinfo:
797         value = {
798           'memory': memory,
799           'vcpus': vcpus,
800           'state': state,
801           'time': times,
802           }
803         if name in output:
804           # we only check static parameters, like memory and vcpus,
805           # and not state and time which can change between the
806           # invocations of the different hypervisors
807           for key in 'memory', 'vcpus':
808             if value[key] != output[name][key]:
809               _Fail("Instance %s is running twice"
810                     " with different parameters", name)
811         output[name] = value
812
813   return output
814
815
816 def _InstanceLogName(kind, os_name, instance):
817   """Compute the OS log filename for a given instance and operation.
818
819   The instance name and os name are passed in as strings since not all
820   operations have these as part of an instance object.
821
822   @type kind: string
823   @param kind: the operation type (e.g. add, import, etc.)
824   @type os_name: string
825   @param os_name: the os name
826   @type instance: string
827   @param instance: the name of the instance being imported/added/etc.
828
829   """
830   base = ("%s-%s-%s-%s.log" %
831           (kind, os_name, instance, utils.TimestampForFilename()))
832   return utils.PathJoin(constants.LOG_OS_DIR, base)
833
834
835 def InstanceOsAdd(instance, reinstall, debug):
836   """Add an OS to an instance.
837
838   @type instance: L{objects.Instance}
839   @param instance: Instance whose OS is to be installed
840   @type reinstall: boolean
841   @param reinstall: whether this is an instance reinstall
842   @type debug: integer
843   @param debug: debug level, passed to the OS scripts
844   @rtype: None
845
846   """
847   inst_os = OSFromDisk(instance.os)
848
849   create_env = OSEnvironment(instance, inst_os, debug)
850   if reinstall:
851     create_env['INSTANCE_REINSTALL'] = "1"
852
853   logfile = _InstanceLogName("add", instance.os, instance.name)
854
855   result = utils.RunCmd([inst_os.create_script], env=create_env,
856                         cwd=inst_os.path, output=logfile,)
857   if result.failed:
858     logging.error("os create command '%s' returned error: %s, logfile: %s,"
859                   " output: %s", result.cmd, result.fail_reason, logfile,
860                   result.output)
861     lines = [utils.SafeEncode(val)
862              for val in utils.TailFile(logfile, lines=20)]
863     _Fail("OS create script failed (%s), last lines in the"
864           " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
865
866
867 def RunRenameInstance(instance, old_name, debug):
868   """Run the OS rename script for an instance.
869
870   @type instance: L{objects.Instance}
871   @param instance: Instance whose OS is to be installed
872   @type old_name: string
873   @param old_name: previous instance name
874   @type debug: integer
875   @param debug: debug level, passed to the OS scripts
876   @rtype: boolean
877   @return: the success of the operation
878
879   """
880   inst_os = OSFromDisk(instance.os)
881
882   rename_env = OSEnvironment(instance, inst_os, debug)
883   rename_env['OLD_INSTANCE_NAME'] = old_name
884
885   logfile = _InstanceLogName("rename", instance.os,
886                              "%s-%s" % (old_name, instance.name))
887
888   result = utils.RunCmd([inst_os.rename_script], env=rename_env,
889                         cwd=inst_os.path, output=logfile)
890
891   if result.failed:
892     logging.error("os create command '%s' returned error: %s output: %s",
893                   result.cmd, result.fail_reason, result.output)
894     lines = [utils.SafeEncode(val)
895              for val in utils.TailFile(logfile, lines=20)]
896     _Fail("OS rename script failed (%s), last lines in the"
897           " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
898
899
900 def _GetVGInfo(vg_name):
901   """Get information about the volume group.
902
903   @type vg_name: str
904   @param vg_name: the volume group which we query
905   @rtype: dict
906   @return:
907     A dictionary with the following keys:
908       - C{vg_size} is the total size of the volume group in MiB
909       - C{vg_free} is the free size of the volume group in MiB
910       - C{pv_count} are the number of physical disks in that VG
911
912     If an error occurs during gathering of data, we return the same dict
913     with keys all set to None.
914
915   """
916   retdic = dict.fromkeys(["vg_size", "vg_free", "pv_count"])
917
918   retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
919                          "--nosuffix", "--units=m", "--separator=:", vg_name])
920
921   if retval.failed:
922     logging.error("volume group %s not present", vg_name)
923     return retdic
924   valarr = retval.stdout.strip().rstrip(':').split(':')
925   if len(valarr) == 3:
926     try:
927       retdic = {
928         "vg_size": int(round(float(valarr[0]), 0)),
929         "vg_free": int(round(float(valarr[1]), 0)),
930         "pv_count": int(valarr[2]),
931         }
932     except (TypeError, ValueError), err:
933       logging.exception("Fail to parse vgs output: %s", err)
934   else:
935     logging.error("vgs output has the wrong number of fields (expected"
936                   " three): %s", str(valarr))
937   return retdic
938
939
940 def _GetBlockDevSymlinkPath(instance_name, idx):
941   return utils.PathJoin(constants.DISK_LINKS_DIR,
942                         "%s:%d" % (instance_name, idx))
943
944
945 def _SymlinkBlockDev(instance_name, device_path, idx):
946   """Set up symlinks to a instance's block device.
947
948   This is an auxiliary function run when an instance is start (on the primary
949   node) or when an instance is migrated (on the target node).
950
951
952   @param instance_name: the name of the target instance
953   @param device_path: path of the physical block device, on the node
954   @param idx: the disk index
955   @return: absolute path to the disk's symlink
956
957   """
958   link_name = _GetBlockDevSymlinkPath(instance_name, idx)
959   try:
960     os.symlink(device_path, link_name)
961   except OSError, err:
962     if err.errno == errno.EEXIST:
963       if (not os.path.islink(link_name) or
964           os.readlink(link_name) != device_path):
965         os.remove(link_name)
966         os.symlink(device_path, link_name)
967     else:
968       raise
969
970   return link_name
971
972
973 def _RemoveBlockDevLinks(instance_name, disks):
974   """Remove the block device symlinks belonging to the given instance.
975
976   """
977   for idx, _ in enumerate(disks):
978     link_name = _GetBlockDevSymlinkPath(instance_name, idx)
979     if os.path.islink(link_name):
980       try:
981         os.remove(link_name)
982       except OSError:
983         logging.exception("Can't remove symlink '%s'", link_name)
984
985
986 def _GatherAndLinkBlockDevs(instance):
987   """Set up an instance's block device(s).
988
989   This is run on the primary node at instance startup. The block
990   devices must be already assembled.
991
992   @type instance: L{objects.Instance}
993   @param instance: the instance whose disks we shoul assemble
994   @rtype: list
995   @return: list of (disk_object, device_path)
996
997   """
998   block_devices = []
999   for idx, disk in enumerate(instance.disks):
1000     device = _RecursiveFindBD(disk)
1001     if device is None:
1002       raise errors.BlockDeviceError("Block device '%s' is not set up." %
1003                                     str(disk))
1004     device.Open()
1005     try:
1006       link_name = _SymlinkBlockDev(instance.name, device.dev_path, idx)
1007     except OSError, e:
1008       raise errors.BlockDeviceError("Cannot create block device symlink: %s" %
1009                                     e.strerror)
1010
1011     block_devices.append((disk, link_name))
1012
1013   return block_devices
1014
1015
1016 def StartInstance(instance):
1017   """Start an instance.
1018
1019   @type instance: L{objects.Instance}
1020   @param instance: the instance object
1021   @rtype: None
1022
1023   """
1024   running_instances = GetInstanceList([instance.hypervisor])
1025
1026   if instance.name in running_instances:
1027     logging.info("Instance %s already running, not starting", instance.name)
1028     return
1029
1030   try:
1031     block_devices = _GatherAndLinkBlockDevs(instance)
1032     hyper = hypervisor.GetHypervisor(instance.hypervisor)
1033     hyper.StartInstance(instance, block_devices)
1034   except errors.BlockDeviceError, err:
1035     _Fail("Block device error: %s", err, exc=True)
1036   except errors.HypervisorError, err:
1037     _RemoveBlockDevLinks(instance.name, instance.disks)
1038     _Fail("Hypervisor error: %s", err, exc=True)
1039
1040
1041 def InstanceShutdown(instance, timeout):
1042   """Shut an instance down.
1043
1044   @note: this functions uses polling with a hardcoded timeout.
1045
1046   @type instance: L{objects.Instance}
1047   @param instance: the instance object
1048   @type timeout: integer
1049   @param timeout: maximum timeout for soft shutdown
1050   @rtype: None
1051
1052   """
1053   hv_name = instance.hypervisor
1054   hyper = hypervisor.GetHypervisor(hv_name)
1055   iname = instance.name
1056
1057   if instance.name not in hyper.ListInstances():
1058     logging.info("Instance %s not running, doing nothing", iname)
1059     return
1060
1061   class _TryShutdown:
1062     def __init__(self):
1063       self.tried_once = False
1064
1065     def __call__(self):
1066       if iname not in hyper.ListInstances():
1067         return
1068
1069       try:
1070         hyper.StopInstance(instance, retry=self.tried_once)
1071       except errors.HypervisorError, err:
1072         if iname not in hyper.ListInstances():
1073           # if the instance is no longer existing, consider this a
1074           # success and go to cleanup
1075           return
1076
1077         _Fail("Failed to stop instance %s: %s", iname, err)
1078
1079       self.tried_once = True
1080
1081       raise utils.RetryAgain()
1082
1083   try:
1084     utils.Retry(_TryShutdown(), 5, timeout)
1085   except utils.RetryTimeout:
1086     # the shutdown did not succeed
1087     logging.error("Shutdown of '%s' unsuccessful, forcing", iname)
1088
1089     try:
1090       hyper.StopInstance(instance, force=True)
1091     except errors.HypervisorError, err:
1092       if iname in hyper.ListInstances():
1093         # only raise an error if the instance still exists, otherwise
1094         # the error could simply be "instance ... unknown"!
1095         _Fail("Failed to force stop instance %s: %s", iname, err)
1096
1097     time.sleep(1)
1098
1099     if iname in hyper.ListInstances():
1100       _Fail("Could not shutdown instance %s even by destroy", iname)
1101
1102   _RemoveBlockDevLinks(iname, instance.disks)
1103
1104
1105 def InstanceReboot(instance, reboot_type, shutdown_timeout):
1106   """Reboot an instance.
1107
1108   @type instance: L{objects.Instance}
1109   @param instance: the instance object to reboot
1110   @type reboot_type: str
1111   @param reboot_type: the type of reboot, one the following
1112     constants:
1113       - L{constants.INSTANCE_REBOOT_SOFT}: only reboot the
1114         instance OS, do not recreate the VM
1115       - L{constants.INSTANCE_REBOOT_HARD}: tear down and
1116         restart the VM (at the hypervisor level)
1117       - the other reboot type (L{constants.INSTANCE_REBOOT_FULL}) is
1118         not accepted here, since that mode is handled differently, in
1119         cmdlib, and translates into full stop and start of the
1120         instance (instead of a call_instance_reboot RPC)
1121   @type shutdown_timeout: integer
1122   @param shutdown_timeout: maximum timeout for soft shutdown
1123   @rtype: None
1124
1125   """
1126   running_instances = GetInstanceList([instance.hypervisor])
1127
1128   if instance.name not in running_instances:
1129     _Fail("Cannot reboot instance %s that is not running", instance.name)
1130
1131   hyper = hypervisor.GetHypervisor(instance.hypervisor)
1132   if reboot_type == constants.INSTANCE_REBOOT_SOFT:
1133     try:
1134       hyper.RebootInstance(instance)
1135     except errors.HypervisorError, err:
1136       _Fail("Failed to soft reboot instance %s: %s", instance.name, err)
1137   elif reboot_type == constants.INSTANCE_REBOOT_HARD:
1138     try:
1139       InstanceShutdown(instance, shutdown_timeout)
1140       return StartInstance(instance)
1141     except errors.HypervisorError, err:
1142       _Fail("Failed to hard reboot instance %s: %s", instance.name, err)
1143   else:
1144     _Fail("Invalid reboot_type received: %s", reboot_type)
1145
1146
1147 def MigrationInfo(instance):
1148   """Gather information about an instance to be migrated.
1149
1150   @type instance: L{objects.Instance}
1151   @param instance: the instance definition
1152
1153   """
1154   hyper = hypervisor.GetHypervisor(instance.hypervisor)
1155   try:
1156     info = hyper.MigrationInfo(instance)
1157   except errors.HypervisorError, err:
1158     _Fail("Failed to fetch migration information: %s", err, exc=True)
1159   return info
1160
1161
1162 def AcceptInstance(instance, info, target):
1163   """Prepare the node to accept an instance.
1164
1165   @type instance: L{objects.Instance}
1166   @param instance: the instance definition
1167   @type info: string/data (opaque)
1168   @param info: migration information, from the source node
1169   @type target: string
1170   @param target: target host (usually ip), on this node
1171
1172   """
1173   hyper = hypervisor.GetHypervisor(instance.hypervisor)
1174   try:
1175     hyper.AcceptInstance(instance, info, target)
1176   except errors.HypervisorError, err:
1177     _Fail("Failed to accept instance: %s", err, exc=True)
1178
1179
1180 def FinalizeMigration(instance, info, success):
1181   """Finalize any preparation to accept an instance.
1182
1183   @type instance: L{objects.Instance}
1184   @param instance: the instance definition
1185   @type info: string/data (opaque)
1186   @param info: migration information, from the source node
1187   @type success: boolean
1188   @param success: whether the migration was a success or a failure
1189
1190   """
1191   hyper = hypervisor.GetHypervisor(instance.hypervisor)
1192   try:
1193     hyper.FinalizeMigration(instance, info, success)
1194   except errors.HypervisorError, err:
1195     _Fail("Failed to finalize migration: %s", err, exc=True)
1196
1197
1198 def MigrateInstance(instance, target, live):
1199   """Migrates an instance to another node.
1200
1201   @type instance: L{objects.Instance}
1202   @param instance: the instance definition
1203   @type target: string
1204   @param target: the target node name
1205   @type live: boolean
1206   @param live: whether the migration should be done live or not (the
1207       interpretation of this parameter is left to the hypervisor)
1208   @rtype: tuple
1209   @return: a tuple of (success, msg) where:
1210       - succes is a boolean denoting the success/failure of the operation
1211       - msg is a string with details in case of failure
1212
1213   """
1214   hyper = hypervisor.GetHypervisor(instance.hypervisor)
1215
1216   try:
1217     hyper.MigrateInstance(instance, target, live)
1218   except errors.HypervisorError, err:
1219     _Fail("Failed to migrate instance: %s", err, exc=True)
1220
1221
1222 def BlockdevCreate(disk, size, owner, on_primary, info):
1223   """Creates a block device for an instance.
1224
1225   @type disk: L{objects.Disk}
1226   @param disk: the object describing the disk we should create
1227   @type size: int
1228   @param size: the size of the physical underlying device, in MiB
1229   @type owner: str
1230   @param owner: the name of the instance for which disk is created,
1231       used for device cache data
1232   @type on_primary: boolean
1233   @param on_primary:  indicates if it is the primary node or not
1234   @type info: string
1235   @param info: string that will be sent to the physical device
1236       creation, used for example to set (LVM) tags on LVs
1237
1238   @return: the new unique_id of the device (this can sometime be
1239       computed only after creation), or None. On secondary nodes,
1240       it's not required to return anything.
1241
1242   """
1243   # TODO: remove the obsolete 'size' argument
1244   # pylint: disable-msg=W0613
1245   clist = []
1246   if disk.children:
1247     for child in disk.children:
1248       try:
1249         crdev = _RecursiveAssembleBD(child, owner, on_primary)
1250       except errors.BlockDeviceError, err:
1251         _Fail("Can't assemble device %s: %s", child, err)
1252       if on_primary or disk.AssembleOnSecondary():
1253         # we need the children open in case the device itself has to
1254         # be assembled
1255         try:
1256           # pylint: disable-msg=E1103
1257           crdev.Open()
1258         except errors.BlockDeviceError, err:
1259           _Fail("Can't make child '%s' read-write: %s", child, err)
1260       clist.append(crdev)
1261
1262   try:
1263     device = bdev.Create(disk.dev_type, disk.physical_id, clist, disk.size)
1264   except errors.BlockDeviceError, err:
1265     _Fail("Can't create block device: %s", err)
1266
1267   if on_primary or disk.AssembleOnSecondary():
1268     try:
1269       device.Assemble()
1270     except errors.BlockDeviceError, err:
1271       _Fail("Can't assemble device after creation, unusual event: %s", err)
1272     device.SetSyncSpeed(constants.SYNC_SPEED)
1273     if on_primary or disk.OpenOnSecondary():
1274       try:
1275         device.Open(force=True)
1276       except errors.BlockDeviceError, err:
1277         _Fail("Can't make device r/w after creation, unusual event: %s", err)
1278     DevCacheManager.UpdateCache(device.dev_path, owner,
1279                                 on_primary, disk.iv_name)
1280
1281   device.SetInfo(info)
1282
1283   return device.unique_id
1284
1285
1286 def BlockdevRemove(disk):
1287   """Remove a block device.
1288
1289   @note: This is intended to be called recursively.
1290
1291   @type disk: L{objects.Disk}
1292   @param disk: the disk object we should remove
1293   @rtype: boolean
1294   @return: the success of the operation
1295
1296   """
1297   msgs = []
1298   try:
1299     rdev = _RecursiveFindBD(disk)
1300   except errors.BlockDeviceError, err:
1301     # probably can't attach
1302     logging.info("Can't attach to device %s in remove", disk)
1303     rdev = None
1304   if rdev is not None:
1305     r_path = rdev.dev_path
1306     try:
1307       rdev.Remove()
1308     except errors.BlockDeviceError, err:
1309       msgs.append(str(err))
1310     if not msgs:
1311       DevCacheManager.RemoveCache(r_path)
1312
1313   if disk.children:
1314     for child in disk.children:
1315       try:
1316         BlockdevRemove(child)
1317       except RPCFail, err:
1318         msgs.append(str(err))
1319
1320   if msgs:
1321     _Fail("; ".join(msgs))
1322
1323
1324 def _RecursiveAssembleBD(disk, owner, as_primary):
1325   """Activate a block device for an instance.
1326
1327   This is run on the primary and secondary nodes for an instance.
1328
1329   @note: this function is called recursively.
1330
1331   @type disk: L{objects.Disk}
1332   @param disk: the disk we try to assemble
1333   @type owner: str
1334   @param owner: the name of the instance which owns the disk
1335   @type as_primary: boolean
1336   @param as_primary: if we should make the block device
1337       read/write
1338
1339   @return: the assembled device or None (in case no device
1340       was assembled)
1341   @raise errors.BlockDeviceError: in case there is an error
1342       during the activation of the children or the device
1343       itself
1344
1345   """
1346   children = []
1347   if disk.children:
1348     mcn = disk.ChildrenNeeded()
1349     if mcn == -1:
1350       mcn = 0 # max number of Nones allowed
1351     else:
1352       mcn = len(disk.children) - mcn # max number of Nones
1353     for chld_disk in disk.children:
1354       try:
1355         cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
1356       except errors.BlockDeviceError, err:
1357         if children.count(None) >= mcn:
1358           raise
1359         cdev = None
1360         logging.error("Error in child activation (but continuing): %s",
1361                       str(err))
1362       children.append(cdev)
1363
1364   if as_primary or disk.AssembleOnSecondary():
1365     r_dev = bdev.Assemble(disk.dev_type, disk.physical_id, children, disk.size)
1366     r_dev.SetSyncSpeed(constants.SYNC_SPEED)
1367     result = r_dev
1368     if as_primary or disk.OpenOnSecondary():
1369       r_dev.Open()
1370     DevCacheManager.UpdateCache(r_dev.dev_path, owner,
1371                                 as_primary, disk.iv_name)
1372
1373   else:
1374     result = True
1375   return result
1376
1377
1378 def BlockdevAssemble(disk, owner, as_primary):
1379   """Activate a block device for an instance.
1380
1381   This is a wrapper over _RecursiveAssembleBD.
1382
1383   @rtype: str or boolean
1384   @return: a C{/dev/...} path for primary nodes, and
1385       C{True} for secondary nodes
1386
1387   """
1388   try:
1389     result = _RecursiveAssembleBD(disk, owner, as_primary)
1390     if isinstance(result, bdev.BlockDev):
1391       # pylint: disable-msg=E1103
1392       result = result.dev_path
1393   except errors.BlockDeviceError, err:
1394     _Fail("Error while assembling disk: %s", err, exc=True)
1395
1396   return result
1397
1398
1399 def BlockdevShutdown(disk):
1400   """Shut down a block device.
1401
1402   First, if the device is assembled (Attach() is successful), then
1403   the device is shutdown. Then the children of the device are
1404   shutdown.
1405
1406   This function is called recursively. Note that we don't cache the
1407   children or such, as oppossed to assemble, shutdown of different
1408   devices doesn't require that the upper device was active.
1409
1410   @type disk: L{objects.Disk}
1411   @param disk: the description of the disk we should
1412       shutdown
1413   @rtype: None
1414
1415   """
1416   msgs = []
1417   r_dev = _RecursiveFindBD(disk)
1418   if r_dev is not None:
1419     r_path = r_dev.dev_path
1420     try:
1421       r_dev.Shutdown()
1422       DevCacheManager.RemoveCache(r_path)
1423     except errors.BlockDeviceError, err:
1424       msgs.append(str(err))
1425
1426   if disk.children:
1427     for child in disk.children:
1428       try:
1429         BlockdevShutdown(child)
1430       except RPCFail, err:
1431         msgs.append(str(err))
1432
1433   if msgs:
1434     _Fail("; ".join(msgs))
1435
1436
1437 def BlockdevAddchildren(parent_cdev, new_cdevs):
1438   """Extend a mirrored block device.
1439
1440   @type parent_cdev: L{objects.Disk}
1441   @param parent_cdev: the disk to which we should add children
1442   @type new_cdevs: list of L{objects.Disk}
1443   @param new_cdevs: the list of children which we should add
1444   @rtype: None
1445
1446   """
1447   parent_bdev = _RecursiveFindBD(parent_cdev)
1448   if parent_bdev is None:
1449     _Fail("Can't find parent device '%s' in add children", parent_cdev)
1450   new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
1451   if new_bdevs.count(None) > 0:
1452     _Fail("Can't find new device(s) to add: %s:%s", new_bdevs, new_cdevs)
1453   parent_bdev.AddChildren(new_bdevs)
1454
1455
1456 def BlockdevRemovechildren(parent_cdev, new_cdevs):
1457   """Shrink a mirrored block device.
1458
1459   @type parent_cdev: L{objects.Disk}
1460   @param parent_cdev: the disk from which we should remove children
1461   @type new_cdevs: list of L{objects.Disk}
1462   @param new_cdevs: the list of children which we should remove
1463   @rtype: None
1464
1465   """
1466   parent_bdev = _RecursiveFindBD(parent_cdev)
1467   if parent_bdev is None:
1468     _Fail("Can't find parent device '%s' in remove children", parent_cdev)
1469   devs = []
1470   for disk in new_cdevs:
1471     rpath = disk.StaticDevPath()
1472     if rpath is None:
1473       bd = _RecursiveFindBD(disk)
1474       if bd is None:
1475         _Fail("Can't find device %s while removing children", disk)
1476       else:
1477         devs.append(bd.dev_path)
1478     else:
1479       if not utils.IsNormAbsPath(rpath):
1480         _Fail("Strange path returned from StaticDevPath: '%s'", rpath)
1481       devs.append(rpath)
1482   parent_bdev.RemoveChildren(devs)
1483
1484
1485 def BlockdevGetmirrorstatus(disks):
1486   """Get the mirroring status of a list of devices.
1487
1488   @type disks: list of L{objects.Disk}
1489   @param disks: the list of disks which we should query
1490   @rtype: disk
1491   @return:
1492       a list of (mirror_done, estimated_time) tuples, which
1493       are the result of L{bdev.BlockDev.CombinedSyncStatus}
1494   @raise errors.BlockDeviceError: if any of the disks cannot be
1495       found
1496
1497   """
1498   stats = []
1499   for dsk in disks:
1500     rbd = _RecursiveFindBD(dsk)
1501     if rbd is None:
1502       _Fail("Can't find device %s", dsk)
1503
1504     stats.append(rbd.CombinedSyncStatus())
1505
1506   return stats
1507
1508
1509 def _RecursiveFindBD(disk):
1510   """Check if a device is activated.
1511
1512   If so, return information about the real device.
1513
1514   @type disk: L{objects.Disk}
1515   @param disk: the disk object we need to find
1516
1517   @return: None if the device can't be found,
1518       otherwise the device instance
1519
1520   """
1521   children = []
1522   if disk.children:
1523     for chdisk in disk.children:
1524       children.append(_RecursiveFindBD(chdisk))
1525
1526   return bdev.FindDevice(disk.dev_type, disk.physical_id, children, disk.size)
1527
1528
1529 def _OpenRealBD(disk):
1530   """Opens the underlying block device of a disk.
1531
1532   @type disk: L{objects.Disk}
1533   @param disk: the disk object we want to open
1534
1535   """
1536   real_disk = _RecursiveFindBD(disk)
1537   if real_disk is None:
1538     _Fail("Block device '%s' is not set up", disk)
1539
1540   real_disk.Open()
1541
1542   return real_disk
1543
1544
1545 def BlockdevFind(disk):
1546   """Check if a device is activated.
1547
1548   If it is, return information about the real device.
1549
1550   @type disk: L{objects.Disk}
1551   @param disk: the disk to find
1552   @rtype: None or objects.BlockDevStatus
1553   @return: None if the disk cannot be found, otherwise a the current
1554            information
1555
1556   """
1557   try:
1558     rbd = _RecursiveFindBD(disk)
1559   except errors.BlockDeviceError, err:
1560     _Fail("Failed to find device: %s", err, exc=True)
1561
1562   if rbd is None:
1563     return None
1564
1565   return rbd.GetSyncStatus()
1566
1567
1568 def BlockdevGetsize(disks):
1569   """Computes the size of the given disks.
1570
1571   If a disk is not found, returns None instead.
1572
1573   @type disks: list of L{objects.Disk}
1574   @param disks: the list of disk to compute the size for
1575   @rtype: list
1576   @return: list with elements None if the disk cannot be found,
1577       otherwise the size
1578
1579   """
1580   result = []
1581   for cf in disks:
1582     try:
1583       rbd = _RecursiveFindBD(cf)
1584     except errors.BlockDeviceError:
1585       result.append(None)
1586       continue
1587     if rbd is None:
1588       result.append(None)
1589     else:
1590       result.append(rbd.GetActualSize())
1591   return result
1592
1593
1594 def BlockdevExport(disk, dest_node, dest_path, cluster_name):
1595   """Export a block device to a remote node.
1596
1597   @type disk: L{objects.Disk}
1598   @param disk: the description of the disk to export
1599   @type dest_node: str
1600   @param dest_node: the destination node to export to
1601   @type dest_path: str
1602   @param dest_path: the destination path on the target node
1603   @type cluster_name: str
1604   @param cluster_name: the cluster name, needed for SSH hostalias
1605   @rtype: None
1606
1607   """
1608   real_disk = _OpenRealBD(disk)
1609
1610   # the block size on the read dd is 1MiB to match our units
1611   expcmd = utils.BuildShellCmd("set -e; set -o pipefail; "
1612                                "dd if=%s bs=1048576 count=%s",
1613                                real_disk.dev_path, str(disk.size))
1614
1615   # we set here a smaller block size as, due to ssh buffering, more
1616   # than 64-128k will mostly ignored; we use nocreat to fail if the
1617   # device is not already there or we pass a wrong path; we use
1618   # notrunc to no attempt truncate on an LV device; we use oflag=dsync
1619   # to not buffer too much memory; this means that at best, we flush
1620   # every 64k, which will not be very fast
1621   destcmd = utils.BuildShellCmd("dd of=%s conv=nocreat,notrunc bs=65536"
1622                                 " oflag=dsync", dest_path)
1623
1624   remotecmd = _GetSshRunner(cluster_name).BuildCmd(dest_node,
1625                                                    constants.GANETI_RUNAS,
1626                                                    destcmd)
1627
1628   # all commands have been checked, so we're safe to combine them
1629   command = '|'.join([expcmd, utils.ShellQuoteArgs(remotecmd)])
1630
1631   result = utils.RunCmd(["bash", "-c", command])
1632
1633   if result.failed:
1634     _Fail("Disk copy command '%s' returned error: %s"
1635           " output: %s", command, result.fail_reason, result.output)
1636
1637
1638 def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
1639   """Write a file to the filesystem.
1640
1641   This allows the master to overwrite(!) a file. It will only perform
1642   the operation if the file belongs to a list of configuration files.
1643
1644   @type file_name: str
1645   @param file_name: the target file name
1646   @type data: str
1647   @param data: the new contents of the file
1648   @type mode: int
1649   @param mode: the mode to give the file (can be None)
1650   @type uid: int
1651   @param uid: the owner of the file (can be -1 for default)
1652   @type gid: int
1653   @param gid: the group of the file (can be -1 for default)
1654   @type atime: float
1655   @param atime: the atime to set on the file (can be None)
1656   @type mtime: float
1657   @param mtime: the mtime to set on the file (can be None)
1658   @rtype: None
1659
1660   """
1661   if not os.path.isabs(file_name):
1662     _Fail("Filename passed to UploadFile is not absolute: '%s'", file_name)
1663
1664   if file_name not in _ALLOWED_UPLOAD_FILES:
1665     _Fail("Filename passed to UploadFile not in allowed upload targets: '%s'",
1666           file_name)
1667
1668   raw_data = _Decompress(data)
1669
1670   utils.WriteFile(file_name, data=raw_data, mode=mode, uid=uid, gid=gid,
1671                   atime=atime, mtime=mtime)
1672
1673
1674 def WriteSsconfFiles(values):
1675   """Update all ssconf files.
1676
1677   Wrapper around the SimpleStore.WriteFiles.
1678
1679   """
1680   ssconf.SimpleStore().WriteFiles(values)
1681
1682
1683 def _ErrnoOrStr(err):
1684   """Format an EnvironmentError exception.
1685
1686   If the L{err} argument has an errno attribute, it will be looked up
1687   and converted into a textual C{E...} description. Otherwise the
1688   string representation of the error will be returned.
1689
1690   @type err: L{EnvironmentError}
1691   @param err: the exception to format
1692
1693   """
1694   if hasattr(err, 'errno'):
1695     detail = errno.errorcode[err.errno]
1696   else:
1697     detail = str(err)
1698   return detail
1699
1700
1701 def _OSOndiskAPIVersion(os_dir):
1702   """Compute and return the API version of a given OS.
1703
1704   This function will try to read the API version of the OS residing in
1705   the 'os_dir' directory.
1706
1707   @type os_dir: str
1708   @param os_dir: the directory in which we should look for the OS
1709   @rtype: tuple
1710   @return: tuple (status, data) with status denoting the validity and
1711       data holding either the vaid versions or an error message
1712
1713   """
1714   api_file = utils.PathJoin(os_dir, constants.OS_API_FILE)
1715
1716   try:
1717     st = os.stat(api_file)
1718   except EnvironmentError, err:
1719     return False, ("Required file '%s' not found under path %s: %s" %
1720                    (constants.OS_API_FILE, os_dir, _ErrnoOrStr(err)))
1721
1722   if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1723     return False, ("File '%s' in %s is not a regular file" %
1724                    (constants.OS_API_FILE, os_dir))
1725
1726   try:
1727     api_versions = utils.ReadFile(api_file).splitlines()
1728   except EnvironmentError, err:
1729     return False, ("Error while reading the API version file at %s: %s" %
1730                    (api_file, _ErrnoOrStr(err)))
1731
1732   try:
1733     api_versions = [int(version.strip()) for version in api_versions]
1734   except (TypeError, ValueError), err:
1735     return False, ("API version(s) can't be converted to integer: %s" %
1736                    str(err))
1737
1738   return True, api_versions
1739
1740
1741 def DiagnoseOS(top_dirs=None):
1742   """Compute the validity for all OSes.
1743
1744   @type top_dirs: list
1745   @param top_dirs: the list of directories in which to
1746       search (if not given defaults to
1747       L{constants.OS_SEARCH_PATH})
1748   @rtype: list of L{objects.OS}
1749   @return: a list of tuples (name, path, status, diagnose, variants)
1750       for all (potential) OSes under all search paths, where:
1751           - name is the (potential) OS name
1752           - path is the full path to the OS
1753           - status True/False is the validity of the OS
1754           - diagnose is the error message for an invalid OS, otherwise empty
1755           - variants is a list of supported OS variants, if any
1756
1757   """
1758   if top_dirs is None:
1759     top_dirs = constants.OS_SEARCH_PATH
1760
1761   result = []
1762   for dir_name in top_dirs:
1763     if os.path.isdir(dir_name):
1764       try:
1765         f_names = utils.ListVisibleFiles(dir_name)
1766       except EnvironmentError, err:
1767         logging.exception("Can't list the OS directory %s: %s", dir_name, err)
1768         break
1769       for name in f_names:
1770         os_path = utils.PathJoin(dir_name, name)
1771         status, os_inst = _TryOSFromDisk(name, base_dir=dir_name)
1772         if status:
1773           diagnose = ""
1774           variants = os_inst.supported_variants
1775         else:
1776           diagnose = os_inst
1777           variants = []
1778         result.append((name, os_path, status, diagnose, variants))
1779
1780   return result
1781
1782
1783 def _TryOSFromDisk(name, base_dir=None):
1784   """Create an OS instance from disk.
1785
1786   This function will return an OS instance if the given name is a
1787   valid OS name.
1788
1789   @type base_dir: string
1790   @keyword base_dir: Base directory containing OS installations.
1791                      Defaults to a search in all the OS_SEARCH_PATH dirs.
1792   @rtype: tuple
1793   @return: success and either the OS instance if we find a valid one,
1794       or error message
1795
1796   """
1797   if base_dir is None:
1798     os_dir = utils.FindFile(name, constants.OS_SEARCH_PATH, os.path.isdir)
1799   else:
1800     os_dir = utils.FindFile(name, [base_dir], os.path.isdir)
1801
1802   if os_dir is None:
1803     return False, "Directory for OS %s not found in search path" % name
1804
1805   status, api_versions = _OSOndiskAPIVersion(os_dir)
1806   if not status:
1807     # push the error up
1808     return status, api_versions
1809
1810   if not constants.OS_API_VERSIONS.intersection(api_versions):
1811     return False, ("API version mismatch for path '%s': found %s, want %s." %
1812                    (os_dir, api_versions, constants.OS_API_VERSIONS))
1813
1814   # OS Files dictionary, we will populate it with the absolute path names
1815   os_files = dict.fromkeys(constants.OS_SCRIPTS)
1816
1817   if max(api_versions) >= constants.OS_API_V15:
1818     os_files[constants.OS_VARIANTS_FILE] = ''
1819
1820   for filename in os_files:
1821     os_files[filename] = utils.PathJoin(os_dir, filename)
1822
1823     try:
1824       st = os.stat(os_files[filename])
1825     except EnvironmentError, err:
1826       return False, ("File '%s' under path '%s' is missing (%s)" %
1827                      (filename, os_dir, _ErrnoOrStr(err)))
1828
1829     if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1830       return False, ("File '%s' under path '%s' is not a regular file" %
1831                      (filename, os_dir))
1832
1833     if filename in constants.OS_SCRIPTS:
1834       if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1835         return False, ("File '%s' under path '%s' is not executable" %
1836                        (filename, os_dir))
1837
1838   variants = None
1839   if constants.OS_VARIANTS_FILE in os_files:
1840     variants_file = os_files[constants.OS_VARIANTS_FILE]
1841     try:
1842       variants = utils.ReadFile(variants_file).splitlines()
1843     except EnvironmentError, err:
1844       return False, ("Error while reading the OS variants file at %s: %s" %
1845                      (variants_file, _ErrnoOrStr(err)))
1846     if not variants:
1847       return False, ("No supported os variant found")
1848
1849   os_obj = objects.OS(name=name, path=os_dir,
1850                       create_script=os_files[constants.OS_SCRIPT_CREATE],
1851                       export_script=os_files[constants.OS_SCRIPT_EXPORT],
1852                       import_script=os_files[constants.OS_SCRIPT_IMPORT],
1853                       rename_script=os_files[constants.OS_SCRIPT_RENAME],
1854                       supported_variants=variants,
1855                       api_versions=api_versions)
1856   return True, os_obj
1857
1858
1859 def OSFromDisk(name, base_dir=None):
1860   """Create an OS instance from disk.
1861
1862   This function will return an OS instance if the given name is a
1863   valid OS name. Otherwise, it will raise an appropriate
1864   L{RPCFail} exception, detailing why this is not a valid OS.
1865
1866   This is just a wrapper over L{_TryOSFromDisk}, which doesn't raise
1867   an exception but returns true/false status data.
1868
1869   @type base_dir: string
1870   @keyword base_dir: Base directory containing OS installations.
1871                      Defaults to a search in all the OS_SEARCH_PATH dirs.
1872   @rtype: L{objects.OS}
1873   @return: the OS instance if we find a valid one
1874   @raise RPCFail: if we don't find a valid OS
1875
1876   """
1877   name_only = name.split("+", 1)[0]
1878   status, payload = _TryOSFromDisk(name_only, base_dir)
1879
1880   if not status:
1881     _Fail(payload)
1882
1883   return payload
1884
1885
1886 def OSEnvironment(instance, inst_os, debug=0):
1887   """Calculate the environment for an os script.
1888
1889   @type instance: L{objects.Instance}
1890   @param instance: target instance for the os script run
1891   @type inst_os: L{objects.OS}
1892   @param inst_os: operating system for which the environment is being built
1893   @type debug: integer
1894   @param debug: debug level (0 or 1, for OS Api 10)
1895   @rtype: dict
1896   @return: dict of environment variables
1897   @raise errors.BlockDeviceError: if the block device
1898       cannot be found
1899
1900   """
1901   result = {}
1902   api_version = \
1903     max(constants.OS_API_VERSIONS.intersection(inst_os.api_versions))
1904   result['OS_API_VERSION'] = '%d' % api_version
1905   result['INSTANCE_NAME'] = instance.name
1906   result['INSTANCE_OS'] = instance.os
1907   result['HYPERVISOR'] = instance.hypervisor
1908   result['DISK_COUNT'] = '%d' % len(instance.disks)
1909   result['NIC_COUNT'] = '%d' % len(instance.nics)
1910   result['DEBUG_LEVEL'] = '%d' % debug
1911   if api_version >= constants.OS_API_V15:
1912     try:
1913       variant = instance.os.split('+', 1)[1]
1914     except IndexError:
1915       variant = inst_os.supported_variants[0]
1916     result['OS_VARIANT'] = variant
1917   for idx, disk in enumerate(instance.disks):
1918     real_disk = _OpenRealBD(disk)
1919     result['DISK_%d_PATH' % idx] = real_disk.dev_path
1920     result['DISK_%d_ACCESS' % idx] = disk.mode
1921     if constants.HV_DISK_TYPE in instance.hvparams:
1922       result['DISK_%d_FRONTEND_TYPE' % idx] = \
1923         instance.hvparams[constants.HV_DISK_TYPE]
1924     if disk.dev_type in constants.LDS_BLOCK:
1925       result['DISK_%d_BACKEND_TYPE' % idx] = 'block'
1926     elif disk.dev_type == constants.LD_FILE:
1927       result['DISK_%d_BACKEND_TYPE' % idx] = \
1928         'file:%s' % disk.physical_id[0]
1929   for idx, nic in enumerate(instance.nics):
1930     result['NIC_%d_MAC' % idx] = nic.mac
1931     if nic.ip:
1932       result['NIC_%d_IP' % idx] = nic.ip
1933     result['NIC_%d_MODE' % idx] = nic.nicparams[constants.NIC_MODE]
1934     if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
1935       result['NIC_%d_BRIDGE' % idx] = nic.nicparams[constants.NIC_LINK]
1936     if nic.nicparams[constants.NIC_LINK]:
1937       result['NIC_%d_LINK' % idx] = nic.nicparams[constants.NIC_LINK]
1938     if constants.HV_NIC_TYPE in instance.hvparams:
1939       result['NIC_%d_FRONTEND_TYPE' % idx] = \
1940         instance.hvparams[constants.HV_NIC_TYPE]
1941
1942   for source, kind in [(instance.beparams, "BE"), (instance.hvparams, "HV")]:
1943     for key, value in source.items():
1944       result["INSTANCE_%s_%s" % (kind, key)] = str(value)
1945
1946   return result
1947
1948
1949 def BlockdevGrow(disk, amount):
1950   """Grow a stack of block devices.
1951
1952   This function is called recursively, with the childrens being the
1953   first ones to resize.
1954
1955   @type disk: L{objects.Disk}
1956   @param disk: the disk to be grown
1957   @rtype: (status, result)
1958   @return: a tuple with the status of the operation
1959       (True/False), and the errors message if status
1960       is False
1961
1962   """
1963   r_dev = _RecursiveFindBD(disk)
1964   if r_dev is None:
1965     _Fail("Cannot find block device %s", disk)
1966
1967   try:
1968     r_dev.Grow(amount)
1969   except errors.BlockDeviceError, err:
1970     _Fail("Failed to grow block device: %s", err, exc=True)
1971
1972
1973 def BlockdevSnapshot(disk):
1974   """Create a snapshot copy of a block device.
1975
1976   This function is called recursively, and the snapshot is actually created
1977   just for the leaf lvm backend device.
1978
1979   @type disk: L{objects.Disk}
1980   @param disk: the disk to be snapshotted
1981   @rtype: string
1982   @return: snapshot disk path
1983
1984   """
1985   if disk.dev_type == constants.LD_DRBD8:
1986     if not disk.children:
1987       _Fail("DRBD device '%s' without backing storage cannot be snapshotted",
1988             disk.unique_id)
1989     return BlockdevSnapshot(disk.children[0])
1990   elif disk.dev_type == constants.LD_LV:
1991     r_dev = _RecursiveFindBD(disk)
1992     if r_dev is not None:
1993       # FIXME: choose a saner value for the snapshot size
1994       # let's stay on the safe side and ask for the full size, for now
1995       return r_dev.Snapshot(disk.size)
1996     else:
1997       _Fail("Cannot find block device %s", disk)
1998   else:
1999     _Fail("Cannot snapshot non-lvm block device '%s' of type '%s'",
2000           disk.unique_id, disk.dev_type)
2001
2002
2003 def ExportSnapshot(disk, dest_node, instance, cluster_name, idx, debug):
2004   """Export a block device snapshot to a remote node.
2005
2006   @type disk: L{objects.Disk}
2007   @param disk: the description of the disk to export
2008   @type dest_node: str
2009   @param dest_node: the destination node to export to
2010   @type instance: L{objects.Instance}
2011   @param instance: the instance object to whom the disk belongs
2012   @type cluster_name: str
2013   @param cluster_name: the cluster name, needed for SSH hostalias
2014   @type idx: int
2015   @param idx: the index of the disk in the instance's disk list,
2016       used to export to the OS scripts environment
2017   @type debug: integer
2018   @param debug: debug level, passed to the OS scripts
2019   @rtype: None
2020
2021   """
2022   inst_os = OSFromDisk(instance.os)
2023   export_env = OSEnvironment(instance, inst_os, debug)
2024
2025   export_script = inst_os.export_script
2026
2027   logfile = _InstanceLogName("export", inst_os.name, instance.name)
2028   if not os.path.exists(constants.LOG_OS_DIR):
2029     os.mkdir(constants.LOG_OS_DIR, 0750)
2030
2031   real_disk = _OpenRealBD(disk)
2032
2033   export_env['EXPORT_DEVICE'] = real_disk.dev_path
2034   export_env['EXPORT_INDEX'] = str(idx)
2035
2036   destdir = utils.PathJoin(constants.EXPORT_DIR, instance.name + ".new")
2037   destfile = disk.physical_id[1]
2038
2039   # the target command is built out of three individual commands,
2040   # which are joined by pipes; we check each individual command for
2041   # valid parameters
2042   expcmd = utils.BuildShellCmd("set -e; set -o pipefail; cd %s; %s 2>%s",
2043                                inst_os.path, export_script, logfile)
2044
2045   comprcmd = "gzip"
2046
2047   destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s",
2048                                 destdir, utils.PathJoin(destdir, destfile))
2049   remotecmd = _GetSshRunner(cluster_name).BuildCmd(dest_node,
2050                                                    constants.GANETI_RUNAS,
2051                                                    destcmd)
2052
2053   # all commands have been checked, so we're safe to combine them
2054   command = '|'.join([expcmd, comprcmd, utils.ShellQuoteArgs(remotecmd)])
2055
2056   result = utils.RunCmd(["bash", "-c", command], env=export_env)
2057
2058   if result.failed:
2059     _Fail("OS snapshot export command '%s' returned error: %s"
2060           " output: %s", command, result.fail_reason, result.output)
2061
2062
2063 def FinalizeExport(instance, snap_disks):
2064   """Write out the export configuration information.
2065
2066   @type instance: L{objects.Instance}
2067   @param instance: the instance which we export, used for
2068       saving configuration
2069   @type snap_disks: list of L{objects.Disk}
2070   @param snap_disks: list of snapshot block devices, which
2071       will be used to get the actual name of the dump file
2072
2073   @rtype: None
2074
2075   """
2076   destdir = utils.PathJoin(constants.EXPORT_DIR, instance.name + ".new")
2077   finaldestdir = utils.PathJoin(constants.EXPORT_DIR, instance.name)
2078
2079   config = objects.SerializableConfigParser()
2080
2081   config.add_section(constants.INISECT_EXP)
2082   config.set(constants.INISECT_EXP, 'version', '0')
2083   config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
2084   config.set(constants.INISECT_EXP, 'source', instance.primary_node)
2085   config.set(constants.INISECT_EXP, 'os', instance.os)
2086   config.set(constants.INISECT_EXP, 'compression', 'gzip')
2087
2088   config.add_section(constants.INISECT_INS)
2089   config.set(constants.INISECT_INS, 'name', instance.name)
2090   config.set(constants.INISECT_INS, 'memory', '%d' %
2091              instance.beparams[constants.BE_MEMORY])
2092   config.set(constants.INISECT_INS, 'vcpus', '%d' %
2093              instance.beparams[constants.BE_VCPUS])
2094   config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
2095   config.set(constants.INISECT_INS, 'hypervisor', instance.hypervisor)
2096
2097   nic_total = 0
2098   for nic_count, nic in enumerate(instance.nics):
2099     nic_total += 1
2100     config.set(constants.INISECT_INS, 'nic%d_mac' %
2101                nic_count, '%s' % nic.mac)
2102     config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
2103     for param in constants.NICS_PARAMETER_TYPES:
2104       config.set(constants.INISECT_INS, 'nic%d_%s' % (nic_count, param),
2105                  '%s' % nic.nicparams.get(param, None))
2106   # TODO: redundant: on load can read nics until it doesn't exist
2107   config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_total)
2108
2109   disk_total = 0
2110   for disk_count, disk in enumerate(snap_disks):
2111     if disk:
2112       disk_total += 1
2113       config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
2114                  ('%s' % disk.iv_name))
2115       config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
2116                  ('%s' % disk.physical_id[1]))
2117       config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
2118                  ('%d' % disk.size))
2119
2120   config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_total)
2121
2122   # New-style hypervisor/backend parameters
2123
2124   config.add_section(constants.INISECT_HYP)
2125   for name, value in instance.hvparams.items():
2126     if name not in constants.HVC_GLOBALS:
2127       config.set(constants.INISECT_HYP, name, str(value))
2128
2129   config.add_section(constants.INISECT_BEP)
2130   for name, value in instance.beparams.items():
2131     config.set(constants.INISECT_BEP, name, str(value))
2132
2133   utils.WriteFile(utils.PathJoin(destdir, constants.EXPORT_CONF_FILE),
2134                   data=config.Dumps())
2135   shutil.rmtree(finaldestdir, ignore_errors=True)
2136   shutil.move(destdir, finaldestdir)
2137
2138
2139 def ExportInfo(dest):
2140   """Get export configuration information.
2141
2142   @type dest: str
2143   @param dest: directory containing the export
2144
2145   @rtype: L{objects.SerializableConfigParser}
2146   @return: a serializable config file containing the
2147       export info
2148
2149   """
2150   cff = utils.PathJoin(dest, constants.EXPORT_CONF_FILE)
2151
2152   config = objects.SerializableConfigParser()
2153   config.read(cff)
2154
2155   if (not config.has_section(constants.INISECT_EXP) or
2156       not config.has_section(constants.INISECT_INS)):
2157     _Fail("Export info file doesn't have the required fields")
2158
2159   return config.Dumps()
2160
2161
2162 def ImportOSIntoInstance(instance, src_node, src_images, cluster_name, debug):
2163   """Import an os image into an instance.
2164
2165   @type instance: L{objects.Instance}
2166   @param instance: instance to import the disks into
2167   @type src_node: string
2168   @param src_node: source node for the disk images
2169   @type src_images: list of string
2170   @param src_images: absolute paths of the disk images
2171   @type debug: integer
2172   @param debug: debug level, passed to the OS scripts
2173   @rtype: list of boolean
2174   @return: each boolean represent the success of importing the n-th disk
2175
2176   """
2177   inst_os = OSFromDisk(instance.os)
2178   import_env = OSEnvironment(instance, inst_os, debug)
2179   import_script = inst_os.import_script
2180
2181   logfile = _InstanceLogName("import", instance.os, instance.name)
2182   if not os.path.exists(constants.LOG_OS_DIR):
2183     os.mkdir(constants.LOG_OS_DIR, 0750)
2184
2185   comprcmd = "gunzip"
2186   impcmd = utils.BuildShellCmd("(cd %s; %s >%s 2>&1)", inst_os.path,
2187                                import_script, logfile)
2188
2189   final_result = []
2190   for idx, image in enumerate(src_images):
2191     if image:
2192       destcmd = utils.BuildShellCmd('cat %s', image)
2193       remotecmd = _GetSshRunner(cluster_name).BuildCmd(src_node,
2194                                                        constants.GANETI_RUNAS,
2195                                                        destcmd)
2196       command = '|'.join([utils.ShellQuoteArgs(remotecmd), comprcmd, impcmd])
2197       import_env['IMPORT_DEVICE'] = import_env['DISK_%d_PATH' % idx]
2198       import_env['IMPORT_INDEX'] = str(idx)
2199       result = utils.RunCmd(command, env=import_env)
2200       if result.failed:
2201         logging.error("Disk import command '%s' returned error: %s"
2202                       " output: %s", command, result.fail_reason,
2203                       result.output)
2204         final_result.append("error importing disk %d: %s, %s" %
2205                             (idx, result.fail_reason, result.output[-100]))
2206
2207   if final_result:
2208     _Fail("; ".join(final_result), log=False)
2209
2210
2211 def ListExports():
2212   """Return a list of exports currently available on this machine.
2213
2214   @rtype: list
2215   @return: list of the exports
2216
2217   """
2218   if os.path.isdir(constants.EXPORT_DIR):
2219     return utils.ListVisibleFiles(constants.EXPORT_DIR)
2220   else:
2221     _Fail("No exports directory")
2222
2223
2224 def RemoveExport(export):
2225   """Remove an existing export from the node.
2226
2227   @type export: str
2228   @param export: the name of the export to remove
2229   @rtype: None
2230
2231   """
2232   target = utils.PathJoin(constants.EXPORT_DIR, export)
2233
2234   try:
2235     shutil.rmtree(target)
2236   except EnvironmentError, err:
2237     _Fail("Error while removing the export: %s", err, exc=True)
2238
2239
2240 def BlockdevRename(devlist):
2241   """Rename a list of block devices.
2242
2243   @type devlist: list of tuples
2244   @param devlist: list of tuples of the form  (disk,
2245       new_logical_id, new_physical_id); disk is an
2246       L{objects.Disk} object describing the current disk,
2247       and new logical_id/physical_id is the name we
2248       rename it to
2249   @rtype: boolean
2250   @return: True if all renames succeeded, False otherwise
2251
2252   """
2253   msgs = []
2254   result = True
2255   for disk, unique_id in devlist:
2256     dev = _RecursiveFindBD(disk)
2257     if dev is None:
2258       msgs.append("Can't find device %s in rename" % str(disk))
2259       result = False
2260       continue
2261     try:
2262       old_rpath = dev.dev_path
2263       dev.Rename(unique_id)
2264       new_rpath = dev.dev_path
2265       if old_rpath != new_rpath:
2266         DevCacheManager.RemoveCache(old_rpath)
2267         # FIXME: we should add the new cache information here, like:
2268         # DevCacheManager.UpdateCache(new_rpath, owner, ...)
2269         # but we don't have the owner here - maybe parse from existing
2270         # cache? for now, we only lose lvm data when we rename, which
2271         # is less critical than DRBD or MD
2272     except errors.BlockDeviceError, err:
2273       msgs.append("Can't rename device '%s' to '%s': %s" %
2274                   (dev, unique_id, err))
2275       logging.exception("Can't rename device '%s' to '%s'", dev, unique_id)
2276       result = False
2277   if not result:
2278     _Fail("; ".join(msgs))
2279
2280
2281 def _TransformFileStorageDir(file_storage_dir):
2282   """Checks whether given file_storage_dir is valid.
2283
2284   Checks wheter the given file_storage_dir is within the cluster-wide
2285   default file_storage_dir stored in SimpleStore. Only paths under that
2286   directory are allowed.
2287
2288   @type file_storage_dir: str
2289   @param file_storage_dir: the path to check
2290
2291   @return: the normalized path if valid, None otherwise
2292
2293   """
2294   if not constants.ENABLE_FILE_STORAGE:
2295     _Fail("File storage disabled at configure time")
2296   cfg = _GetConfig()
2297   file_storage_dir = os.path.normpath(file_storage_dir)
2298   base_file_storage_dir = cfg.GetFileStorageDir()
2299   if (os.path.commonprefix([file_storage_dir, base_file_storage_dir]) !=
2300       base_file_storage_dir):
2301     _Fail("File storage directory '%s' is not under base file"
2302           " storage directory '%s'", file_storage_dir, base_file_storage_dir)
2303   return file_storage_dir
2304
2305
2306 def CreateFileStorageDir(file_storage_dir):
2307   """Create file storage directory.
2308
2309   @type file_storage_dir: str
2310   @param file_storage_dir: directory to create
2311
2312   @rtype: tuple
2313   @return: tuple with first element a boolean indicating wheter dir
2314       creation was successful or not
2315
2316   """
2317   file_storage_dir = _TransformFileStorageDir(file_storage_dir)
2318   if os.path.exists(file_storage_dir):
2319     if not os.path.isdir(file_storage_dir):
2320       _Fail("Specified storage dir '%s' is not a directory",
2321             file_storage_dir)
2322   else:
2323     try:
2324       os.makedirs(file_storage_dir, 0750)
2325     except OSError, err:
2326       _Fail("Cannot create file storage directory '%s': %s",
2327             file_storage_dir, err, exc=True)
2328
2329
2330 def RemoveFileStorageDir(file_storage_dir):
2331   """Remove file storage directory.
2332
2333   Remove it only if it's empty. If not log an error and return.
2334
2335   @type file_storage_dir: str
2336   @param file_storage_dir: the directory we should cleanup
2337   @rtype: tuple (success,)
2338   @return: tuple of one element, C{success}, denoting
2339       whether the operation was successful
2340
2341   """
2342   file_storage_dir = _TransformFileStorageDir(file_storage_dir)
2343   if os.path.exists(file_storage_dir):
2344     if not os.path.isdir(file_storage_dir):
2345       _Fail("Specified Storage directory '%s' is not a directory",
2346             file_storage_dir)
2347     # deletes dir only if empty, otherwise we want to fail the rpc call
2348     try:
2349       os.rmdir(file_storage_dir)
2350     except OSError, err:
2351       _Fail("Cannot remove file storage directory '%s': %s",
2352             file_storage_dir, err)
2353
2354
2355 def RenameFileStorageDir(old_file_storage_dir, new_file_storage_dir):
2356   """Rename the file storage directory.
2357
2358   @type old_file_storage_dir: str
2359   @param old_file_storage_dir: the current path
2360   @type new_file_storage_dir: str
2361   @param new_file_storage_dir: the name we should rename to
2362   @rtype: tuple (success,)
2363   @return: tuple of one element, C{success}, denoting
2364       whether the operation was successful
2365
2366   """
2367   old_file_storage_dir = _TransformFileStorageDir(old_file_storage_dir)
2368   new_file_storage_dir = _TransformFileStorageDir(new_file_storage_dir)
2369   if not os.path.exists(new_file_storage_dir):
2370     if os.path.isdir(old_file_storage_dir):
2371       try:
2372         os.rename(old_file_storage_dir, new_file_storage_dir)
2373       except OSError, err:
2374         _Fail("Cannot rename '%s' to '%s': %s",
2375               old_file_storage_dir, new_file_storage_dir, err)
2376     else:
2377       _Fail("Specified storage dir '%s' is not a directory",
2378             old_file_storage_dir)
2379   else:
2380     if os.path.exists(old_file_storage_dir):
2381       _Fail("Cannot rename '%s' to '%s': both locations exist",
2382             old_file_storage_dir, new_file_storage_dir)
2383
2384
2385 def _EnsureJobQueueFile(file_name):
2386   """Checks whether the given filename is in the queue directory.
2387
2388   @type file_name: str
2389   @param file_name: the file name we should check
2390   @rtype: None
2391   @raises RPCFail: if the file is not valid
2392
2393   """
2394   queue_dir = os.path.normpath(constants.QUEUE_DIR)
2395   result = (os.path.commonprefix([queue_dir, file_name]) == queue_dir)
2396
2397   if not result:
2398     _Fail("Passed job queue file '%s' does not belong to"
2399           " the queue directory '%s'", file_name, queue_dir)
2400
2401
2402 def JobQueueUpdate(file_name, content):
2403   """Updates a file in the queue directory.
2404
2405   This is just a wrapper over L{utils.WriteFile}, with proper
2406   checking.
2407
2408   @type file_name: str
2409   @param file_name: the job file name
2410   @type content: str
2411   @param content: the new job contents
2412   @rtype: boolean
2413   @return: the success of the operation
2414
2415   """
2416   _EnsureJobQueueFile(file_name)
2417
2418   # Write and replace the file atomically
2419   utils.WriteFile(file_name, data=_Decompress(content))
2420
2421
2422 def JobQueueRename(old, new):
2423   """Renames a job queue file.
2424
2425   This is just a wrapper over os.rename with proper checking.
2426
2427   @type old: str
2428   @param old: the old (actual) file name
2429   @type new: str
2430   @param new: the desired file name
2431   @rtype: tuple
2432   @return: the success of the operation and payload
2433
2434   """
2435   _EnsureJobQueueFile(old)
2436   _EnsureJobQueueFile(new)
2437
2438   utils.RenameFile(old, new, mkdir=True)
2439
2440
2441 def JobQueueSetDrainFlag(drain_flag):
2442   """Set the drain flag for the queue.
2443
2444   This will set or unset the queue drain flag.
2445
2446   @type drain_flag: boolean
2447   @param drain_flag: if True, will set the drain flag, otherwise reset it.
2448   @rtype: truple
2449   @return: always True, None
2450   @warning: the function always returns True
2451
2452   """
2453   if drain_flag:
2454     utils.WriteFile(constants.JOB_QUEUE_DRAIN_FILE, data="", close=True)
2455   else:
2456     utils.RemoveFile(constants.JOB_QUEUE_DRAIN_FILE)
2457
2458
2459 def BlockdevClose(instance_name, disks):
2460   """Closes the given block devices.
2461
2462   This means they will be switched to secondary mode (in case of
2463   DRBD).
2464
2465   @param instance_name: if the argument is not empty, the symlinks
2466       of this instance will be removed
2467   @type disks: list of L{objects.Disk}
2468   @param disks: the list of disks to be closed
2469   @rtype: tuple (success, message)
2470   @return: a tuple of success and message, where success
2471       indicates the succes of the operation, and message
2472       which will contain the error details in case we
2473       failed
2474
2475   """
2476   bdevs = []
2477   for cf in disks:
2478     rd = _RecursiveFindBD(cf)
2479     if rd is None:
2480       _Fail("Can't find device %s", cf)
2481     bdevs.append(rd)
2482
2483   msg = []
2484   for rd in bdevs:
2485     try:
2486       rd.Close()
2487     except errors.BlockDeviceError, err:
2488       msg.append(str(err))
2489   if msg:
2490     _Fail("Can't make devices secondary: %s", ",".join(msg))
2491   else:
2492     if instance_name:
2493       _RemoveBlockDevLinks(instance_name, disks)
2494
2495
2496 def ValidateHVParams(hvname, hvparams):
2497   """Validates the given hypervisor parameters.
2498
2499   @type hvname: string
2500   @param hvname: the hypervisor name
2501   @type hvparams: dict
2502   @param hvparams: the hypervisor parameters to be validated
2503   @rtype: None
2504
2505   """
2506   try:
2507     hv_type = hypervisor.GetHypervisor(hvname)
2508     hv_type.ValidateParameters(hvparams)
2509   except errors.HypervisorError, err:
2510     _Fail(str(err), log=False)
2511
2512
2513 def DemoteFromMC():
2514   """Demotes the current node from master candidate role.
2515
2516   """
2517   # try to ensure we're not the master by mistake
2518   master, myself = ssconf.GetMasterAndMyself()
2519   if master == myself:
2520     _Fail("ssconf status shows I'm the master node, will not demote")
2521
2522   result = utils.RunCmd([constants.DAEMON_UTIL, "check", constants.MASTERD])
2523   if not result.failed:
2524     _Fail("The master daemon is running, will not demote")
2525
2526   try:
2527     if os.path.isfile(constants.CLUSTER_CONF_FILE):
2528       utils.CreateBackup(constants.CLUSTER_CONF_FILE)
2529   except EnvironmentError, err:
2530     if err.errno != errno.ENOENT:
2531       _Fail("Error while backing up cluster file: %s", err, exc=True)
2532
2533   utils.RemoveFile(constants.CLUSTER_CONF_FILE)
2534
2535
2536 def _FindDisks(nodes_ip, disks):
2537   """Sets the physical ID on disks and returns the block devices.
2538
2539   """
2540   # set the correct physical ID
2541   my_name = utils.HostInfo().name
2542   for cf in disks:
2543     cf.SetPhysicalID(my_name, nodes_ip)
2544
2545   bdevs = []
2546
2547   for cf in disks:
2548     rd = _RecursiveFindBD(cf)
2549     if rd is None:
2550       _Fail("Can't find device %s", cf)
2551     bdevs.append(rd)
2552   return bdevs
2553
2554
2555 def DrbdDisconnectNet(nodes_ip, disks):
2556   """Disconnects the network on a list of drbd devices.
2557
2558   """
2559   bdevs = _FindDisks(nodes_ip, disks)
2560
2561   # disconnect disks
2562   for rd in bdevs:
2563     try:
2564       rd.DisconnectNet()
2565     except errors.BlockDeviceError, err:
2566       _Fail("Can't change network configuration to standalone mode: %s",
2567             err, exc=True)
2568
2569
2570 def DrbdAttachNet(nodes_ip, disks, instance_name, multimaster):
2571   """Attaches the network on a list of drbd devices.
2572
2573   """
2574   bdevs = _FindDisks(nodes_ip, disks)
2575
2576   if multimaster:
2577     for idx, rd in enumerate(bdevs):
2578       try:
2579         _SymlinkBlockDev(instance_name, rd.dev_path, idx)
2580       except EnvironmentError, err:
2581         _Fail("Can't create symlink: %s", err)
2582   # reconnect disks, switch to new master configuration and if
2583   # needed primary mode
2584   for rd in bdevs:
2585     try:
2586       rd.AttachNet(multimaster)
2587     except errors.BlockDeviceError, err:
2588       _Fail("Can't change network configuration: %s", err)
2589
2590   # wait until the disks are connected; we need to retry the re-attach
2591   # if the device becomes standalone, as this might happen if the one
2592   # node disconnects and reconnects in a different mode before the
2593   # other node reconnects; in this case, one or both of the nodes will
2594   # decide it has wrong configuration and switch to standalone
2595
2596   def _Attach():
2597     all_connected = True
2598
2599     for rd in bdevs:
2600       stats = rd.GetProcStatus()
2601
2602       all_connected = (all_connected and
2603                        (stats.is_connected or stats.is_in_resync))
2604
2605       if stats.is_standalone:
2606         # peer had different config info and this node became
2607         # standalone, even though this should not happen with the
2608         # new staged way of changing disk configs
2609         try:
2610           rd.AttachNet(multimaster)
2611         except errors.BlockDeviceError, err:
2612           _Fail("Can't change network configuration: %s", err)
2613
2614     if not all_connected:
2615       raise utils.RetryAgain()
2616
2617   try:
2618     # Start with a delay of 100 miliseconds and go up to 5 seconds
2619     utils.Retry(_Attach, (0.1, 1.5, 5.0), 2 * 60)
2620   except utils.RetryTimeout:
2621     _Fail("Timeout in disk reconnecting")
2622
2623   if multimaster:
2624     # change to primary mode
2625     for rd in bdevs:
2626       try:
2627         rd.Open()
2628       except errors.BlockDeviceError, err:
2629         _Fail("Can't change to primary mode: %s", err)
2630
2631
2632 def DrbdWaitSync(nodes_ip, disks):
2633   """Wait until DRBDs have synchronized.
2634
2635   """
2636   def _helper(rd):
2637     stats = rd.GetProcStatus()
2638     if not (stats.is_connected or stats.is_in_resync):
2639       raise utils.RetryAgain()
2640     return stats
2641
2642   bdevs = _FindDisks(nodes_ip, disks)
2643
2644   min_resync = 100
2645   alldone = True
2646   for rd in bdevs:
2647     try:
2648       # poll each second for 15 seconds
2649       stats = utils.Retry(_helper, 1, 15, args=[rd])
2650     except utils.RetryTimeout:
2651       stats = rd.GetProcStatus()
2652       # last check
2653       if not (stats.is_connected or stats.is_in_resync):
2654         _Fail("DRBD device %s is not in sync: stats=%s", rd, stats)
2655     alldone = alldone and (not stats.is_in_resync)
2656     if stats.sync_percent is not None:
2657       min_resync = min(min_resync, stats.sync_percent)
2658
2659   return (alldone, min_resync)
2660
2661
2662 def PowercycleNode(hypervisor_type):
2663   """Hard-powercycle the node.
2664
2665   Because we need to return first, and schedule the powercycle in the
2666   background, we won't be able to report failures nicely.
2667
2668   """
2669   hyper = hypervisor.GetHypervisor(hypervisor_type)
2670   try:
2671     pid = os.fork()
2672   except OSError:
2673     # if we can't fork, we'll pretend that we're in the child process
2674     pid = 0
2675   if pid > 0:
2676     return "Reboot scheduled in 5 seconds"
2677   time.sleep(5)
2678   hyper.PowercycleNode()
2679
2680
2681 class HooksRunner(object):
2682   """Hook runner.
2683
2684   This class is instantiated on the node side (ganeti-noded) and not
2685   on the master side.
2686
2687   """
2688   def __init__(self, hooks_base_dir=None):
2689     """Constructor for hooks runner.
2690
2691     @type hooks_base_dir: str or None
2692     @param hooks_base_dir: if not None, this overrides the
2693         L{constants.HOOKS_BASE_DIR} (useful for unittests)
2694
2695     """
2696     if hooks_base_dir is None:
2697       hooks_base_dir = constants.HOOKS_BASE_DIR
2698     # yeah, _BASE_DIR is not valid for attributes, we use it like a
2699     # constant
2700     self._BASE_DIR = hooks_base_dir # pylint: disable-msg=C0103
2701
2702   def RunHooks(self, hpath, phase, env):
2703     """Run the scripts in the hooks directory.
2704
2705     @type hpath: str
2706     @param hpath: the path to the hooks directory which
2707         holds the scripts
2708     @type phase: str
2709     @param phase: either L{constants.HOOKS_PHASE_PRE} or
2710         L{constants.HOOKS_PHASE_POST}
2711     @type env: dict
2712     @param env: dictionary with the environment for the hook
2713     @rtype: list
2714     @return: list of 3-element tuples:
2715       - script path
2716       - script result, either L{constants.HKR_SUCCESS} or
2717         L{constants.HKR_FAIL}
2718       - output of the script
2719
2720     @raise errors.ProgrammerError: for invalid input
2721         parameters
2722
2723     """
2724     if phase == constants.HOOKS_PHASE_PRE:
2725       suffix = "pre"
2726     elif phase == constants.HOOKS_PHASE_POST:
2727       suffix = "post"
2728     else:
2729       _Fail("Unknown hooks phase '%s'", phase)
2730
2731
2732     subdir = "%s-%s.d" % (hpath, suffix)
2733     dir_name = utils.PathJoin(self._BASE_DIR, subdir)
2734
2735     results = []
2736
2737     if not os.path.isdir(dir_name):
2738       # for non-existing/non-dirs, we simply exit instead of logging a
2739       # warning at every operation
2740       return results
2741
2742     runparts_results = utils.RunParts(dir_name, env=env, reset_env=True)
2743
2744     for (relname, relstatus, runresult)  in runparts_results:
2745       if relstatus == constants.RUNPARTS_SKIP:
2746         rrval = constants.HKR_SKIP
2747         output = ""
2748       elif relstatus == constants.RUNPARTS_ERR:
2749         rrval = constants.HKR_FAIL
2750         output = "Hook script execution error: %s" % runresult
2751       elif relstatus == constants.RUNPARTS_RUN:
2752         if runresult.failed:
2753           rrval = constants.HKR_FAIL
2754         else:
2755           rrval = constants.HKR_SUCCESS
2756         output = utils.SafeEncode(runresult.output.strip())
2757       results.append(("%s/%s" % (subdir, relname), rrval, output))
2758
2759     return results
2760
2761
2762 class IAllocatorRunner(object):
2763   """IAllocator runner.
2764
2765   This class is instantiated on the node side (ganeti-noded) and not on
2766   the master side.
2767
2768   """
2769   @staticmethod
2770   def Run(name, idata):
2771     """Run an iallocator script.
2772
2773     @type name: str
2774     @param name: the iallocator script name
2775     @type idata: str
2776     @param idata: the allocator input data
2777
2778     @rtype: tuple
2779     @return: two element tuple of:
2780        - status
2781        - either error message or stdout of allocator (for success)
2782
2783     """
2784     alloc_script = utils.FindFile(name, constants.IALLOCATOR_SEARCH_PATH,
2785                                   os.path.isfile)
2786     if alloc_script is None:
2787       _Fail("iallocator module '%s' not found in the search path", name)
2788
2789     fd, fin_name = tempfile.mkstemp(prefix="ganeti-iallocator.")
2790     try:
2791       os.write(fd, idata)
2792       os.close(fd)
2793       result = utils.RunCmd([alloc_script, fin_name])
2794       if result.failed:
2795         _Fail("iallocator module '%s' failed: %s, output '%s'",
2796               name, result.fail_reason, result.output)
2797     finally:
2798       os.unlink(fin_name)
2799
2800     return result.stdout
2801
2802
2803 class DevCacheManager(object):
2804   """Simple class for managing a cache of block device information.
2805
2806   """
2807   _DEV_PREFIX = "/dev/"
2808   _ROOT_DIR = constants.BDEV_CACHE_DIR
2809
2810   @classmethod
2811   def _ConvertPath(cls, dev_path):
2812     """Converts a /dev/name path to the cache file name.
2813
2814     This replaces slashes with underscores and strips the /dev
2815     prefix. It then returns the full path to the cache file.
2816
2817     @type dev_path: str
2818     @param dev_path: the C{/dev/} path name
2819     @rtype: str
2820     @return: the converted path name
2821
2822     """
2823     if dev_path.startswith(cls._DEV_PREFIX):
2824       dev_path = dev_path[len(cls._DEV_PREFIX):]
2825     dev_path = dev_path.replace("/", "_")
2826     fpath = utils.PathJoin(cls._ROOT_DIR, "bdev_%s" % dev_path)
2827     return fpath
2828
2829   @classmethod
2830   def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
2831     """Updates the cache information for a given device.
2832
2833     @type dev_path: str
2834     @param dev_path: the pathname of the device
2835     @type owner: str
2836     @param owner: the owner (instance name) of the device
2837     @type on_primary: bool
2838     @param on_primary: whether this is the primary
2839         node nor not
2840     @type iv_name: str
2841     @param iv_name: the instance-visible name of the
2842         device, as in objects.Disk.iv_name
2843
2844     @rtype: None
2845
2846     """
2847     if dev_path is None:
2848       logging.error("DevCacheManager.UpdateCache got a None dev_path")
2849       return
2850     fpath = cls._ConvertPath(dev_path)
2851     if on_primary:
2852       state = "primary"
2853     else:
2854       state = "secondary"
2855     if iv_name is None:
2856       iv_name = "not_visible"
2857     fdata = "%s %s %s\n" % (str(owner), state, iv_name)
2858     try:
2859       utils.WriteFile(fpath, data=fdata)
2860     except EnvironmentError, err:
2861       logging.exception("Can't update bdev cache for %s: %s", dev_path, err)
2862
2863   @classmethod
2864   def RemoveCache(cls, dev_path):
2865     """Remove data for a dev_path.
2866
2867     This is just a wrapper over L{utils.RemoveFile} with a converted
2868     path name and logging.
2869
2870     @type dev_path: str
2871     @param dev_path: the pathname of the device
2872
2873     @rtype: None
2874
2875     """
2876     if dev_path is None:
2877       logging.error("DevCacheManager.RemoveCache got a None dev_path")
2878       return
2879     fpath = cls._ConvertPath(dev_path)
2880     try:
2881       utils.RemoveFile(fpath)
2882     except EnvironmentError, err:
2883       logging.exception("Can't update bdev cache for %s: %s", dev_path, err)