Comment formatting updates.
[ganeti-local] / lib / bdev.py
1 #!/usr/bin/python
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 """Block device abstraction"""
23
24 import re
25 import time
26 import errno
27
28 from ganeti import utils
29 from ganeti import logger
30 from ganeti import errors
31
32
33 class BlockDev(object):
34   """Block device abstract class.
35
36   A block device can be in the following states:
37     - not existing on the system, and by `Create()` it goes into:
38     - existing but not setup/not active, and by `Assemble()` goes into:
39     - active read-write and by `Open()` it goes into
40     - online (=used, or ready for use)
41
42   A device can also be online but read-only, however we are not using
43   the readonly state (MD and LV have it, if needed in the future)
44   and we are usually looking at this like at a stack, so it's easier
45   to conceptualise the transition from not-existing to online and back
46   like a linear one.
47
48   The many different states of the device are due to the fact that we
49   need to cover many device types:
50     - logical volumes are created, lvchange -a y $lv, and used
51     - md arrays are created or assembled and used
52     - drbd devices are attached to a local disk/remote peer and made primary
53
54   The status of the device can be examined by `GetStatus()`, which
55   returns a numerical value, depending on the position in the
56   transition stack of the device.
57
58   A block device is identified by three items:
59     - the /dev path of the device (dynamic)
60     - a unique ID of the device (static)
61     - it's major/minor pair (dynamic)
62
63   Not all devices implement both the first two as distinct items. LVM
64   logical volumes have their unique ID (the pair volume group, logical
65   volume name) in a 1-to-1 relation to the dev path. For MD devices,
66   the /dev path is dynamic and the unique ID is the UUID generated at
67   array creation plus the slave list. For DRBD devices, the /dev path
68   is again dynamic and the unique id is the pair (host1, dev1),
69   (host2, dev2).
70
71   You can get to a device in two ways:
72     - creating the (real) device, which returns you
73       an attached instance (lvcreate, mdadm --create)
74     - attaching of a python instance to an existing (real) device
75
76   The second point, the attachement to a device, is different
77   depending on whether the device is assembled or not. At init() time,
78   we search for a device with the same unique_id as us. If found,
79   good. It also means that the device is already assembled. If not,
80   after assembly we'll have our correct major/minor.
81
82   """
83   STATUS_UNKNOWN = 0
84   STATUS_EXISTING = 1
85   STATUS_STANDBY = 2
86   STATUS_ONLINE = 3
87
88   STATUS_MAP = {
89     STATUS_UNKNOWN: "unknown",
90     STATUS_EXISTING: "existing",
91     STATUS_STANDBY: "ready for use",
92     STATUS_ONLINE: "online",
93     }
94
95
96   def __init__(self, unique_id, children):
97     self._children = children
98     self.dev_path = None
99     self.unique_id = unique_id
100     self.major = None
101     self.minor = None
102
103
104   def Assemble(self):
105     """Assemble the device from its components.
106
107     If this is a plain block device (e.g. LVM) than assemble does
108     nothing, as the LVM has no children and we don't put logical
109     volumes offline.
110
111     One guarantee is that after the device has been assembled, it
112     knows its major/minor numbers. This allows other devices (usually
113     parents) to probe correctly for their children.
114
115     """
116     status = True
117     for child in self._children:
118       if not isinstance(child, BlockDev):
119         raise TypeError("Invalid child passed of type '%s'" % type(child))
120       if not status:
121         break
122       status = status and child.Assemble()
123       if not status:
124         break
125       status = status and child.Open()
126
127     if not status:
128       for child in self._children:
129         child.Shutdown()
130     return status
131
132
133   def Attach(self):
134     """Find a device which matches our config and attach to it.
135
136     """
137     raise NotImplementedError
138
139
140   def Close(self):
141     """Notifies that the device will no longer be used for I/O.
142
143     """
144     raise NotImplementedError
145
146
147   @classmethod
148   def Create(cls, unique_id, children, size):
149     """Create the device.
150
151     If the device cannot be created, it will return None
152     instead. Error messages go to the logging system.
153
154     Note that for some devices, the unique_id is used, and for other,
155     the children. The idea is that these two, taken together, are
156     enough for both creation and assembly (later).
157
158     """
159     raise NotImplementedError
160
161
162   def Remove(self):
163     """Remove this device.
164
165     This makes sense only for some of the device types: LV and to a
166     lesser degree, md devices. Also note that if the device can't
167     attach, the removal can't be completed.
168
169     """
170     raise NotImplementedError
171
172
173   def GetStatus(self):
174     """Return the status of the device.
175
176     """
177     raise NotImplementedError
178
179
180   def Open(self, force=False):
181     """Make the device ready for use.
182
183     This makes the device ready for I/O. For now, just the DRBD
184     devices need this.
185
186     The force parameter signifies that if the device has any kind of
187     --force thing, it should be used, we know what we are doing.
188
189     """
190     raise NotImplementedError
191
192
193   def Shutdown(self):
194     """Shut down the device, freeing its children.
195
196     This undoes the `Assemble()` work, except for the child
197     assembling; as such, the children on the device are still
198     assembled after this call.
199
200     """
201     raise NotImplementedError
202
203
204   def SetSyncSpeed(self, speed):
205     """Adjust the sync speed of the mirror.
206
207     In case this is not a mirroring device, this is no-op.
208
209     """
210     result = True
211     if self._children:
212       for child in self._children:
213         result = result and child.SetSyncSpeed(speed)
214     return result
215
216
217   def GetSyncStatus(self):
218     """Returns the sync status of the device.
219
220     If this device is a mirroring device, this function returns the
221     status of the mirror.
222
223     Returns:
224      (sync_percent, estimated_time, is_degraded)
225
226     If sync_percent is None, it means all is ok
227     If estimated_time is None, it means we can't estimate
228     the time needed, otherwise it's the time left in seconds
229     If is_degraded is True, it means the device is missing
230     redundancy. This is usually a sign that something went wrong in
231     the device setup, if sync_percent is None.
232
233     """
234     return None, None, False
235
236
237   def CombinedSyncStatus(self):
238     """Calculate the mirror status recursively for our children.
239
240     The return value is the same as for `GetSyncStatus()` except the
241     minimum percent and maximum time are calculated across our
242     children.
243
244     """
245     min_percent, max_time, is_degraded = self.GetSyncStatus()
246     if self._children:
247       for child in self._children:
248         c_percent, c_time, c_degraded = child.GetSyncStatus()
249         if min_percent is None:
250           min_percent = c_percent
251         elif c_percent is not None:
252           min_percent = min(min_percent, c_percent)
253         if max_time is None:
254           max_time = c_time
255         elif c_time is not None:
256           max_time = max(max_time, c_time)
257         is_degraded = is_degraded or c_degraded
258     return min_percent, max_time, is_degraded
259
260
261   def __repr__(self):
262     return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
263             (self.__class__, self.unique_id, self._children,
264              self.major, self.minor, self.dev_path))
265
266
267 class LogicalVolume(BlockDev):
268   """Logical Volume block device.
269
270   """
271   def __init__(self, unique_id, children):
272     """Attaches to a LV device.
273
274     The unique_id is a tuple (vg_name, lv_name)
275
276     """
277     super(LogicalVolume, self).__init__(unique_id, children)
278     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
279       raise ValueError("Invalid configuration data %s" % str(unique_id))
280     self._vg_name, self._lv_name = unique_id
281     self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
282     self.Attach()
283
284
285   @classmethod
286   def Create(cls, unique_id, children, size):
287     """Create a new logical volume.
288
289     """
290     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
291       raise ValueError("Invalid configuration data %s" % str(unique_id))
292     vg_name, lv_name = unique_id
293     pvs_info = cls.GetPVInfo(vg_name)
294     if not pvs_info:
295       raise errors.BlockDeviceError, ("Can't compute PV info for vg %s" %
296                                       vg_name)
297     pvs_info.sort()
298     pvs_info.reverse()
299
300     pvlist = [ pv[1] for pv in pvs_info ]
301     free_size = sum([ pv[0] for pv in pvs_info ])
302
303     # The size constraint should have been checked from the master before
304     # calling the create function.
305     if free_size < size:
306       raise errors.BlockDeviceError, ("Not enough free space: required %s,"
307                                       " available %s" % (size, free_size))
308     result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
309                            vg_name] + pvlist)
310     if result.failed:
311       raise errors.BlockDeviceError(result.fail_reason)
312     return LogicalVolume(unique_id, children)
313
314   @staticmethod
315   def GetPVInfo(vg_name):
316     """Get the free space info for PVs in a volume group.
317
318     Args:
319       vg_name: the volume group name
320
321     Returns:
322       list of (free_space, name) with free_space in mebibytes
323
324     """
325     command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
326                "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
327                "--separator=:"]
328     result = utils.RunCmd(command)
329     if result.failed:
330       logger.Error("Can't get the PV information: %s" % result.fail_reason)
331       return None
332     data = []
333     for line in result.stdout.splitlines():
334       fields = line.strip().split(':')
335       if len(fields) != 4:
336         logger.Error("Can't parse pvs output: line '%s'" % line)
337         return None
338       # skip over pvs from another vg or ones which are not allocatable
339       if fields[1] != vg_name or fields[3][0] != 'a':
340         continue
341       data.append((float(fields[2]), fields[0]))
342
343     return data
344
345   def Remove(self):
346     """Remove this logical volume.
347
348     """
349     if not self.minor and not self.Attach():
350       # the LV does not exist
351       return True
352     result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
353                            (self._vg_name, self._lv_name)])
354     if result.failed:
355       logger.Error("Can't lvremove: %s" % result.fail_reason)
356
357     return not result.failed
358
359
360   def Attach(self):
361     """Attach to an existing LV.
362
363     This method will try to see if an existing and active LV exists
364     which matches the our name. If so, its major/minor will be
365     recorded.
366
367     """
368     result = utils.RunCmd(["lvdisplay", self.dev_path])
369     if result.failed:
370       logger.Error("Can't find LV %s: %s" %
371                    (self.dev_path, result.fail_reason))
372       return False
373     match = re.compile("^ *Block device *([0-9]+):([0-9]+).*$")
374     for line in result.stdout.splitlines():
375       match_result = match.match(line)
376       if match_result:
377         self.major = int(match_result.group(1))
378         self.minor = int(match_result.group(2))
379         return True
380     return False
381
382
383   def Assemble(self):
384     """Assemble the device.
385
386     This is a no-op for the LV device type. Eventually, we could
387     lvchange -ay here if we see that the LV is not active.
388
389     """
390     return True
391
392
393   def Shutdown(self):
394     """Shutdown the device.
395
396     This is a no-op for the LV device type, as we don't deactivate the
397     volumes on shutdown.
398
399     """
400     return True
401
402
403   def GetStatus(self):
404     """Return the status of the device.
405
406     Logical volumes will can be in all four states, although we don't
407     deactivate (lvchange -an) them when shutdown, so STATUS_EXISTING
408     should not be seen for our devices.
409
410     """
411     result = utils.RunCmd(["lvs", "--noheadings", "-olv_attr", self.dev_path])
412     if result.failed:
413       logger.Error("Can't display lv: %s" % result.fail_reason)
414       return self.STATUS_UNKNOWN
415     out = result.stdout.strip()
416     # format: type/permissions/alloc/fixed_minor/state/open
417     if len(out) != 6:
418       return self.STATUS_UNKNOWN
419     #writable = (out[1] == "w")
420     active = (out[4] == "a")
421     online = (out[5] == "o")
422     if online:
423       retval = self.STATUS_ONLINE
424     elif active:
425       retval = self.STATUS_STANDBY
426     else:
427       retval = self.STATUS_EXISTING
428
429     return retval
430
431
432   def Open(self, force=False):
433     """Make the device ready for I/O.
434
435     This is a no-op for the LV device type.
436
437     """
438     return True
439
440
441   def Close(self):
442     """Notifies that the device will no longer be used for I/O.
443
444     This is a no-op for the LV device type.
445
446     """
447     return True
448
449
450   def Snapshot(self, size):
451     """Create a snapshot copy of an lvm block device.
452
453     """
454     snap_name = self._lv_name + ".snap"
455
456     # remove existing snapshot if found
457     snap = LogicalVolume((self._vg_name, snap_name), None)
458     snap.Remove()
459
460     pvs_info = self.GetPVInfo(self._vg_name)
461     if not pvs_info:
462       raise errors.BlockDeviceError, ("Can't compute PV info for vg %s" %
463                                       self._vg_name)
464     pvs_info.sort()
465     pvs_info.reverse()
466     free_size, pv_name = pvs_info[0]
467     if free_size < size:
468       raise errors.BlockDeviceError, ("Not enough free space: required %s,"
469                                       " available %s" % (size, free_size))
470
471     result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
472                            "-n%s" % snap_name, self.dev_path])
473     if result.failed:
474       raise errors.BlockDeviceError, ("command: %s error: %s" %
475                                       (result.cmd, result.fail_reason))
476
477     return snap_name
478
479
480 class MDRaid1(BlockDev):
481   """raid1 device implemented via md.
482
483   """
484   def __init__(self, unique_id, children):
485     super(MDRaid1, self).__init__(unique_id, children)
486     self.major = 9
487     self.Attach()
488
489
490   def Attach(self):
491     """Find an array which matches our config and attach to it.
492
493     This tries to find a MD array which has the same UUID as our own.
494
495     """
496     minor = self._FindMDByUUID(self.unique_id)
497     if minor is not None:
498       self._SetFromMinor(minor)
499     else:
500       self.minor = None
501       self.dev_path = None
502
503     return (minor is not None)
504
505
506   @staticmethod
507   def _GetUsedDevs():
508     """Compute the list of in-use MD devices.
509
510     It doesn't matter if the used device have other raid level, just
511     that they are in use.
512
513     """
514     mdstat = open("/proc/mdstat", "r")
515     data = mdstat.readlines()
516     mdstat.close()
517
518     used_md = {}
519     valid_line = re.compile("^md([0-9]+) : .*$")
520     for line in data:
521       match = valid_line.match(line)
522       if match:
523         md_no = int(match.group(1))
524         used_md[md_no] = line
525
526     return used_md
527
528
529   @staticmethod
530   def _GetDevInfo(minor):
531     """Get info about a MD device.
532
533     Currently only uuid is returned.
534
535     """
536     result = utils.RunCmd(["mdadm", "-D", "/dev/md%d" % minor])
537     if result.failed:
538       logger.Error("Can't display md: %s" % result.fail_reason)
539       return None
540     retval = {}
541     for line in result.stdout.splitlines():
542       line = line.strip()
543       kv = line.split(" : ", 1)
544       if kv:
545         if kv[0] == "UUID":
546           retval["uuid"] = kv[1]
547         elif kv[0] == "State":
548           retval["state"] = kv[1].split(", ")
549     return retval
550
551
552   @staticmethod
553   def _FindUnusedMinor():
554     """Compute an unused MD minor.
555
556     This code assumes that there are 256 minors only.
557
558     """
559     used_md = MDRaid1._GetUsedDevs()
560     i = 0
561     while i < 256:
562       if i not in used_md:
563         break
564       i += 1
565     if i == 256:
566       logger.Error("Critical: Out of md minor numbers.")
567       return None
568     return i
569
570
571   @classmethod
572   def _FindMDByUUID(cls, uuid):
573     """Find the minor of an MD array with a given UUID.
574
575     """
576     md_list = cls._GetUsedDevs()
577     for minor in md_list:
578       info = cls._GetDevInfo(minor)
579       if info and info["uuid"] == uuid:
580         return minor
581     return None
582
583
584   @classmethod
585   def Create(cls, unique_id, children, size):
586     """Create a new MD raid1 array.
587
588     """
589     if not isinstance(children, (tuple, list)):
590       raise ValueError("Invalid setup data for MDRaid1 dev: %s" %
591                        str(children))
592     for i in children:
593       if not isinstance(i, BlockDev):
594         raise ValueError("Invalid member in MDRaid1 dev: %s" % type(i))
595     for i in children:
596       result = utils.RunCmd(["mdadm", "--zero-superblock", "--force",
597                              i.dev_path])
598       if result.failed:
599         logger.Error("Can't zero superblock: %s" % result.fail_reason)
600         return None
601     minor = cls._FindUnusedMinor()
602     result = utils.RunCmd(["mdadm", "--create", "/dev/md%d" % minor,
603                            "--auto=yes", "--force", "-l1",
604                            "-n%d" % len(children)] +
605                           [dev.dev_path for dev in children])
606
607     if result.failed:
608       logger.Error("Can't create md: %s" % result.fail_reason)
609       return None
610     info = cls._GetDevInfo(minor)
611     if not info or not "uuid" in info:
612       logger.Error("Wrong information returned from mdadm -D: %s" % str(info))
613       return None
614     return MDRaid1(info["uuid"], children)
615
616
617   def Remove(self):
618     """Stub remove function for MD RAID 1 arrays.
619
620     We don't remove the superblock right now. Mark a to do.
621
622     """
623     #TODO: maybe zero superblock on child devices?
624     return self.Shutdown()
625
626
627   def AddChild(self, device):
628     """Add a new member to the md raid1.
629
630     """
631     if self.minor is None and not self.Attach():
632       raise errors.BlockDeviceError, "Can't attach to device"
633     if device.dev_path is None:
634       raise errors.BlockDeviceError, "New child is not initialised"
635     result = utils.RunCmd(["mdadm", "-a", self.dev_path, device.dev_path])
636     if result.failed:
637       raise errors.BlockDeviceError, ("Failed to add new device to array: %s" %
638                                       result.output)
639     new_len = len(self._children) + 1
640     result = utils.RunCmd(["mdadm", "--grow", self.dev_path, "-n", new_len])
641     if result.failed:
642       raise errors.BlockDeviceError, ("Can't grow md array: %s" %
643                                       result.output)
644     self._children.append(device)
645
646
647   def RemoveChild(self, dev_path):
648     """Remove member from the md raid1.
649
650     """
651     if self.minor is None and not self.Attach():
652       raise errors.BlockDeviceError, "Can't attach to device"
653     if len(self._children) == 1:
654       raise errors.BlockDeviceError, ("Can't reduce member when only one"
655                                       " child left")
656     for device in self._children:
657       if device.dev_path == dev_path:
658         break
659     else:
660       raise errors.BlockDeviceError, "Can't find child with this path"
661     new_len = len(self._children) - 1
662     result = utils.RunCmd(["mdadm", "-f", self.dev_path, dev_path])
663     if result.failed:
664       raise errors.BlockDeviceError, ("Failed to mark device as failed: %s" %
665                                       result.output)
666
667     # it seems here we need a short delay for MD to update its
668     # superblocks
669     time.sleep(0.5)
670     result = utils.RunCmd(["mdadm", "-r", self.dev_path, dev_path])
671     if result.failed:
672       raise errors.BlockDeviceError, ("Failed to remove device from array:"
673                                       " %s" % result.output)
674     result = utils.RunCmd(["mdadm", "--grow", "--force", self.dev_path,
675                            "-n", new_len])
676     if result.failed:
677       raise errors.BlockDeviceError, ("Can't shrink md array: %s" %
678                                       result.output)
679     self._children.remove(device)
680
681
682   def GetStatus(self):
683     """Return the status of the device.
684
685     """
686     self.Attach()
687     if self.minor is None:
688       retval = self.STATUS_UNKNOWN
689     else:
690       retval = self.STATUS_ONLINE
691     return retval
692
693
694   def _SetFromMinor(self, minor):
695     """Set our parameters based on the given minor.
696
697     This sets our minor variable and our dev_path.
698
699     """
700     self.minor = minor
701     self.dev_path = "/dev/md%d" % minor
702
703
704   def Assemble(self):
705     """Assemble the MD device.
706
707     At this point we should have:
708       - list of children devices
709       - uuid
710
711     """
712     result = super(MDRaid1, self).Assemble()
713     if not result:
714       return result
715     md_list = self._GetUsedDevs()
716     for minor in md_list:
717       info = self._GetDevInfo(minor)
718       if info and info["uuid"] == self.unique_id:
719         self._SetFromMinor(minor)
720         logger.Info("MD array %s already started" % str(self))
721         return True
722     free_minor = self._FindUnusedMinor()
723     result = utils.RunCmd(["mdadm", "-A", "--auto=yes", "--uuid",
724                            self.unique_id, "/dev/md%d" % free_minor] +
725                           [bdev.dev_path for bdev in self._children])
726     if result.failed:
727       logger.Error("Can't assemble MD array: %s" % result.fail_reason)
728       self.minor = None
729     else:
730       self.minor = free_minor
731     return not result.failed
732
733
734   def Shutdown(self):
735     """Tear down the MD array.
736
737     This does a 'mdadm --stop' so after this command, the array is no
738     longer available.
739
740     """
741     if self.minor is None and not self.Attach():
742       logger.Info("MD object not attached to a device")
743       return True
744
745     result = utils.RunCmd(["mdadm", "--stop", "/dev/md%d" % self.minor])
746     if result.failed:
747       logger.Error("Can't stop MD array: %s" % result.fail_reason)
748       return False
749     self.minor = None
750     self.dev_path = None
751     return True
752
753
754   def SetSyncSpeed(self, kbytes):
755     """Set the maximum sync speed for the MD array.
756
757     """
758     result = super(MDRaid1, self).SetSyncSpeed(kbytes)
759     if self.minor is None:
760       logger.Error("MD array not attached to a device")
761       return False
762     f = open("/sys/block/md%d/md/sync_speed_max" % self.minor, "w")
763     try:
764       f.write("%d" % kbytes)
765     finally:
766       f.close()
767     f = open("/sys/block/md%d/md/sync_speed_min" % self.minor, "w")
768     try:
769       f.write("%d" % (kbytes/2))
770     finally:
771       f.close()
772     return result
773
774
775   def GetSyncStatus(self):
776     """Returns the sync status of the device.
777
778     Returns:
779      (sync_percent, estimated_time)
780
781     If sync_percent is None, it means all is ok
782     If estimated_time is None, it means we can't esimate
783     the time needed, otherwise it's the time left in seconds
784
785     """
786     if self.minor is None and not self.Attach():
787       raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
788     dev_info = self._GetDevInfo(self.minor)
789     is_clean = ("state" in dev_info and
790                 len(dev_info["state"]) == 1 and
791                 dev_info["state"][0] in ("clean", "active"))
792     sys_path = "/sys/block/md%s/md/" % self.minor
793     f = file(sys_path + "sync_action")
794     sync_status = f.readline().strip()
795     f.close()
796     if sync_status == "idle":
797       return None, None, not is_clean
798     f = file(sys_path + "sync_completed")
799     sync_completed = f.readline().strip().split(" / ")
800     f.close()
801     if len(sync_completed) != 2:
802       return 0, None, not is_clean
803     sync_done, sync_total = [float(i) for i in sync_completed]
804     sync_percent = 100.0*sync_done/sync_total
805     f = file(sys_path + "sync_speed")
806     sync_speed_k = int(f.readline().strip())
807     if sync_speed_k == 0:
808       time_est = None
809     else:
810       time_est = (sync_total - sync_done) / 2 / sync_speed_k
811     return sync_percent, time_est, not is_clean
812
813
814   def Open(self, force=False):
815     """Make the device ready for I/O.
816
817     This is a no-op for the MDRaid1 device type, although we could use
818     the 2.6.18's new array_state thing.
819
820     """
821     return True
822
823
824   def Close(self):
825     """Notifies that the device will no longer be used for I/O.
826
827     This is a no-op for the MDRaid1 device type, but see comment for
828     `Open()`.
829
830     """
831     return True
832
833
834
835 class DRBDev(BlockDev):
836   """DRBD block device.
837
838   This implements the local host part of the DRBD device, i.e. it
839   doesn't do anything to the supposed peer. If you need a fully
840   connected DRBD pair, you need to use this class on both hosts.
841
842   The unique_id for the drbd device is the (local_ip, local_port,
843   remote_ip, remote_port) tuple, and it must have two children: the
844   data device and the meta_device. The meta device is checked for
845   valid size and is zeroed on create.
846
847   """
848   _DRBD_MAJOR = 147
849   _ST_UNCONFIGURED = "Unconfigured"
850   _ST_WFCONNECTION = "WFConnection"
851   _ST_CONNECTED = "Connected"
852
853   def __init__(self, unique_id, children):
854     super(DRBDev, self).__init__(unique_id, children)
855     self.major = self._DRBD_MAJOR
856     if len(children) != 2:
857       raise ValueError("Invalid configuration data %s" % str(children))
858     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
859       raise ValueError("Invalid configuration data %s" % str(unique_id))
860     self._lhost, self._lport, self._rhost, self._rport = unique_id
861     self.Attach()
862
863   @staticmethod
864   def _DevPath(minor):
865     """Return the path to a drbd device for a given minor.
866
867     """
868     return "/dev/drbd%d" % minor
869
870   @staticmethod
871   def _GetProcData():
872     """Return data from /proc/drbd.
873
874     """
875     stat = open("/proc/drbd", "r")
876     data = stat.read().splitlines()
877     stat.close()
878     return data
879
880
881   @classmethod
882   def _GetUsedDevs(cls):
883     """Compute the list of used DRBD devices.
884
885     """
886     data = cls._GetProcData()
887
888     used_devs = {}
889     valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
890     for line in data:
891       match = valid_line.match(line)
892       if not match:
893         continue
894       minor = int(match.group(1))
895       state = match.group(2)
896       if state == cls._ST_UNCONFIGURED:
897         continue
898       used_devs[minor] = state, line
899
900     return used_devs
901
902
903   @classmethod
904   def _FindUnusedMinor(cls):
905     """Find an unused DRBD device.
906
907     """
908     data = cls._GetProcData()
909
910     valid_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
911     for line in data:
912       match = valid_line.match(line)
913       if match:
914         return int(match.group(1))
915     logger.Error("Error: no free drbd minors!")
916     return None
917
918
919   @classmethod
920   def _GetDevInfo(cls, minor):
921     """Get details about a given DRBD minor.
922
923     This return, if available, the local backing device in (major,
924     minor) formant and the local and remote (ip, port) information.
925
926     """
927     data = {}
928     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
929     if result.failed:
930       logger.Error("Can't display the drbd config: %s" % result.fail_reason)
931       return data
932     out = result.stdout
933     if out == "Not configured\n":
934       return data
935     for line in out.splitlines():
936       if "local_dev" not in data:
937         match = re.match("^Lower device: ([0-9]+):([0-9]+) .*$", line)
938         if match:
939           data["local_dev"] = (int(match.group(1)), int(match.group(2)))
940           continue
941       if "meta_dev" not in data:
942         match = re.match("^Meta device: (([0-9]+):([0-9]+)|internal).*$", line)
943         if match:
944           if match.group(2) is not None and match.group(3) is not None:
945             # matched on the major/minor
946             data["meta_dev"] = (int(match.group(2)), int(match.group(3)))
947           else:
948             # matched on the "internal" string
949             data["meta_dev"] = match.group(1)
950             # in this case, no meta_index is in the output
951             data["meta_index"] = -1
952           continue
953       if "meta_index" not in data:
954         match = re.match("^Meta index: ([0-9]+).*$", line)
955         if match:
956           data["meta_index"] = int(match.group(1))
957           continue
958       if "local_addr" not in data:
959         match = re.match("^Local address: ([0-9.]+):([0-9]+)$", line)
960         if match:
961           data["local_addr"] = (match.group(1), int(match.group(2)))
962           continue
963       if "remote_addr" not in data:
964         match = re.match("^Remote address: ([0-9.]+):([0-9]+)$", line)
965         if match:
966           data["remote_addr"] = (match.group(1), int(match.group(2)))
967           continue
968     return data
969
970
971   def _MatchesLocal(self, info):
972     """Test if our local config matches with an existing device.
973
974     The parameter should be as returned from `_GetDevInfo()`. This
975     method tests if our local backing device is the same as the one in
976     the info parameter, in effect testing if we look like the given
977     device.
978
979     """
980     if not ("local_dev" in info and "meta_dev" in info and
981             "meta_index" in info):
982       return False
983
984     backend = self._children[0]
985     if backend is not None:
986       retval = (info["local_dev"] == (backend.major, backend.minor))
987     else:
988       retval = (info["local_dev"] == (0, 0))
989     meta = self._children[1]
990     if meta is not None:
991       retval = retval and (info["meta_dev"] == (meta.major, meta.minor))
992       retval = retval and (info["meta_index"] == 0)
993     else:
994       retval = retval and (info["meta_dev"] == "internal" and
995                            info["meta_index"] == -1)
996     return retval
997
998
999   def _MatchesNet(self, info):
1000     """Test if our network config matches with an existing device.
1001
1002     The parameter should be as returned from `_GetDevInfo()`. This
1003     method tests if our network configuration is the same as the one
1004     in the info parameter, in effect testing if we look like the given
1005     device.
1006
1007     """
1008     if (((self._lhost is None and not ("local_addr" in info)) and
1009          (self._rhost is None and not ("remote_addr" in info)))):
1010       return True
1011
1012     if self._lhost is None:
1013       return False
1014
1015     if not ("local_addr" in info and
1016             "remote_addr" in info):
1017       return False
1018
1019     retval = (info["local_addr"] == (self._lhost, self._lport))
1020     retval = (retval and
1021               info["remote_addr"] == (self._rhost, self._rport))
1022     return retval
1023
1024
1025   @staticmethod
1026   def _IsValidMeta(meta_device):
1027     """Check if the given meta device looks like a valid one.
1028
1029     This currently only check the size, which must be around
1030     128MiB.
1031
1032     """
1033     result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1034     if result.failed:
1035       logger.Error("Failed to get device size: %s" % result.fail_reason)
1036       return False
1037     try:
1038       sectors = int(result.stdout)
1039     except ValueError:
1040       logger.Error("Invalid output from blockdev: '%s'" % result.stdout)
1041       return False
1042     bytes = sectors * 512
1043     if bytes < 128*1024*1024: # less than 128MiB
1044       logger.Error("Meta device too small (%.2fMib)" % (bytes/1024/1024))
1045       return False
1046     if bytes > (128+32)*1024*1024: # account for an extra (big) PE on LVM
1047       logger.Error("Meta device too big (%.2fMiB)" % (bytes/1024/1024))
1048       return False
1049     return True
1050
1051
1052   @classmethod
1053   def _AssembleLocal(cls, minor, backend, meta):
1054     """Configure the local part of a DRBD device.
1055
1056     This is the first thing that must be done on an unconfigured DRBD
1057     device. And it must be done only once.
1058
1059     """
1060     if not cls._IsValidMeta(meta):
1061       return False
1062     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
1063                            backend, meta, "0", "-e", "detach"])
1064     if result.failed:
1065       logger.Error("Can't attach local disk: %s" % result.output)
1066     return not result.failed
1067
1068
1069   @classmethod
1070   def _ShutdownLocal(cls, minor):
1071     """Detach from the local device.
1072
1073     I/Os will continue to be served from the remote device. If we
1074     don't have a remote device, this operation will fail.
1075
1076     """
1077     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1078     if result.failed:
1079       logger.Error("Can't detach local device: %s" % result.output)
1080     return not result.failed
1081
1082
1083   @staticmethod
1084   def _ShutdownAll(minor):
1085     """Deactivate the device.
1086
1087     This will, of course, fail if the device is in use.
1088
1089     """
1090     result = utils.RunCmd(["drbdsetup", DRBDev._DevPath(minor), "down"])
1091     if result.failed:
1092       logger.Error("Can't shutdown drbd device: %s" % result.output)
1093     return not result.failed
1094
1095
1096   @classmethod
1097   def _AssembleNet(cls, minor, net_info, protocol):
1098     """Configure the network part of the device.
1099
1100     This operation can be, in theory, done multiple times, but there
1101     have been cases (in lab testing) in which the network part of the
1102     device had become stuck and couldn't be shut down because activity
1103     from the new peer (also stuck) triggered a timer re-init and
1104     needed remote peer interface shutdown in order to clear. So please
1105     don't change online the net config.
1106
1107     """
1108     lhost, lport, rhost, rport = net_info
1109     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "net",
1110                            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport),
1111                            protocol])
1112     if result.failed:
1113       logger.Error("Can't setup network for dbrd device: %s" %
1114                    result.fail_reason)
1115       return False
1116
1117     timeout = time.time() + 10
1118     ok = False
1119     while time.time() < timeout:
1120       info = cls._GetDevInfo(minor)
1121       if not "local_addr" in info or not "remote_addr" in info:
1122         time.sleep(1)
1123         continue
1124       if (info["local_addr"] != (lhost, lport) or
1125           info["remote_addr"] != (rhost, rport)):
1126         time.sleep(1)
1127         continue
1128       ok = True
1129       break
1130     if not ok:
1131       logger.Error("Timeout while configuring network")
1132       return False
1133     return True
1134
1135
1136   @classmethod
1137   def _ShutdownNet(cls, minor):
1138     """Disconnect from the remote peer.
1139
1140     This fails if we don't have a local device.
1141
1142     """
1143     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1144     logger.Error("Can't shutdown network: %s" % result.output)
1145     return not result.failed
1146
1147
1148   def _SetFromMinor(self, minor):
1149     """Set our parameters based on the given minor.
1150
1151     This sets our minor variable and our dev_path.
1152
1153     """
1154     if minor is None:
1155       self.minor = self.dev_path = None
1156     else:
1157       self.minor = minor
1158       self.dev_path = self._DevPath(minor)
1159
1160
1161   def Assemble(self):
1162     """Assemble the drbd.
1163
1164     Method:
1165       - if we have a local backing device, we bind to it by:
1166         - checking the list of used drbd devices
1167         - check if the local minor use of any of them is our own device
1168         - if yes, abort?
1169         - if not, bind
1170       - if we have a local/remote net info:
1171         - redo the local backing device step for the remote device
1172         - check if any drbd device is using the local port,
1173           if yes abort
1174         - check if any remote drbd device is using the remote
1175           port, if yes abort (for now)
1176         - bind our net port
1177         - bind the remote net port
1178
1179     """
1180     self.Attach()
1181     if self.minor is not None:
1182       logger.Info("Already assembled")
1183       return True
1184
1185     result = super(DRBDev, self).Assemble()
1186     if not result:
1187       return result
1188
1189     minor = self._FindUnusedMinor()
1190     if minor is None:
1191       raise errors.BlockDeviceError, "Not enough free minors for DRBD!"
1192     need_localdev_teardown = False
1193     if self._children[0]:
1194       result = self._AssembleLocal(minor, self._children[0].dev_path,
1195                                    self._children[1].dev_path)
1196       if not result:
1197         return False
1198       need_localdev_teardown = True
1199     if self._lhost and self._lport and self._rhost and self._rport:
1200       result = self._AssembleNet(minor,
1201                                  (self._lhost, self._lport,
1202                                   self._rhost, self._rport),
1203                                  "C")
1204       if not result:
1205         if need_localdev_teardown:
1206           # we will ignore failures from this
1207           logger.Error("net setup failed, tearing down local device")
1208           self._ShutdownAll(minor)
1209         return False
1210     self._SetFromMinor(minor)
1211     return True
1212
1213
1214   def Shutdown(self):
1215     """Shutdown the DRBD device.
1216
1217     """
1218     if self.minor is None and not self.Attach():
1219       logger.Info("DRBD device not attached to a device during Shutdown")
1220       return True
1221     if not self._ShutdownAll(self.minor):
1222       return False
1223     self.minor = None
1224     self.dev_path = None
1225     return True
1226
1227
1228   def Attach(self):
1229     """Find a DRBD device which matches our config and attach to it.
1230
1231     In case of partially attached (local device matches but no network
1232     setup), we perform the network attach. If successful, we re-test
1233     the attach if can return success.
1234
1235     """
1236     for minor in self._GetUsedDevs():
1237       info = self._GetDevInfo(minor)
1238       match_l = self._MatchesLocal(info)
1239       match_r = self._MatchesNet(info)
1240       if match_l and match_r:
1241         break
1242       if match_l and not match_r and "local_addr" not in info:
1243         res_r = self._AssembleNet(minor,
1244                                   (self._lhost, self._lport,
1245                                    self._rhost, self._rport),
1246                                   "C")
1247         if res_r and self._MatchesNet(self._GetDevInfo(minor)):
1248           break
1249     else:
1250       minor = None
1251
1252     self._SetFromMinor(minor)
1253     return minor is not None
1254
1255
1256   def Open(self, force=False):
1257     """Make the local state primary.
1258
1259     If the 'force' parameter is given, the '--do-what-I-say' parameter
1260     is given. Since this is a pottentialy dangerous operation, the
1261     force flag should be only given after creation, when it actually
1262     has to be given.
1263
1264     """
1265     if self.minor is None and not self.Attach():
1266       logger.Error("DRBD cannot attach to a device during open")
1267       return False
1268     cmd = ["drbdsetup", self.dev_path, "primary"]
1269     if force:
1270       cmd.append("--do-what-I-say")
1271     result = utils.RunCmd(cmd)
1272     if result.failed:
1273       logger.Error("Can't make drbd device primary: %s" % result.output)
1274       return False
1275     return True
1276
1277
1278   def Close(self):
1279     """Make the local state secondary.
1280
1281     This will, of course, fail if the device is in use.
1282
1283     """
1284     if self.minor is None and not self.Attach():
1285       logger.Info("Instance not attached to a device")
1286       raise errors.BlockDeviceError("Can't find device")
1287     result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1288     if result.failed:
1289       logger.Error("Can't switch drbd device to secondary: %s" % result.output)
1290       raise errors.BlockDeviceError("Can't switch drbd device to secondary")
1291
1292
1293   def SetSyncSpeed(self, kbytes):
1294     """Set the speed of the DRBD syncer.
1295
1296     """
1297     children_result = super(DRBDev, self).SetSyncSpeed(kbytes)
1298     if self.minor is None:
1299       logger.Info("Instance not attached to a device")
1300       return False
1301     result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
1302                            kbytes])
1303     if result.failed:
1304       logger.Error("Can't change syncer rate: %s " % result.fail_reason)
1305     return not result.failed and children_result
1306
1307
1308   def GetSyncStatus(self):
1309     """Returns the sync status of the device.
1310
1311     Returns:
1312      (sync_percent, estimated_time)
1313
1314     If sync_percent is None, it means all is ok
1315     If estimated_time is None, it means we can't esimate
1316     the time needed, otherwise it's the time left in seconds
1317
1318     """
1319     if self.minor is None and not self.Attach():
1320       raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1321     proc_info = self._MassageProcData(self._GetProcData())
1322     if self.minor not in proc_info:
1323       raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1324                                     self.minor)
1325     line = proc_info[self.minor]
1326     match = re.match("^.*sync'ed: *([0-9.]+)%.*"
1327                      " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
1328     if match:
1329       sync_percent = float(match.group(1))
1330       hours = int(match.group(2))
1331       minutes = int(match.group(3))
1332       seconds = int(match.group(4))
1333       est_time = hours * 3600 + minutes * 60 + seconds
1334     else:
1335       sync_percent = None
1336       est_time = None
1337     match = re.match("^ *[0-9]+: cs:([^ ]+).*$", line)
1338     if not match:
1339       raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
1340                                     self.minor)
1341     client_state = match.group(1)
1342     is_degraded = client_state != "Connected"
1343     return sync_percent, est_time, is_degraded
1344
1345
1346   @staticmethod
1347   def _MassageProcData(data):
1348     """Transform the output of _GetProdData into a nicer form.
1349
1350     Returns:
1351       a dictionary of minor: joined lines from /proc/drbd for that minor
1352
1353     """
1354     lmatch = re.compile("^ *([0-9]+):.*$")
1355     results = {}
1356     old_minor = old_line = None
1357     for line in data:
1358       lresult = lmatch.match(line)
1359       if lresult is not None:
1360         if old_minor is not None:
1361           results[old_minor] = old_line
1362         old_minor = int(lresult.group(1))
1363         old_line = line
1364       else:
1365         if old_minor is not None:
1366           old_line += " " + line.strip()
1367     # add last line
1368     if old_minor is not None:
1369       results[old_minor] = old_line
1370     return results
1371
1372
1373   def GetStatus(self):
1374     """Compute the status of the DRBD device
1375
1376     Note that DRBD devices don't have the STATUS_EXISTING state.
1377
1378     """
1379     if self.minor is None and not self.Attach():
1380       return self.STATUS_UNKNOWN
1381
1382     data = self._GetProcData()
1383     match = re.compile("^ *%d: cs:[^ ]+ st:(Primary|Secondary)/.*$" %
1384                        self.minor)
1385     for line in data:
1386       mresult = match.match(line)
1387       if mresult:
1388         break
1389     else:
1390       logger.Error("Can't find myself!")
1391       return self.STATUS_UNKNOWN
1392
1393     state = mresult.group(2)
1394     if state == "Primary":
1395       result = self.STATUS_ONLINE
1396     else:
1397       result = self.STATUS_STANDBY
1398
1399     return result
1400
1401
1402   @staticmethod
1403   def _ZeroDevice(device):
1404     """Zero a device.
1405
1406     This writes until we get ENOSPC.
1407
1408     """
1409     f = open(device, "w")
1410     buf = "\0" * 1048576
1411     try:
1412       while True:
1413         f.write(buf)
1414     except IOError, err:
1415       if err.errno != errno.ENOSPC:
1416         raise
1417
1418
1419   @classmethod
1420   def Create(cls, unique_id, children, size):
1421     """Create a new DRBD device.
1422
1423     Since DRBD devices are not created per se, just assembled, this
1424     function just zeroes the meta device.
1425
1426     """
1427     if len(children) != 2:
1428       raise errors.ProgrammerError("Invalid setup for the drbd device")
1429     meta = children[1]
1430     meta.Assemble()
1431     if not meta.Attach():
1432       raise errors.BlockDeviceError("Can't attach to meta device")
1433     if not cls._IsValidMeta(meta.dev_path):
1434       raise errors.BlockDeviceError("Invalid meta device")
1435     logger.Info("Started zeroing device %s" % meta.dev_path)
1436     cls._ZeroDevice(meta.dev_path)
1437     logger.Info("Done zeroing device %s" % meta.dev_path)
1438     return cls(unique_id, children)
1439
1440
1441   def Remove(self):
1442     """Stub remove for DRBD devices.
1443
1444     """
1445     return self.Shutdown()
1446
1447
1448 DEV_MAP = {
1449   "lvm": LogicalVolume,
1450   "md_raid1": MDRaid1,
1451   "drbd": DRBDev,
1452   }
1453
1454
1455 def FindDevice(dev_type, unique_id, children):
1456   """Search for an existing, assembled device.
1457
1458   This will succeed only if the device exists and is assembled, but it
1459   does not do any actions in order to activate the device.
1460
1461   """
1462   if dev_type not in DEV_MAP:
1463     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1464   device = DEV_MAP[dev_type](unique_id, children)
1465   if not device.Attach():
1466     return None
1467   return  device
1468
1469
1470 def AttachOrAssemble(dev_type, unique_id, children):
1471   """Try to attach or assemble an existing device.
1472
1473   This will attach to an existing assembled device or will assemble
1474   the device, as needed, to bring it fully up.
1475
1476   """
1477   if dev_type not in DEV_MAP:
1478     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1479   device = DEV_MAP[dev_type](unique_id, children)
1480   if not device.Attach():
1481     device.Assemble()
1482   if not device.Attach():
1483     raise errors.BlockDeviceError("Can't find a valid block device for"
1484                                   " %s/%s/%s" %
1485                                   (dev_type, unique_id, children))
1486   return device
1487
1488
1489 def Create(dev_type, unique_id, children, size):
1490   """Create a device.
1491
1492   """
1493   if dev_type not in DEV_MAP:
1494     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1495   device = DEV_MAP[dev_type].Create(unique_id, children, size)
1496   return device