Enhance mirror operations for DRBD8
[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
25 import os
26 import os.path
27 import shutil
28 import time
29 import tempfile
30 import stat
31 import errno
32 import re
33 import subprocess
34
35 from ganeti import logger
36 from ganeti import errors
37 from ganeti import utils
38 from ganeti import ssh
39 from ganeti import hypervisor
40 from ganeti import constants
41 from ganeti import bdev
42 from ganeti import objects
43 from ganeti import ssconf
44
45
46 def StartMaster():
47   """Activate local node as master node.
48
49   There are two needed steps for this:
50     - run the master script
51     - register the cron script
52
53   """
54   result = utils.RunCmd([constants.MASTER_SCRIPT, "-d", "start"])
55
56   if result.failed:
57     logger.Error("could not activate cluster interface with command %s,"
58                  " error: '%s'" % (result.cmd, result.output))
59     return False
60
61   return True
62
63
64 def StopMaster():
65   """Deactivate this node as master.
66
67   This does two things:
68     - run the master stop script
69     - remove link to master cron script.
70
71   """
72   result = utils.RunCmd([constants.MASTER_SCRIPT, "-d", "stop"])
73
74   if result.failed:
75     logger.Error("could not deactivate cluster interface with command %s,"
76                  " error: '%s'" % (result.cmd, result.output))
77     return False
78
79   return True
80
81
82 def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
83   """Joins this node to the cluster.
84
85   This does the following:
86       - updates the hostkeys of the machine (rsa and dsa)
87       - adds the ssh private key to the user
88       - adds the ssh public key to the users' authorized_keys file
89
90   """
91   sshd_keys =  [(constants.SSH_HOST_RSA_PRIV, rsa, 0600),
92                 (constants.SSH_HOST_RSA_PUB, rsapub, 0644),
93                 (constants.SSH_HOST_DSA_PRIV, dsa, 0600),
94                 (constants.SSH_HOST_DSA_PUB, dsapub, 0644)]
95   for name, content, mode in sshd_keys:
96     utils.WriteFile(name, data=content, mode=mode)
97
98   try:
99     priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS,
100                                                     mkdir=True)
101   except errors.OpExecError, err:
102     logger.Error("Error while processing user ssh files: %s" % err)
103     return False
104
105   for name, content in [(priv_key, sshkey), (pub_key, sshpub)]:
106     utils.WriteFile(name, data=content, mode=0600)
107
108   utils.AddAuthorizedKey(auth_keys, sshpub)
109
110   utils.RunCmd([constants.SSH_INITD_SCRIPT, "restart"])
111
112   return True
113
114
115 def LeaveCluster():
116   """Cleans up the current node and prepares it to be removed from the cluster.
117
118   """
119   if os.path.isdir(constants.DATA_DIR):
120     for rel_name in utils.ListVisibleFiles(constants.DATA_DIR):
121       full_name = os.path.join(constants.DATA_DIR, rel_name)
122       if os.path.isfile(full_name) and not os.path.islink(full_name):
123         utils.RemoveFile(full_name)
124
125
126   try:
127     priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
128   except errors.OpExecError, err:
129     logger.Error("Error while processing ssh files: %s" % err)
130     return
131
132   f = open(pub_key, 'r')
133   try:
134     utils.RemoveAuthorizedKey(auth_keys, f.read(8192))
135   finally:
136     f.close()
137
138   utils.RemoveFile(priv_key)
139   utils.RemoveFile(pub_key)
140
141
142 def GetNodeInfo(vgname):
143   """Gives back a hash with different informations about the node.
144
145   Returns:
146     { 'vg_size' : xxx,  'vg_free' : xxx, 'memory_domain0': xxx,
147       'memory_free' : xxx, 'memory_total' : xxx }
148     where
149     vg_size is the size of the configured volume group in MiB
150     vg_free is the free size of the volume group in MiB
151     memory_dom0 is the memory allocated for domain0 in MiB
152     memory_free is the currently available (free) ram in MiB
153     memory_total is the total number of ram in MiB
154
155   """
156   outputarray = {}
157   vginfo = _GetVGInfo(vgname)
158   outputarray['vg_size'] = vginfo['vg_size']
159   outputarray['vg_free'] = vginfo['vg_free']
160
161   hyper = hypervisor.GetHypervisor()
162   hyp_info = hyper.GetNodeInfo()
163   if hyp_info is not None:
164     outputarray.update(hyp_info)
165
166   f = open("/proc/sys/kernel/random/boot_id", 'r')
167   try:
168     outputarray["bootid"] = f.read(128).rstrip("\n")
169   finally:
170     f.close()
171
172   return outputarray
173
174
175 def VerifyNode(what):
176   """Verify the status of the local node.
177
178   Args:
179     what - a dictionary of things to check:
180       'filelist' : list of files for which to compute checksums
181       'nodelist' : list of nodes we should check communication with
182       'hypervisor': run the hypervisor-specific verify
183
184   Requested files on local node are checksummed and the result returned.
185
186   The nodelist is traversed, with the following checks being made
187   for each node:
188   - known_hosts key correct
189   - correct resolving of node name (target node returns its own hostname
190     by ssh-execution of 'hostname', result compared against name in list.
191
192   """
193   result = {}
194
195   if 'hypervisor' in what:
196     result['hypervisor'] = hypervisor.GetHypervisor().Verify()
197
198   if 'filelist' in what:
199     result['filelist'] = utils.FingerprintFiles(what['filelist'])
200
201   if 'nodelist' in what:
202     result['nodelist'] = {}
203     for node in what['nodelist']:
204       success, message = ssh.VerifyNodeHostname(node)
205       if not success:
206         result['nodelist'][node] = message
207   return result
208
209
210 def GetVolumeList(vg_name):
211   """Compute list of logical volumes and their size.
212
213   Returns:
214     dictionary of all partions (key) with their size:
215     test1: 20.06MiB
216
217   """
218   result = utils.RunCmd(["lvs", "--noheadings", "--units=m",
219                          "-oname,size", vg_name])
220   if result.failed:
221     logger.Error("Failed to list logical volumes, lvs output: %s" %
222                  result.output)
223     return {}
224
225   lvlist = [line.split() for line in result.output.splitlines()]
226   return dict(lvlist)
227
228
229 def ListVolumeGroups():
230   """List the volume groups and their size.
231
232   Returns:
233     Dictionary with keys volume name and values the size of the volume
234
235   """
236   return utils.ListVolumeGroups()
237
238
239 def NodeVolumes():
240   """List all volumes on this node.
241
242   """
243   result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
244                          "--separator=|",
245                          "--options=lv_name,lv_size,devices,vg_name"])
246   if result.failed:
247     logger.Error("Failed to list logical volumes, lvs output: %s" %
248                  result.output)
249     return {}
250
251   def parse_dev(dev):
252     if '(' in dev:
253       return dev.split('(')[0]
254     else:
255       return dev
256
257   def map_line(line):
258     return {
259       'name': line[0].strip(),
260       'size': line[1].strip(),
261       'dev': parse_dev(line[2].strip()),
262       'vg': line[3].strip(),
263     }
264
265   return [map_line(line.split('|')) for line in result.output.splitlines()]
266
267
268 def BridgesExist(bridges_list):
269   """Check if a list of bridges exist on the current node.
270
271   Returns:
272     True if all of them exist, false otherwise
273
274   """
275   for bridge in bridges_list:
276     if not utils.BridgeExists(bridge):
277       return False
278
279   return True
280
281
282 def GetInstanceList():
283   """Provides a list of instances.
284
285   Returns:
286     A list of all running instances on the current node
287     - instance1.example.com
288     - instance2.example.com
289
290   """
291   try:
292     names = hypervisor.GetHypervisor().ListInstances()
293   except errors.HypervisorError, err:
294     logger.Error("error enumerating instances: %s" % str(err))
295     raise
296
297   return names
298
299
300 def GetInstanceInfo(instance):
301   """Gives back the informations about an instance as a dictionary.
302
303   Args:
304     instance: name of the instance (ex. instance1.example.com)
305
306   Returns:
307     { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
308     where
309     memory: memory size of instance (int)
310     state: xen state of instance (string)
311     time: cpu time of instance (float)
312
313   """
314   output = {}
315
316   iinfo = hypervisor.GetHypervisor().GetInstanceInfo(instance)
317   if iinfo is not None:
318     output['memory'] = iinfo[2]
319     output['state'] = iinfo[4]
320     output['time'] = iinfo[5]
321
322   return output
323
324
325 def GetAllInstancesInfo():
326   """Gather data about all instances.
327
328   This is the equivalent of `GetInstanceInfo()`, except that it
329   computes data for all instances at once, thus being faster if one
330   needs data about more than one instance.
331
332   Returns: a dictionary of dictionaries, keys being the instance name,
333     and with values:
334     { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
335     where
336     memory: memory size of instance (int)
337     state: xen state of instance (string)
338     time: cpu time of instance (float)
339     vcpus: the number of cpus
340
341   """
342   output = {}
343
344   iinfo = hypervisor.GetHypervisor().GetAllInstancesInfo()
345   if iinfo:
346     for name, inst_id, memory, vcpus, state, times in iinfo:
347       output[name] = {
348         'memory': memory,
349         'vcpus': vcpus,
350         'state': state,
351         'time': times,
352         }
353
354   return output
355
356
357 def AddOSToInstance(instance, os_disk, swap_disk):
358   """Add an OS to an instance.
359
360   Args:
361     instance: the instance object
362     os_disk: the instance-visible name of the os device
363     swap_disk: the instance-visible name of the swap device
364
365   """
366   inst_os = OSFromDisk(instance.os)
367
368   create_script = inst_os.create_script
369
370   os_device = instance.FindDisk(os_disk)
371   if os_device is None:
372     logger.Error("Can't find this device-visible name '%s'" % os_disk)
373     return False
374
375   swap_device = instance.FindDisk(swap_disk)
376   if swap_device is None:
377     logger.Error("Can't find this device-visible name '%s'" % swap_disk)
378     return False
379
380   real_os_dev = _RecursiveFindBD(os_device)
381   if real_os_dev is None:
382     raise errors.BlockDeviceError("Block device '%s' is not set up" %
383                                   str(os_device))
384   real_os_dev.Open()
385
386   real_swap_dev = _RecursiveFindBD(swap_device)
387   if real_swap_dev is None:
388     raise errors.BlockDeviceError("Block device '%s' is not set up" %
389                                   str(swap_device))
390   real_swap_dev.Open()
391
392   logfile = "%s/add-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
393                                      instance.name, int(time.time()))
394   if not os.path.exists(constants.LOG_OS_DIR):
395     os.mkdir(constants.LOG_OS_DIR, 0750)
396
397   command = utils.BuildShellCmd("cd %s && %s -i %s -b %s -s %s &>%s",
398                                 inst_os.path, create_script, instance.name,
399                                 real_os_dev.dev_path, real_swap_dev.dev_path,
400                                 logfile)
401
402   result = utils.RunCmd(command)
403
404   if result.failed:
405     logger.Error("os create command '%s' returned error: %s"
406                  " output: %s" %
407                  (command, result.fail_reason, result.output))
408     return False
409
410   return True
411
412
413 def RunRenameInstance(instance, old_name, os_disk, swap_disk):
414   """Run the OS rename script for an instance.
415
416   Args:
417     instance: the instance object
418     old_name: the old name of the instance
419     os_disk: the instance-visible name of the os device
420     swap_disk: the instance-visible name of the swap device
421
422   """
423   inst_os = OSFromDisk(instance.os)
424
425   script = inst_os.rename_script
426
427   os_device = instance.FindDisk(os_disk)
428   if os_device is None:
429     logger.Error("Can't find this device-visible name '%s'" % os_disk)
430     return False
431
432   swap_device = instance.FindDisk(swap_disk)
433   if swap_device is None:
434     logger.Error("Can't find this device-visible name '%s'" % swap_disk)
435     return False
436
437   real_os_dev = _RecursiveFindBD(os_device)
438   if real_os_dev is None:
439     raise errors.BlockDeviceError("Block device '%s' is not set up" %
440                                   str(os_device))
441   real_os_dev.Open()
442
443   real_swap_dev = _RecursiveFindBD(swap_device)
444   if real_swap_dev is None:
445     raise errors.BlockDeviceError("Block device '%s' is not set up" %
446                                   str(swap_device))
447   real_swap_dev.Open()
448
449   logfile = "%s/rename-%s-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
450                                            old_name,
451                                            instance.name, int(time.time()))
452   if not os.path.exists(constants.LOG_OS_DIR):
453     os.mkdir(constants.LOG_OS_DIR, 0750)
454
455   command = utils.BuildShellCmd("cd %s && %s -o %s -n %s -b %s -s %s &>%s",
456                                 inst_os.path, script, old_name, instance.name,
457                                 real_os_dev.dev_path, real_swap_dev.dev_path,
458                                 logfile)
459
460   result = utils.RunCmd(command)
461
462   if result.failed:
463     logger.Error("os create command '%s' returned error: %s"
464                  " output: %s" %
465                  (command, result.fail_reason, result.output))
466     return False
467
468   return True
469
470
471 def _GetVGInfo(vg_name):
472   """Get informations about the volume group.
473
474   Args:
475     vg_name: the volume group
476
477   Returns:
478     { 'vg_size' : xxx, 'vg_free' : xxx, 'pv_count' : xxx }
479     where
480     vg_size is the total size of the volume group in MiB
481     vg_free is the free size of the volume group in MiB
482     pv_count are the number of physical disks in that vg
483
484   """
485   retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
486                          "--nosuffix", "--units=m", "--separator=:", vg_name])
487
488   if retval.failed:
489     errmsg = "volume group %s not present" % vg_name
490     logger.Error(errmsg)
491     raise errors.LVMError(errmsg)
492   valarr = retval.stdout.strip().split(':')
493   retdic = {
494     "vg_size": int(round(float(valarr[0]), 0)),
495     "vg_free": int(round(float(valarr[1]), 0)),
496     "pv_count": int(valarr[2]),
497     }
498   return retdic
499
500
501 def _GatherBlockDevs(instance):
502   """Set up an instance's block device(s).
503
504   This is run on the primary node at instance startup. The block
505   devices must be already assembled.
506
507   """
508   block_devices = []
509   for disk in instance.disks:
510     device = _RecursiveFindBD(disk)
511     if device is None:
512       raise errors.BlockDeviceError("Block device '%s' is not set up." %
513                                     str(disk))
514     device.Open()
515     block_devices.append((disk, device))
516   return block_devices
517
518
519 def StartInstance(instance, extra_args):
520   """Start an instance.
521
522   Args:
523     instance - name of instance to start.
524
525   """
526   running_instances = GetInstanceList()
527
528   if instance.name in running_instances:
529     return True
530
531   block_devices = _GatherBlockDevs(instance)
532   hyper = hypervisor.GetHypervisor()
533
534   try:
535     hyper.StartInstance(instance, block_devices, extra_args)
536   except errors.HypervisorError, err:
537     logger.Error("Failed to start instance: %s" % err)
538     return False
539
540   return True
541
542
543 def ShutdownInstance(instance):
544   """Shut an instance down.
545
546   Args:
547     instance - name of instance to shutdown.
548
549   """
550   running_instances = GetInstanceList()
551
552   if instance.name not in running_instances:
553     return True
554
555   hyper = hypervisor.GetHypervisor()
556   try:
557     hyper.StopInstance(instance)
558   except errors.HypervisorError, err:
559     logger.Error("Failed to stop instance: %s" % err)
560     return False
561
562   # test every 10secs for 2min
563   shutdown_ok = False
564
565   time.sleep(1)
566   for dummy in range(11):
567     if instance.name not in GetInstanceList():
568       break
569     time.sleep(10)
570   else:
571     # the shutdown did not succeed
572     logger.Error("shutdown of '%s' unsuccessful, using destroy" % instance)
573
574     try:
575       hyper.StopInstance(instance, force=True)
576     except errors.HypervisorError, err:
577       logger.Error("Failed to stop instance: %s" % err)
578       return False
579
580     time.sleep(1)
581     if instance.name in GetInstanceList():
582       logger.Error("could not shutdown instance '%s' even by destroy")
583       return False
584
585   return True
586
587
588 def RebootInstance(instance, reboot_type, extra_args):
589   """Reboot an instance.
590
591   Args:
592     instance    - name of instance to reboot
593     reboot_type - how to reboot [soft,hard,full]
594
595   """
596   running_instances = GetInstanceList()
597
598   if instance.name not in running_instances:
599     logger.Error("Cannot reboot instance that is not running")
600     return False
601
602   hyper = hypervisor.GetHypervisor()
603   if reboot_type == constants.INSTANCE_REBOOT_SOFT:
604     try:
605       hyper.RebootInstance(instance)
606     except errors.HypervisorError, err:
607       logger.Error("Failed to soft reboot instance: %s" % err)
608       return False
609   elif reboot_type == constants.INSTANCE_REBOOT_HARD:
610     try:
611       ShutdownInstance(instance)
612       StartInstance(instance, extra_args)
613     except errors.HypervisorError, err:
614       logger.Error("Failed to hard reboot instance: %s" % err)
615       return False
616   else:
617     raise errors.ParameterError("reboot_type invalid")
618
619
620   return True
621
622
623 def CreateBlockDevice(disk, size, owner, on_primary, info):
624   """Creates a block device for an instance.
625
626   Args:
627    bdev: a ganeti.objects.Disk object
628    size: the size of the physical underlying devices
629    do_open: if the device should be `Assemble()`-d and
630             `Open()`-ed after creation
631
632   Returns:
633     the new unique_id of the device (this can sometime be
634     computed only after creation), or None. On secondary nodes,
635     it's not required to return anything.
636
637   """
638   clist = []
639   if disk.children:
640     for child in disk.children:
641       crdev = _RecursiveAssembleBD(child, owner, on_primary)
642       if on_primary or disk.AssembleOnSecondary():
643         # we need the children open in case the device itself has to
644         # be assembled
645         crdev.Open()
646       else:
647         crdev.Close()
648       clist.append(crdev)
649   try:
650     device = bdev.FindDevice(disk.dev_type, disk.physical_id, clist)
651     if device is not None:
652       logger.Info("removing existing device %s" % disk)
653       device.Remove()
654   except errors.BlockDeviceError, err:
655     pass
656
657   device = bdev.Create(disk.dev_type, disk.physical_id,
658                        clist, size)
659   if device is None:
660     raise ValueError("Can't create child device for %s, %s" %
661                      (disk, size))
662   if on_primary or disk.AssembleOnSecondary():
663     if not device.Assemble():
664       raise errors.BlockDeviceError("Can't assemble device after creation,"
665                                     " very unusual event - check the node"
666                                     " daemon logs")
667     device.SetSyncSpeed(constants.SYNC_SPEED)
668     if on_primary or disk.OpenOnSecondary():
669       device.Open(force=True)
670     DevCacheManager.UpdateCache(device.dev_path, owner,
671                                 on_primary, disk.iv_name)
672
673   device.SetInfo(info)
674
675   physical_id = device.unique_id
676   return physical_id
677
678
679 def RemoveBlockDevice(disk):
680   """Remove a block device.
681
682   This is intended to be called recursively.
683
684   """
685   try:
686     # since we are removing the device, allow a partial match
687     # this allows removal of broken mirrors
688     rdev = _RecursiveFindBD(disk, allow_partial=True)
689   except errors.BlockDeviceError, err:
690     # probably can't attach
691     logger.Info("Can't attach to device %s in remove" % disk)
692     rdev = None
693   if rdev is not None:
694     r_path = rdev.dev_path
695     result = rdev.Remove()
696     if result:
697       DevCacheManager.RemoveCache(r_path)
698   else:
699     result = True
700   if disk.children:
701     for child in disk.children:
702       result = result and RemoveBlockDevice(child)
703   return result
704
705
706 def _RecursiveAssembleBD(disk, owner, as_primary):
707   """Activate a block device for an instance.
708
709   This is run on the primary and secondary nodes for an instance.
710
711   This function is called recursively.
712
713   Args:
714     disk: a objects.Disk object
715     as_primary: if we should make the block device read/write
716
717   Returns:
718     the assembled device or None (in case no device was assembled)
719
720   If the assembly is not successful, an exception is raised.
721
722   """
723   children = []
724   if disk.children:
725     mcn = disk.ChildrenNeeded()
726     if mcn == -1:
727       mcn = 0 # max number of Nones allowed
728     else:
729       mcn = len(disk.children) - mcn # max number of Nones
730     for chld_disk in disk.children:
731       try:
732         cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
733       except errors.BlockDeviceError, err:
734         if children.count(None) > mcn:
735           raise
736         cdev = None
737         logger.Debug("Error in child activation: %s" % str(err))
738       children.append(cdev)
739
740   if as_primary or disk.AssembleOnSecondary():
741     r_dev = bdev.AttachOrAssemble(disk.dev_type, disk.physical_id, children)
742     r_dev.SetSyncSpeed(constants.SYNC_SPEED)
743     result = r_dev
744     if as_primary or disk.OpenOnSecondary():
745       r_dev.Open()
746     else:
747       r_dev.Close()
748     DevCacheManager.UpdateCache(r_dev.dev_path, owner,
749                                 as_primary, disk.iv_name)
750
751   else:
752     result = True
753   return result
754
755
756 def AssembleBlockDevice(disk, owner, as_primary):
757   """Activate a block device for an instance.
758
759   This is a wrapper over _RecursiveAssembleBD.
760
761   Returns:
762     a /dev path for primary nodes
763     True for secondary nodes
764
765   """
766   result = _RecursiveAssembleBD(disk, owner, as_primary)
767   if isinstance(result, bdev.BlockDev):
768     result = result.dev_path
769   return result
770
771
772 def ShutdownBlockDevice(disk):
773   """Shut down a block device.
774
775   First, if the device is assembled (can `Attach()`), then the device
776   is shutdown. Then the children of the device are shutdown.
777
778   This function is called recursively. Note that we don't cache the
779   children or such, as oppossed to assemble, shutdown of different
780   devices doesn't require that the upper device was active.
781
782   """
783   r_dev = _RecursiveFindBD(disk)
784   if r_dev is not None:
785     r_path = r_dev.dev_path
786     result = r_dev.Shutdown()
787     if result:
788       DevCacheManager.RemoveCache(r_path)
789   else:
790     result = True
791   if disk.children:
792     for child in disk.children:
793       result = result and ShutdownBlockDevice(child)
794   return result
795
796
797 def MirrorAddChildren(parent_cdev, new_cdevs):
798   """Extend a mirrored block device.
799
800   """
801   parent_bdev = _RecursiveFindBD(parent_cdev, allow_partial=True)
802   if parent_bdev is None:
803     logger.Error("Can't find parent device")
804     return False
805   new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
806   if new_bdevs.count(None) > 0:
807     logger.Error("Can't find new device(s) to add: %s:%s" %
808                  (new_bdevs, new_cdevs))
809     return False
810   parent_bdev.AddChildren(new_bdevs)
811   return True
812
813
814 def MirrorRemoveChildren(parent_cdev, new_cdevs):
815   """Shrink a mirrored block device.
816
817   """
818   parent_bdev = _RecursiveFindBD(parent_cdev)
819   if parent_bdev is None:
820     logger.Error("Can't find parent in remove children: %s" % parent_cdev)
821     return False
822   devs = []
823   for disk in new_cdevs:
824     rpath = disk.StaticDevPath()
825     if rpath is None:
826       bd = _RecursiveFindBD(disk)
827       if bd is None:
828         logger.Error("Can't find dynamic device %s while removing children" %
829                      disk)
830         return False
831       else:
832         devs.append(bd.dev_path)
833     else:
834       devs.append(rpath)
835   parent_bdev.RemoveChildren(devs)
836   return True
837
838
839 def GetMirrorStatus(disks):
840   """Get the mirroring status of a list of devices.
841
842   Args:
843     disks: list of `objects.Disk`
844
845   Returns:
846     list of (mirror_done, estimated_time) tuples, which
847     are the result of bdev.BlockDevice.CombinedSyncStatus()
848
849   """
850   stats = []
851   for dsk in disks:
852     rbd = _RecursiveFindBD(dsk)
853     if rbd is None:
854       raise errors.BlockDeviceError("Can't find device %s" % str(dsk))
855     stats.append(rbd.CombinedSyncStatus())
856   return stats
857
858
859 def _RecursiveFindBD(disk, allow_partial=False):
860   """Check if a device is activated.
861
862   If so, return informations about the real device.
863
864   Args:
865     disk: the objects.Disk instance
866     allow_partial: don't abort the find if a child of the
867                    device can't be found; this is intended to be
868                    used when repairing mirrors
869
870   Returns:
871     None if the device can't be found
872     otherwise the device instance
873
874   """
875   children = []
876   if disk.children:
877     for chdisk in disk.children:
878       children.append(_RecursiveFindBD(chdisk))
879
880   return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
881
882
883 def FindBlockDevice(disk):
884   """Check if a device is activated.
885
886   If so, return informations about the real device.
887
888   Args:
889     disk: the objects.Disk instance
890   Returns:
891     None if the device can't be found
892     (device_path, major, minor, sync_percent, estimated_time, is_degraded)
893
894   """
895   rbd = _RecursiveFindBD(disk)
896   if rbd is None:
897     return rbd
898   sync_p, est_t, is_degr = rbd.GetSyncStatus()
899   return rbd.dev_path, rbd.major, rbd.minor, sync_p, est_t, is_degr
900
901
902 def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
903   """Write a file to the filesystem.
904
905   This allows the master to overwrite(!) a file. It will only perform
906   the operation if the file belongs to a list of configuration files.
907
908   """
909   if not os.path.isabs(file_name):
910     logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
911                  file_name)
912     return False
913
914   allowed_files = [constants.CLUSTER_CONF_FILE, "/etc/hosts",
915                    constants.SSH_KNOWN_HOSTS_FILE]
916   allowed_files.extend(ssconf.SimpleStore().GetFileList())
917   if file_name not in allowed_files:
918     logger.Error("Filename passed to UploadFile not in allowed"
919                  " upload targets: '%s'" % file_name)
920     return False
921
922   dir_name, small_name = os.path.split(file_name)
923   fd, new_name = tempfile.mkstemp('.new', small_name, dir_name)
924   # here we need to make sure we remove the temp file, if any error
925   # leaves it in place
926   try:
927     os.chown(new_name, uid, gid)
928     os.chmod(new_name, mode)
929     os.write(fd, data)
930     os.fsync(fd)
931     os.utime(new_name, (atime, mtime))
932     os.rename(new_name, file_name)
933   finally:
934     os.close(fd)
935     utils.RemoveFile(new_name)
936   return True
937
938
939 def _ErrnoOrStr(err):
940   """Format an EnvironmentError exception.
941
942   If the `err` argument has an errno attribute, it will be looked up
943   and converted into a textual EXXXX description. Otherwise the string
944   representation of the error will be returned.
945
946   """
947   if hasattr(err, 'errno'):
948     detail = errno.errorcode[err.errno]
949   else:
950     detail = str(err)
951   return detail
952
953
954 def _OSSearch(name, search_path=None):
955   """Search for OSes with the given name in the search_path.
956
957   Args:
958     name: The name of the OS to look for
959     search_path: List of dirs to search (defaults to constants.OS_SEARCH_PATH)
960
961   Returns:
962     The base_dir the OS resides in
963
964   """
965   if search_path is None:
966     search_path = constants.OS_SEARCH_PATH
967
968   for dir_name in search_path:
969     t_os_dir = os.path.sep.join([dir_name, name])
970     if os.path.isdir(t_os_dir):
971       return dir_name
972
973   return None
974
975
976 def _OSOndiskVersion(name, os_dir):
977   """Compute and return the API version of a given OS.
978
979   This function will try to read the API version of the os given by
980   the 'name' parameter and residing in the 'os_dir' directory.
981
982   Return value will be either an integer denoting the version or None in the
983   case when this is not a valid OS name.
984
985   """
986   api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
987
988   try:
989     st = os.stat(api_file)
990   except EnvironmentError, err:
991     raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
992                            " found (%s)" % _ErrnoOrStr(err))
993
994   if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
995     raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
996                            " a regular file")
997
998   try:
999     f = open(api_file)
1000     try:
1001       api_version = f.read(256)
1002     finally:
1003       f.close()
1004   except EnvironmentError, err:
1005     raise errors.InvalidOS(name, os_dir, "error while reading the"
1006                            " API version (%s)" % _ErrnoOrStr(err))
1007
1008   api_version = api_version.strip()
1009   try:
1010     api_version = int(api_version)
1011   except (TypeError, ValueError), err:
1012     raise errors.InvalidOS(name, os_dir,
1013                            "API version is not integer (%s)" % str(err))
1014
1015   return api_version
1016
1017
1018 def DiagnoseOS(top_dirs=None):
1019   """Compute the validity for all OSes.
1020
1021   Returns an OS object for each name in all the given top directories
1022   (if not given defaults to constants.OS_SEARCH_PATH)
1023
1024   Returns:
1025     list of OS objects
1026
1027   """
1028   if top_dirs is None:
1029     top_dirs = constants.OS_SEARCH_PATH
1030
1031   result = []
1032   for dir_name in top_dirs:
1033     if os.path.isdir(dir_name):
1034       try:
1035         f_names = utils.ListVisibleFiles(dir_name)
1036       except EnvironmentError, err:
1037         logger.Error("Can't list the OS directory %s: %s" %
1038                      (dir_name, str(err)))
1039         break
1040       for name in f_names:
1041         try:
1042           os_inst = OSFromDisk(name, base_dir=dir_name)
1043           result.append(os_inst)
1044         except errors.InvalidOS, err:
1045           result.append(objects.OS.FromInvalidOS(err))
1046
1047   return result
1048
1049
1050 def OSFromDisk(name, base_dir=None):
1051   """Create an OS instance from disk.
1052
1053   This function will return an OS instance if the given name is a
1054   valid OS name. Otherwise, it will raise an appropriate
1055   `errors.InvalidOS` exception, detailing why this is not a valid
1056   OS.
1057
1058   Args:
1059     os_dir: Directory containing the OS scripts. Defaults to a search
1060             in all the OS_SEARCH_PATH directories.
1061
1062   """
1063
1064   if base_dir is None:
1065     base_dir = _OSSearch(name)
1066
1067   if base_dir is None:
1068     raise errors.InvalidOS(name, None, "OS dir not found in search path")
1069
1070   os_dir = os.path.sep.join([base_dir, name])
1071   api_version = _OSOndiskVersion(name, os_dir)
1072
1073   if api_version != constants.OS_API_VERSION:
1074     raise errors.InvalidOS(name, os_dir, "API version mismatch"
1075                            " (found %s want %s)"
1076                            % (api_version, constants.OS_API_VERSION))
1077
1078   # OS Scripts dictionary, we will populate it with the actual script names
1079   os_scripts = {'create': '', 'export': '', 'import': '', 'rename': ''}
1080
1081   for script in os_scripts:
1082     os_scripts[script] = os.path.sep.join([os_dir, script])
1083
1084     try:
1085       st = os.stat(os_scripts[script])
1086     except EnvironmentError, err:
1087       raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1088                              (script, _ErrnoOrStr(err)))
1089
1090     if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1091       raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1092                              script)
1093
1094     if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1095       raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1096                              script)
1097
1098
1099   return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1100                     create_script=os_scripts['create'],
1101                     export_script=os_scripts['export'],
1102                     import_script=os_scripts['import'],
1103                     rename_script=os_scripts['rename'],
1104                     api_version=api_version)
1105
1106
1107 def SnapshotBlockDevice(disk):
1108   """Create a snapshot copy of a block device.
1109
1110   This function is called recursively, and the snapshot is actually created
1111   just for the leaf lvm backend device.
1112
1113   Args:
1114     disk: the disk to be snapshotted
1115
1116   Returns:
1117     a config entry for the actual lvm device snapshotted.
1118
1119   """
1120   if disk.children:
1121     if len(disk.children) == 1:
1122       # only one child, let's recurse on it
1123       return SnapshotBlockDevice(disk.children[0])
1124     else:
1125       # more than one child, choose one that matches
1126       for child in disk.children:
1127         if child.size == disk.size:
1128           # return implies breaking the loop
1129           return SnapshotBlockDevice(child)
1130   elif disk.dev_type == constants.LD_LV:
1131     r_dev = _RecursiveFindBD(disk)
1132     if r_dev is not None:
1133       # let's stay on the safe side and ask for the full size, for now
1134       return r_dev.Snapshot(disk.size)
1135     else:
1136       return None
1137   else:
1138     raise errors.ProgrammerError("Cannot snapshot non-lvm block device"
1139                                  "'%s' of type '%s'" %
1140                                  (disk.unique_id, disk.dev_type))
1141
1142
1143 def ExportSnapshot(disk, dest_node, instance):
1144   """Export a block device snapshot to a remote node.
1145
1146   Args:
1147     disk: the snapshot block device
1148     dest_node: the node to send the image to
1149     instance: instance being exported
1150
1151   Returns:
1152     True if successful, False otherwise.
1153
1154   """
1155   inst_os = OSFromDisk(instance.os)
1156   export_script = inst_os.export_script
1157
1158   logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1159                                      instance.name, int(time.time()))
1160   if not os.path.exists(constants.LOG_OS_DIR):
1161     os.mkdir(constants.LOG_OS_DIR, 0750)
1162
1163   real_os_dev = _RecursiveFindBD(disk)
1164   if real_os_dev is None:
1165     raise errors.BlockDeviceError("Block device '%s' is not set up" %
1166                                   str(disk))
1167   real_os_dev.Open()
1168
1169   destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1170   destfile = disk.physical_id[1]
1171
1172   # the target command is built out of three individual commands,
1173   # which are joined by pipes; we check each individual command for
1174   # valid parameters
1175
1176   expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1177                                export_script, instance.name,
1178                                real_os_dev.dev_path, logfile)
1179
1180   comprcmd = "gzip"
1181
1182   destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1183                                 destdir, destdir, destfile)
1184   remotecmd = ssh.BuildSSHCmd(dest_node, constants.GANETI_RUNAS, destcmd)
1185
1186
1187
1188   # all commands have been checked, so we're safe to combine them
1189   command = '|'.join([expcmd, comprcmd, utils.ShellQuoteArgs(remotecmd)])
1190
1191   result = utils.RunCmd(command)
1192
1193   if result.failed:
1194     logger.Error("os snapshot export command '%s' returned error: %s"
1195                  " output: %s" %
1196                  (command, result.fail_reason, result.output))
1197     return False
1198
1199   return True
1200
1201
1202 def FinalizeExport(instance, snap_disks):
1203   """Write out the export configuration information.
1204
1205   Args:
1206     instance: instance configuration
1207     snap_disks: snapshot block devices
1208
1209   Returns:
1210     False in case of error, True otherwise.
1211
1212   """
1213   destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1214   finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1215
1216   config = objects.SerializableConfigParser()
1217
1218   config.add_section(constants.INISECT_EXP)
1219   config.set(constants.INISECT_EXP, 'version', '0')
1220   config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1221   config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1222   config.set(constants.INISECT_EXP, 'os', instance.os)
1223   config.set(constants.INISECT_EXP, 'compression', 'gzip')
1224
1225   config.add_section(constants.INISECT_INS)
1226   config.set(constants.INISECT_INS, 'name', instance.name)
1227   config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1228   config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1229   config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1230   for nic_count, nic in enumerate(instance.nics):
1231     config.set(constants.INISECT_INS, 'nic%d_mac' %
1232                nic_count, '%s' % nic.mac)
1233     config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1234   # TODO: redundant: on load can read nics until it doesn't exist
1235   config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1236
1237   for disk_count, disk in enumerate(snap_disks):
1238     config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1239                ('%s' % disk.iv_name))
1240     config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1241                ('%s' % disk.physical_id[1]))
1242     config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1243                ('%d' % disk.size))
1244   config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1245
1246   cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1247   cfo = open(cff, 'w')
1248   try:
1249     config.write(cfo)
1250   finally:
1251     cfo.close()
1252
1253   shutil.rmtree(finaldestdir, True)
1254   shutil.move(destdir, finaldestdir)
1255
1256   return True
1257
1258
1259 def ExportInfo(dest):
1260   """Get export configuration information.
1261
1262   Args:
1263     dest: directory containing the export
1264
1265   Returns:
1266     A serializable config file containing the export info.
1267
1268   """
1269   cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1270
1271   config = objects.SerializableConfigParser()
1272   config.read(cff)
1273
1274   if (not config.has_section(constants.INISECT_EXP) or
1275       not config.has_section(constants.INISECT_INS)):
1276     return None
1277
1278   return config
1279
1280
1281 def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1282   """Import an os image into an instance.
1283
1284   Args:
1285     instance: the instance object
1286     os_disk: the instance-visible name of the os device
1287     swap_disk: the instance-visible name of the swap device
1288     src_node: node holding the source image
1289     src_image: path to the source image on src_node
1290
1291   Returns:
1292     False in case of error, True otherwise.
1293
1294   """
1295   inst_os = OSFromDisk(instance.os)
1296   import_script = inst_os.import_script
1297
1298   os_device = instance.FindDisk(os_disk)
1299   if os_device is None:
1300     logger.Error("Can't find this device-visible name '%s'" % os_disk)
1301     return False
1302
1303   swap_device = instance.FindDisk(swap_disk)
1304   if swap_device is None:
1305     logger.Error("Can't find this device-visible name '%s'" % swap_disk)
1306     return False
1307
1308   real_os_dev = _RecursiveFindBD(os_device)
1309   if real_os_dev is None:
1310     raise errors.BlockDeviceError("Block device '%s' is not set up" %
1311                                   str(os_device))
1312   real_os_dev.Open()
1313
1314   real_swap_dev = _RecursiveFindBD(swap_device)
1315   if real_swap_dev is None:
1316     raise errors.BlockDeviceError("Block device '%s' is not set up" %
1317                                   str(swap_device))
1318   real_swap_dev.Open()
1319
1320   logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1321                                         instance.name, int(time.time()))
1322   if not os.path.exists(constants.LOG_OS_DIR):
1323     os.mkdir(constants.LOG_OS_DIR, 0750)
1324
1325   destcmd = utils.BuildShellCmd('cat %s', src_image)
1326   remotecmd = ssh.BuildSSHCmd(src_node, constants.GANETI_RUNAS, destcmd)
1327
1328   comprcmd = "gunzip"
1329   impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1330                                inst_os.path, import_script, instance.name,
1331                                real_os_dev.dev_path, real_swap_dev.dev_path,
1332                                logfile)
1333
1334   command = '|'.join([utils.ShellQuoteArgs(remotecmd), comprcmd, impcmd])
1335
1336   result = utils.RunCmd(command)
1337
1338   if result.failed:
1339     logger.Error("os import command '%s' returned error: %s"
1340                  " output: %s" %
1341                  (command, result.fail_reason, result.output))
1342     return False
1343
1344   return True
1345
1346
1347 def ListExports():
1348   """Return a list of exports currently available on this machine.
1349
1350   """
1351   if os.path.isdir(constants.EXPORT_DIR):
1352     return utils.ListVisibleFiles(constants.EXPORT_DIR)
1353   else:
1354     return []
1355
1356
1357 def RemoveExport(export):
1358   """Remove an existing export from the node.
1359
1360   Args:
1361     export: the name of the export to remove
1362
1363   Returns:
1364     False in case of error, True otherwise.
1365
1366   """
1367   target = os.path.join(constants.EXPORT_DIR, export)
1368
1369   shutil.rmtree(target)
1370   # TODO: catch some of the relevant exceptions and provide a pretty
1371   # error message if rmtree fails.
1372
1373   return True
1374
1375
1376 def RenameBlockDevices(devlist):
1377   """Rename a list of block devices.
1378
1379   The devlist argument is a list of tuples (disk, new_logical,
1380   new_physical). The return value will be a combined boolean result
1381   (True only if all renames succeeded).
1382
1383   """
1384   result = True
1385   for disk, unique_id in devlist:
1386     dev = _RecursiveFindBD(disk)
1387     if dev is None:
1388       result = False
1389       continue
1390     try:
1391       old_rpath = dev.dev_path
1392       dev.Rename(unique_id)
1393       new_rpath = dev.dev_path
1394       if old_rpath != new_rpath:
1395         DevCacheManager.RemoveCache(old_rpath)
1396         # FIXME: we should add the new cache information here, like:
1397         # DevCacheManager.UpdateCache(new_rpath, owner, ...)
1398         # but we don't have the owner here - maybe parse from existing
1399         # cache? for now, we only lose lvm data when we rename, which
1400         # is less critical than DRBD or MD
1401     except errors.BlockDeviceError, err:
1402       logger.Error("Can't rename device '%s' to '%s': %s" %
1403                    (dev, unique_id, err))
1404       result = False
1405   return result
1406
1407
1408 class HooksRunner(object):
1409   """Hook runner.
1410
1411   This class is instantiated on the node side (ganeti-noded) and not on
1412   the master side.
1413
1414   """
1415   RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1416
1417   def __init__(self, hooks_base_dir=None):
1418     """Constructor for hooks runner.
1419
1420     Args:
1421       - hooks_base_dir: if not None, this overrides the
1422         constants.HOOKS_BASE_DIR (useful for unittests)
1423       - logs_base_dir: if not None, this overrides the
1424         constants.LOG_HOOKS_DIR (useful for unittests)
1425       - logging: enable or disable logging of script output
1426
1427     """
1428     if hooks_base_dir is None:
1429       hooks_base_dir = constants.HOOKS_BASE_DIR
1430     self._BASE_DIR = hooks_base_dir
1431
1432   @staticmethod
1433   def ExecHook(script, env):
1434     """Exec one hook script.
1435
1436     Args:
1437      - phase: the phase
1438      - script: the full path to the script
1439      - env: the environment with which to exec the script
1440
1441     """
1442     # exec the process using subprocess and log the output
1443     fdstdin = None
1444     try:
1445       fdstdin = open("/dev/null", "r")
1446       child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1447                                stderr=subprocess.STDOUT, close_fds=True,
1448                                shell=False, cwd="/",env=env)
1449       output = ""
1450       try:
1451         output = child.stdout.read(4096)
1452         child.stdout.close()
1453       except EnvironmentError, err:
1454         output += "Hook script error: %s" % str(err)
1455
1456       while True:
1457         try:
1458           result = child.wait()
1459           break
1460         except EnvironmentError, err:
1461           if err.errno == errno.EINTR:
1462             continue
1463           raise
1464     finally:
1465       # try not to leak fds
1466       for fd in (fdstdin, ):
1467         if fd is not None:
1468           try:
1469             fd.close()
1470           except EnvironmentError, err:
1471             # just log the error
1472             #logger.Error("While closing fd %s: %s" % (fd, err))
1473             pass
1474
1475     return result == 0, output
1476
1477   def RunHooks(self, hpath, phase, env):
1478     """Run the scripts in the hooks directory.
1479
1480     This method will not be usually overriden by child opcodes.
1481
1482     """
1483     if phase == constants.HOOKS_PHASE_PRE:
1484       suffix = "pre"
1485     elif phase == constants.HOOKS_PHASE_POST:
1486       suffix = "post"
1487     else:
1488       raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1489     rr = []
1490
1491     subdir = "%s-%s.d" % (hpath, suffix)
1492     dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1493     try:
1494       dir_contents = utils.ListVisibleFiles(dir_name)
1495     except OSError, err:
1496       # must log
1497       return rr
1498
1499     # we use the standard python sort order,
1500     # so 00name is the recommended naming scheme
1501     dir_contents.sort()
1502     for relname in dir_contents:
1503       fname = os.path.join(dir_name, relname)
1504       if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
1505           self.RE_MASK.match(relname) is not None):
1506         rrval = constants.HKR_SKIP
1507         output = ""
1508       else:
1509         result, output = self.ExecHook(fname, env)
1510         if not result:
1511           rrval = constants.HKR_FAIL
1512         else:
1513           rrval = constants.HKR_SUCCESS
1514       rr.append(("%s/%s" % (subdir, relname), rrval, output))
1515
1516     return rr
1517
1518
1519 class DevCacheManager(object):
1520   """Simple class for managing a chache of block device information.
1521
1522   """
1523   _DEV_PREFIX = "/dev/"
1524   _ROOT_DIR = constants.BDEV_CACHE_DIR
1525
1526   @classmethod
1527   def _ConvertPath(cls, dev_path):
1528     """Converts a /dev/name path to the cache file name.
1529
1530     This replaces slashes with underscores and strips the /dev
1531     prefix. It then returns the full path to the cache file
1532
1533     """
1534     if dev_path.startswith(cls._DEV_PREFIX):
1535       dev_path = dev_path[len(cls._DEV_PREFIX):]
1536     dev_path = dev_path.replace("/", "_")
1537     fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1538     return fpath
1539
1540   @classmethod
1541   def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1542     """Updates the cache information for a given device.
1543
1544     """
1545     if dev_path is None:
1546       logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1547       return
1548     fpath = cls._ConvertPath(dev_path)
1549     if on_primary:
1550       state = "primary"
1551     else:
1552       state = "secondary"
1553     if iv_name is None:
1554       iv_name = "not_visible"
1555     fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1556     try:
1557       utils.WriteFile(fpath, data=fdata)
1558     except EnvironmentError, err:
1559       logger.Error("Can't update bdev cache for %s, error %s" %
1560                    (dev_path, str(err)))
1561
1562   @classmethod
1563   def RemoveCache(cls, dev_path):
1564     """Remove data for a dev_path.
1565
1566     """
1567     if dev_path is None:
1568       logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1569       return
1570     fpath = cls._ConvertPath(dev_path)
1571     try:
1572       utils.RemoveFile(fpath)
1573     except EnvironmentError, err:
1574       logger.Error("Can't update bdev cache for %s, error %s" %
1575                    (dev_path, str(err)))