Add a generic write file function
[ganeti-local] / lib / bdev.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 """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 SetInfo(self, text):
262     """Update metadata with info text.
263
264     Only supported for some device types.
265
266     """
267     for child in self._children:
268       child.SetInfo(text)
269
270
271   def __repr__(self):
272     return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
273             (self.__class__, self.unique_id, self._children,
274              self.major, self.minor, self.dev_path))
275
276
277 class LogicalVolume(BlockDev):
278   """Logical Volume block device.
279
280   """
281   def __init__(self, unique_id, children):
282     """Attaches to a LV device.
283
284     The unique_id is a tuple (vg_name, lv_name)
285
286     """
287     super(LogicalVolume, self).__init__(unique_id, children)
288     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
289       raise ValueError("Invalid configuration data %s" % str(unique_id))
290     self._vg_name, self._lv_name = unique_id
291     self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
292     self.Attach()
293
294
295   @classmethod
296   def Create(cls, unique_id, children, size):
297     """Create a new logical volume.
298
299     """
300     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
301       raise ValueError("Invalid configuration data %s" % str(unique_id))
302     vg_name, lv_name = unique_id
303     pvs_info = cls.GetPVInfo(vg_name)
304     if not pvs_info:
305       raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
306                                     vg_name)
307     pvs_info.sort()
308     pvs_info.reverse()
309
310     pvlist = [ pv[1] for pv in pvs_info ]
311     free_size = sum([ pv[0] for pv in pvs_info ])
312
313     # The size constraint should have been checked from the master before
314     # calling the create function.
315     if free_size < size:
316       raise errors.BlockDeviceError("Not enough free space: required %s,"
317                                     " available %s" % (size, free_size))
318     result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
319                            vg_name] + pvlist)
320     if result.failed:
321       raise errors.BlockDeviceError(result.fail_reason)
322     return LogicalVolume(unique_id, children)
323
324   @staticmethod
325   def GetPVInfo(vg_name):
326     """Get the free space info for PVs in a volume group.
327
328     Args:
329       vg_name: the volume group name
330
331     Returns:
332       list of (free_space, name) with free_space in mebibytes
333
334     """
335     command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
336                "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
337                "--separator=:"]
338     result = utils.RunCmd(command)
339     if result.failed:
340       logger.Error("Can't get the PV information: %s" % result.fail_reason)
341       return None
342     data = []
343     for line in result.stdout.splitlines():
344       fields = line.strip().split(':')
345       if len(fields) != 4:
346         logger.Error("Can't parse pvs output: line '%s'" % line)
347         return None
348       # skip over pvs from another vg or ones which are not allocatable
349       if fields[1] != vg_name or fields[3][0] != 'a':
350         continue
351       data.append((float(fields[2]), fields[0]))
352
353     return data
354
355   def Remove(self):
356     """Remove this logical volume.
357
358     """
359     if not self.minor and not self.Attach():
360       # the LV does not exist
361       return True
362     result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
363                            (self._vg_name, self._lv_name)])
364     if result.failed:
365       logger.Error("Can't lvremove: %s" % result.fail_reason)
366
367     return not result.failed
368
369
370   def Attach(self):
371     """Attach to an existing LV.
372
373     This method will try to see if an existing and active LV exists
374     which matches the our name. If so, its major/minor will be
375     recorded.
376
377     """
378     result = utils.RunCmd(["lvdisplay", self.dev_path])
379     if result.failed:
380       logger.Error("Can't find LV %s: %s" %
381                    (self.dev_path, result.fail_reason))
382       return False
383     match = re.compile("^ *Block device *([0-9]+):([0-9]+).*$")
384     for line in result.stdout.splitlines():
385       match_result = match.match(line)
386       if match_result:
387         self.major = int(match_result.group(1))
388         self.minor = int(match_result.group(2))
389         return True
390     return False
391
392
393   def Assemble(self):
394     """Assemble the device.
395
396     This is a no-op for the LV device type. Eventually, we could
397     lvchange -ay here if we see that the LV is not active.
398
399     """
400     return True
401
402
403   def Shutdown(self):
404     """Shutdown the device.
405
406     This is a no-op for the LV device type, as we don't deactivate the
407     volumes on shutdown.
408
409     """
410     return True
411
412
413   def GetStatus(self):
414     """Return the status of the device.
415
416     Logical volumes will can be in all four states, although we don't
417     deactivate (lvchange -an) them when shutdown, so STATUS_EXISTING
418     should not be seen for our devices.
419
420     """
421     result = utils.RunCmd(["lvs", "--noheadings", "-olv_attr", self.dev_path])
422     if result.failed:
423       logger.Error("Can't display lv: %s" % result.fail_reason)
424       return self.STATUS_UNKNOWN
425     out = result.stdout.strip()
426     # format: type/permissions/alloc/fixed_minor/state/open
427     if len(out) != 6:
428       return self.STATUS_UNKNOWN
429     #writable = (out[1] == "w")
430     active = (out[4] == "a")
431     online = (out[5] == "o")
432     if online:
433       retval = self.STATUS_ONLINE
434     elif active:
435       retval = self.STATUS_STANDBY
436     else:
437       retval = self.STATUS_EXISTING
438
439     return retval
440
441
442   def Open(self, force=False):
443     """Make the device ready for I/O.
444
445     This is a no-op for the LV device type.
446
447     """
448     return True
449
450
451   def Close(self):
452     """Notifies that the device will no longer be used for I/O.
453
454     This is a no-op for the LV device type.
455
456     """
457     return True
458
459
460   def Snapshot(self, size):
461     """Create a snapshot copy of an lvm block device.
462
463     """
464     snap_name = self._lv_name + ".snap"
465
466     # remove existing snapshot if found
467     snap = LogicalVolume((self._vg_name, snap_name), None)
468     snap.Remove()
469
470     pvs_info = self.GetPVInfo(self._vg_name)
471     if not pvs_info:
472       raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
473                                     self._vg_name)
474     pvs_info.sort()
475     pvs_info.reverse()
476     free_size, pv_name = pvs_info[0]
477     if free_size < size:
478       raise errors.BlockDeviceError("Not enough free space: required %s,"
479                                     " available %s" % (size, free_size))
480
481     result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
482                            "-n%s" % snap_name, self.dev_path])
483     if result.failed:
484       raise errors.BlockDeviceError("command: %s error: %s" %
485                                     (result.cmd, result.fail_reason))
486
487     return snap_name
488
489
490   def SetInfo(self, text):
491     """Update metadata with info text.
492
493     """
494     BlockDev.SetInfo(self, text)
495
496     # Replace invalid characters
497     text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
498     text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
499
500     # Only up to 128 characters are allowed
501     text = text[:128]
502
503     result = utils.RunCmd(["lvchange", "--addtag", text,
504                            self.dev_path])
505     if result.failed:
506       raise errors.BlockDeviceError("Command: %s error: %s" %
507                                     (result.cmd, result.fail_reason))
508
509
510 class MDRaid1(BlockDev):
511   """raid1 device implemented via md.
512
513   """
514   def __init__(self, unique_id, children):
515     super(MDRaid1, self).__init__(unique_id, children)
516     self.major = 9
517     self.Attach()
518
519
520   def Attach(self):
521     """Find an array which matches our config and attach to it.
522
523     This tries to find a MD array which has the same UUID as our own.
524
525     """
526     minor = self._FindMDByUUID(self.unique_id)
527     if minor is not None:
528       self._SetFromMinor(minor)
529     else:
530       self.minor = None
531       self.dev_path = None
532
533     return (minor is not None)
534
535
536   @staticmethod
537   def _GetUsedDevs():
538     """Compute the list of in-use MD devices.
539
540     It doesn't matter if the used device have other raid level, just
541     that they are in use.
542
543     """
544     mdstat = open("/proc/mdstat", "r")
545     data = mdstat.readlines()
546     mdstat.close()
547
548     used_md = {}
549     valid_line = re.compile("^md([0-9]+) : .*$")
550     for line in data:
551       match = valid_line.match(line)
552       if match:
553         md_no = int(match.group(1))
554         used_md[md_no] = line
555
556     return used_md
557
558
559   @staticmethod
560   def _GetDevInfo(minor):
561     """Get info about a MD device.
562
563     Currently only uuid is returned.
564
565     """
566     result = utils.RunCmd(["mdadm", "-D", "/dev/md%d" % minor])
567     if result.failed:
568       logger.Error("Can't display md: %s" % result.fail_reason)
569       return None
570     retval = {}
571     for line in result.stdout.splitlines():
572       line = line.strip()
573       kv = line.split(" : ", 1)
574       if kv:
575         if kv[0] == "UUID":
576           retval["uuid"] = kv[1].split()[0]
577         elif kv[0] == "State":
578           retval["state"] = kv[1].split(", ")
579     return retval
580
581
582   @staticmethod
583   def _FindUnusedMinor():
584     """Compute an unused MD minor.
585
586     This code assumes that there are 256 minors only.
587
588     """
589     used_md = MDRaid1._GetUsedDevs()
590     i = 0
591     while i < 256:
592       if i not in used_md:
593         break
594       i += 1
595     if i == 256:
596       logger.Error("Critical: Out of md minor numbers.")
597       return None
598     return i
599
600
601   @classmethod
602   def _FindMDByUUID(cls, uuid):
603     """Find the minor of an MD array with a given UUID.
604
605     """
606     md_list = cls._GetUsedDevs()
607     for minor in md_list:
608       info = cls._GetDevInfo(minor)
609       if info and info["uuid"] == uuid:
610         return minor
611     return None
612
613
614   @staticmethod
615   def _ZeroSuperblock(dev_path):
616     """Zero the possible locations for an MD superblock.
617
618     The zero-ing can't be done via ``mdadm --zero-superblock`` as that
619     fails in versions 2.x with the same error code as non-writable
620     device.
621
622     The superblocks are located at (negative values are relative to
623     the end of the block device):
624       - -128k to end for version 0.90 superblock
625       - -8k to -12k for version 1.0 superblock (included in the above)
626       - 0k to 4k for version 1.1 superblock
627       - 4k to 8k for version 1.2 superblock
628
629     To cover all situations, the zero-ing will be:
630       - 0k to 128k
631       - -128k to end
632
633     As such, the minimum device size must be 128k, otherwise we'll get
634     I/O errors.
635
636     Note that this function depends on the fact that one can open,
637     read and write block devices normally.
638
639     """
640     overwrite_size = 128 * 1024
641     empty_buf = '\0' * overwrite_size
642     fd = open(dev_path, "r+")
643     try:
644       fd.seek(0, 0)
645       p1 = fd.tell()
646       fd.write(empty_buf)
647       p2 = fd.tell()
648       logger.Debug("Zeroed %s from %d to %d" % (dev_path, p1, p2))
649       fd.seek(-overwrite_size, 2)
650       p1 = fd.tell()
651       fd.write(empty_buf)
652       p2 = fd.tell()
653       logger.Debug("Zeroed %s from %d to %d" % (dev_path, p1, p2))
654     finally:
655       fd.close()
656
657   @classmethod
658   def Create(cls, unique_id, children, size):
659     """Create a new MD raid1 array.
660
661     """
662     if not isinstance(children, (tuple, list)):
663       raise ValueError("Invalid setup data for MDRaid1 dev: %s" %
664                        str(children))
665     for i in children:
666       if not isinstance(i, BlockDev):
667         raise ValueError("Invalid member in MDRaid1 dev: %s" % type(i))
668     for i in children:
669       try:
670         cls._ZeroSuperblock(i.dev_path)
671       except EnvironmentError, err:
672         logger.Error("Can't zero superblock for %s: %s" %
673                      (i.dev_path, str(err)))
674         return None
675     minor = cls._FindUnusedMinor()
676     result = utils.RunCmd(["mdadm", "--create", "/dev/md%d" % minor,
677                            "--auto=yes", "--force", "-l1",
678                            "-n%d" % len(children)] +
679                           [dev.dev_path for dev in children])
680
681     if result.failed:
682       logger.Error("Can't create md: %s: %s" % (result.fail_reason,
683                                                 result.output))
684       return None
685     info = cls._GetDevInfo(minor)
686     if not info or not "uuid" in info:
687       logger.Error("Wrong information returned from mdadm -D: %s" % str(info))
688       return None
689     return MDRaid1(info["uuid"], children)
690
691
692   def Remove(self):
693     """Stub remove function for MD RAID 1 arrays.
694
695     We don't remove the superblock right now. Mark a to do.
696
697     """
698     #TODO: maybe zero superblock on child devices?
699     return self.Shutdown()
700
701
702   def AddChild(self, device):
703     """Add a new member to the md raid1.
704
705     """
706     if self.minor is None and not self.Attach():
707       raise errors.BlockDeviceError("Can't attach to device")
708     if device.dev_path is None:
709       raise errors.BlockDeviceError("New child is not initialised")
710     result = utils.RunCmd(["mdadm", "-a", self.dev_path, device.dev_path])
711     if result.failed:
712       raise errors.BlockDeviceError("Failed to add new device to array: %s" %
713                                     result.output)
714     new_len = len(self._children) + 1
715     result = utils.RunCmd(["mdadm", "--grow", self.dev_path, "-n", new_len])
716     if result.failed:
717       raise errors.BlockDeviceError("Can't grow md array: %s" %
718                                     result.output)
719     self._children.append(device)
720
721
722   def RemoveChild(self, dev_path):
723     """Remove member from the md raid1.
724
725     """
726     if self.minor is None and not self.Attach():
727       raise errors.BlockDeviceError("Can't attach to device")
728     if len(self._children) == 1:
729       raise errors.BlockDeviceError("Can't reduce member when only one"
730                                     " child left")
731     for device in self._children:
732       if device.dev_path == dev_path:
733         break
734     else:
735       raise errors.BlockDeviceError("Can't find child with this path")
736     new_len = len(self._children) - 1
737     result = utils.RunCmd(["mdadm", "-f", self.dev_path, dev_path])
738     if result.failed:
739       raise errors.BlockDeviceError("Failed to mark device as failed: %s" %
740                                     result.output)
741
742     # it seems here we need a short delay for MD to update its
743     # superblocks
744     time.sleep(0.5)
745     result = utils.RunCmd(["mdadm", "-r", self.dev_path, dev_path])
746     if result.failed:
747       raise errors.BlockDeviceError("Failed to remove device from array:"
748                                         " %s" % result.output)
749     result = utils.RunCmd(["mdadm", "--grow", "--force", self.dev_path,
750                            "-n", new_len])
751     if result.failed:
752       raise errors.BlockDeviceError("Can't shrink md array: %s" %
753                                     result.output)
754     self._children.remove(device)
755
756
757   def GetStatus(self):
758     """Return the status of the device.
759
760     """
761     self.Attach()
762     if self.minor is None:
763       retval = self.STATUS_UNKNOWN
764     else:
765       retval = self.STATUS_ONLINE
766     return retval
767
768
769   def _SetFromMinor(self, minor):
770     """Set our parameters based on the given minor.
771
772     This sets our minor variable and our dev_path.
773
774     """
775     self.minor = minor
776     self.dev_path = "/dev/md%d" % minor
777
778
779   def Assemble(self):
780     """Assemble the MD device.
781
782     At this point we should have:
783       - list of children devices
784       - uuid
785
786     """
787     result = super(MDRaid1, self).Assemble()
788     if not result:
789       return result
790     md_list = self._GetUsedDevs()
791     for minor in md_list:
792       info = self._GetDevInfo(minor)
793       if info and info["uuid"] == self.unique_id:
794         self._SetFromMinor(minor)
795         logger.Info("MD array %s already started" % str(self))
796         return True
797     free_minor = self._FindUnusedMinor()
798     result = utils.RunCmd(["mdadm", "-A", "--auto=yes", "--uuid",
799                            self.unique_id, "/dev/md%d" % free_minor] +
800                           [bdev.dev_path for bdev in self._children])
801     if result.failed:
802       logger.Error("Can't assemble MD array: %s: %s" %
803                    (result.fail_reason, result.output))
804       self.minor = None
805     else:
806       self.minor = free_minor
807     return not result.failed
808
809
810   def Shutdown(self):
811     """Tear down the MD array.
812
813     This does a 'mdadm --stop' so after this command, the array is no
814     longer available.
815
816     """
817     if self.minor is None and not self.Attach():
818       logger.Info("MD object not attached to a device")
819       return True
820
821     result = utils.RunCmd(["mdadm", "--stop", "/dev/md%d" % self.minor])
822     if result.failed:
823       logger.Error("Can't stop MD array: %s" % result.fail_reason)
824       return False
825     self.minor = None
826     self.dev_path = None
827     return True
828
829
830   def SetSyncSpeed(self, kbytes):
831     """Set the maximum sync speed for the MD array.
832
833     """
834     result = super(MDRaid1, self).SetSyncSpeed(kbytes)
835     if self.minor is None:
836       logger.Error("MD array not attached to a device")
837       return False
838     f = open("/sys/block/md%d/md/sync_speed_max" % self.minor, "w")
839     try:
840       f.write("%d" % kbytes)
841     finally:
842       f.close()
843     f = open("/sys/block/md%d/md/sync_speed_min" % self.minor, "w")
844     try:
845       f.write("%d" % (kbytes/2))
846     finally:
847       f.close()
848     return result
849
850
851   def GetSyncStatus(self):
852     """Returns the sync status of the device.
853
854     Returns:
855      (sync_percent, estimated_time)
856
857     If sync_percent is None, it means all is ok
858     If estimated_time is None, it means we can't esimate
859     the time needed, otherwise it's the time left in seconds
860
861     """
862     if self.minor is None and not self.Attach():
863       raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
864     dev_info = self._GetDevInfo(self.minor)
865     is_clean = ("state" in dev_info and
866                 len(dev_info["state"]) == 1 and
867                 dev_info["state"][0] in ("clean", "active"))
868     sys_path = "/sys/block/md%s/md/" % self.minor
869     f = file(sys_path + "sync_action")
870     sync_status = f.readline().strip()
871     f.close()
872     if sync_status == "idle":
873       return None, None, not is_clean
874     f = file(sys_path + "sync_completed")
875     sync_completed = f.readline().strip().split(" / ")
876     f.close()
877     if len(sync_completed) != 2:
878       return 0, None, not is_clean
879     sync_done, sync_total = [float(i) for i in sync_completed]
880     sync_percent = 100.0*sync_done/sync_total
881     f = file(sys_path + "sync_speed")
882     sync_speed_k = int(f.readline().strip())
883     if sync_speed_k == 0:
884       time_est = None
885     else:
886       time_est = (sync_total - sync_done) / 2 / sync_speed_k
887     return sync_percent, time_est, not is_clean
888
889
890   def Open(self, force=False):
891     """Make the device ready for I/O.
892
893     This is a no-op for the MDRaid1 device type, although we could use
894     the 2.6.18's new array_state thing.
895
896     """
897     return True
898
899
900   def Close(self):
901     """Notifies that the device will no longer be used for I/O.
902
903     This is a no-op for the MDRaid1 device type, but see comment for
904     `Open()`.
905
906     """
907     return True
908
909
910
911 class DRBDev(BlockDev):
912   """DRBD block device.
913
914   This implements the local host part of the DRBD device, i.e. it
915   doesn't do anything to the supposed peer. If you need a fully
916   connected DRBD pair, you need to use this class on both hosts.
917
918   The unique_id for the drbd device is the (local_ip, local_port,
919   remote_ip, remote_port) tuple, and it must have two children: the
920   data device and the meta_device. The meta device is checked for
921   valid size and is zeroed on create.
922
923   """
924   _DRBD_MAJOR = 147
925   _ST_UNCONFIGURED = "Unconfigured"
926   _ST_WFCONNECTION = "WFConnection"
927   _ST_CONNECTED = "Connected"
928
929   def __init__(self, unique_id, children):
930     super(DRBDev, self).__init__(unique_id, children)
931     self.major = self._DRBD_MAJOR
932     if len(children) != 2:
933       raise ValueError("Invalid configuration data %s" % str(children))
934     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
935       raise ValueError("Invalid configuration data %s" % str(unique_id))
936     self._lhost, self._lport, self._rhost, self._rport = unique_id
937     self.Attach()
938
939   @staticmethod
940   def _DevPath(minor):
941     """Return the path to a drbd device for a given minor.
942
943     """
944     return "/dev/drbd%d" % minor
945
946   @staticmethod
947   def _GetProcData():
948     """Return data from /proc/drbd.
949
950     """
951     stat = open("/proc/drbd", "r")
952     data = stat.read().splitlines()
953     stat.close()
954     return data
955
956
957   @classmethod
958   def _GetUsedDevs(cls):
959     """Compute the list of used DRBD devices.
960
961     """
962     data = cls._GetProcData()
963
964     used_devs = {}
965     valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
966     for line in data:
967       match = valid_line.match(line)
968       if not match:
969         continue
970       minor = int(match.group(1))
971       state = match.group(2)
972       if state == cls._ST_UNCONFIGURED:
973         continue
974       used_devs[minor] = state, line
975
976     return used_devs
977
978
979   @classmethod
980   def _FindUnusedMinor(cls):
981     """Find an unused DRBD device.
982
983     """
984     data = cls._GetProcData()
985
986     valid_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
987     for line in data:
988       match = valid_line.match(line)
989       if match:
990         return int(match.group(1))
991     logger.Error("Error: no free drbd minors!")
992     return None
993
994
995   @classmethod
996   def _GetDevInfo(cls, minor):
997     """Get details about a given DRBD minor.
998
999     This return, if available, the local backing device in (major,
1000     minor) formant and the local and remote (ip, port) information.
1001
1002     """
1003     data = {}
1004     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1005     if result.failed:
1006       logger.Error("Can't display the drbd config: %s" % result.fail_reason)
1007       return data
1008     out = result.stdout
1009     if out == "Not configured\n":
1010       return data
1011     for line in out.splitlines():
1012       if "local_dev" not in data:
1013         match = re.match("^Lower device: ([0-9]+):([0-9]+) .*$", line)
1014         if match:
1015           data["local_dev"] = (int(match.group(1)), int(match.group(2)))
1016           continue
1017       if "meta_dev" not in data:
1018         match = re.match("^Meta device: (([0-9]+):([0-9]+)|internal).*$", line)
1019         if match:
1020           if match.group(2) is not None and match.group(3) is not None:
1021             # matched on the major/minor
1022             data["meta_dev"] = (int(match.group(2)), int(match.group(3)))
1023           else:
1024             # matched on the "internal" string
1025             data["meta_dev"] = match.group(1)
1026             # in this case, no meta_index is in the output
1027             data["meta_index"] = -1
1028           continue
1029       if "meta_index" not in data:
1030         match = re.match("^Meta index: ([0-9]+).*$", line)
1031         if match:
1032           data["meta_index"] = int(match.group(1))
1033           continue
1034       if "local_addr" not in data:
1035         match = re.match("^Local address: ([0-9.]+):([0-9]+)$", line)
1036         if match:
1037           data["local_addr"] = (match.group(1), int(match.group(2)))
1038           continue
1039       if "remote_addr" not in data:
1040         match = re.match("^Remote address: ([0-9.]+):([0-9]+)$", line)
1041         if match:
1042           data["remote_addr"] = (match.group(1), int(match.group(2)))
1043           continue
1044     return data
1045
1046
1047   def _MatchesLocal(self, info):
1048     """Test if our local config matches with an existing device.
1049
1050     The parameter should be as returned from `_GetDevInfo()`. This
1051     method tests if our local backing device is the same as the one in
1052     the info parameter, in effect testing if we look like the given
1053     device.
1054
1055     """
1056     if not ("local_dev" in info and "meta_dev" in info and
1057             "meta_index" in info):
1058       return False
1059
1060     backend = self._children[0]
1061     if backend is not None:
1062       retval = (info["local_dev"] == (backend.major, backend.minor))
1063     else:
1064       retval = (info["local_dev"] == (0, 0))
1065     meta = self._children[1]
1066     if meta is not None:
1067       retval = retval and (info["meta_dev"] == (meta.major, meta.minor))
1068       retval = retval and (info["meta_index"] == 0)
1069     else:
1070       retval = retval and (info["meta_dev"] == "internal" and
1071                            info["meta_index"] == -1)
1072     return retval
1073
1074
1075   def _MatchesNet(self, info):
1076     """Test if our network config matches with an existing device.
1077
1078     The parameter should be as returned from `_GetDevInfo()`. This
1079     method tests if our network configuration is the same as the one
1080     in the info parameter, in effect testing if we look like the given
1081     device.
1082
1083     """
1084     if (((self._lhost is None and not ("local_addr" in info)) and
1085          (self._rhost is None and not ("remote_addr" in info)))):
1086       return True
1087
1088     if self._lhost is None:
1089       return False
1090
1091     if not ("local_addr" in info and
1092             "remote_addr" in info):
1093       return False
1094
1095     retval = (info["local_addr"] == (self._lhost, self._lport))
1096     retval = (retval and
1097               info["remote_addr"] == (self._rhost, self._rport))
1098     return retval
1099
1100
1101   @staticmethod
1102   def _IsValidMeta(meta_device):
1103     """Check if the given meta device looks like a valid one.
1104
1105     This currently only check the size, which must be around
1106     128MiB.
1107
1108     """
1109     result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1110     if result.failed:
1111       logger.Error("Failed to get device size: %s" % result.fail_reason)
1112       return False
1113     try:
1114       sectors = int(result.stdout)
1115     except ValueError:
1116       logger.Error("Invalid output from blockdev: '%s'" % result.stdout)
1117       return False
1118     bytes = sectors * 512
1119     if bytes < 128*1024*1024: # less than 128MiB
1120       logger.Error("Meta device too small (%.2fMib)" % (bytes/1024/1024))
1121       return False
1122     if bytes > (128+32)*1024*1024: # account for an extra (big) PE on LVM
1123       logger.Error("Meta device too big (%.2fMiB)" % (bytes/1024/1024))
1124       return False
1125     return True
1126
1127
1128   @classmethod
1129   def _AssembleLocal(cls, minor, backend, meta):
1130     """Configure the local part of a DRBD device.
1131
1132     This is the first thing that must be done on an unconfigured DRBD
1133     device. And it must be done only once.
1134
1135     """
1136     if not cls._IsValidMeta(meta):
1137       return False
1138     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
1139                            backend, meta, "0", "-e", "detach"])
1140     if result.failed:
1141       logger.Error("Can't attach local disk: %s" % result.output)
1142     return not result.failed
1143
1144
1145   @classmethod
1146   def _ShutdownLocal(cls, minor):
1147     """Detach from the local device.
1148
1149     I/Os will continue to be served from the remote device. If we
1150     don't have a remote device, this operation will fail.
1151
1152     """
1153     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1154     if result.failed:
1155       logger.Error("Can't detach local device: %s" % result.output)
1156     return not result.failed
1157
1158
1159   @staticmethod
1160   def _ShutdownAll(minor):
1161     """Deactivate the device.
1162
1163     This will, of course, fail if the device is in use.
1164
1165     """
1166     result = utils.RunCmd(["drbdsetup", DRBDev._DevPath(minor), "down"])
1167     if result.failed:
1168       logger.Error("Can't shutdown drbd device: %s" % result.output)
1169     return not result.failed
1170
1171
1172   @classmethod
1173   def _AssembleNet(cls, minor, net_info, protocol):
1174     """Configure the network part of the device.
1175
1176     This operation can be, in theory, done multiple times, but there
1177     have been cases (in lab testing) in which the network part of the
1178     device had become stuck and couldn't be shut down because activity
1179     from the new peer (also stuck) triggered a timer re-init and
1180     needed remote peer interface shutdown in order to clear. So please
1181     don't change online the net config.
1182
1183     """
1184     lhost, lport, rhost, rport = net_info
1185     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "net",
1186                            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport),
1187                            protocol])
1188     if result.failed:
1189       logger.Error("Can't setup network for dbrd device: %s" %
1190                    result.fail_reason)
1191       return False
1192
1193     timeout = time.time() + 10
1194     ok = False
1195     while time.time() < timeout:
1196       info = cls._GetDevInfo(minor)
1197       if not "local_addr" in info or not "remote_addr" in info:
1198         time.sleep(1)
1199         continue
1200       if (info["local_addr"] != (lhost, lport) or
1201           info["remote_addr"] != (rhost, rport)):
1202         time.sleep(1)
1203         continue
1204       ok = True
1205       break
1206     if not ok:
1207       logger.Error("Timeout while configuring network")
1208       return False
1209     return True
1210
1211
1212   @classmethod
1213   def _ShutdownNet(cls, minor):
1214     """Disconnect from the remote peer.
1215
1216     This fails if we don't have a local device.
1217
1218     """
1219     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1220     logger.Error("Can't shutdown network: %s" % result.output)
1221     return not result.failed
1222
1223
1224   def _SetFromMinor(self, minor):
1225     """Set our parameters based on the given minor.
1226
1227     This sets our minor variable and our dev_path.
1228
1229     """
1230     if minor is None:
1231       self.minor = self.dev_path = None
1232     else:
1233       self.minor = minor
1234       self.dev_path = self._DevPath(minor)
1235
1236
1237   def Assemble(self):
1238     """Assemble the drbd.
1239
1240     Method:
1241       - if we have a local backing device, we bind to it by:
1242         - checking the list of used drbd devices
1243         - check if the local minor use of any of them is our own device
1244         - if yes, abort?
1245         - if not, bind
1246       - if we have a local/remote net info:
1247         - redo the local backing device step for the remote device
1248         - check if any drbd device is using the local port,
1249           if yes abort
1250         - check if any remote drbd device is using the remote
1251           port, if yes abort (for now)
1252         - bind our net port
1253         - bind the remote net port
1254
1255     """
1256     self.Attach()
1257     if self.minor is not None:
1258       logger.Info("Already assembled")
1259       return True
1260
1261     result = super(DRBDev, self).Assemble()
1262     if not result:
1263       return result
1264
1265     minor = self._FindUnusedMinor()
1266     if minor is None:
1267       raise errors.BlockDeviceError("Not enough free minors for DRBD!")
1268     need_localdev_teardown = False
1269     if self._children[0]:
1270       result = self._AssembleLocal(minor, self._children[0].dev_path,
1271                                    self._children[1].dev_path)
1272       if not result:
1273         return False
1274       need_localdev_teardown = True
1275     if self._lhost and self._lport and self._rhost and self._rport:
1276       result = self._AssembleNet(minor,
1277                                  (self._lhost, self._lport,
1278                                   self._rhost, self._rport),
1279                                  "C")
1280       if not result:
1281         if need_localdev_teardown:
1282           # we will ignore failures from this
1283           logger.Error("net setup failed, tearing down local device")
1284           self._ShutdownAll(minor)
1285         return False
1286     self._SetFromMinor(minor)
1287     return True
1288
1289
1290   def Shutdown(self):
1291     """Shutdown the DRBD device.
1292
1293     """
1294     if self.minor is None and not self.Attach():
1295       logger.Info("DRBD device not attached to a device during Shutdown")
1296       return True
1297     if not self._ShutdownAll(self.minor):
1298       return False
1299     self.minor = None
1300     self.dev_path = None
1301     return True
1302
1303
1304   def Attach(self):
1305     """Find a DRBD device which matches our config and attach to it.
1306
1307     In case of partially attached (local device matches but no network
1308     setup), we perform the network attach. If successful, we re-test
1309     the attach if can return success.
1310
1311     """
1312     for minor in self._GetUsedDevs():
1313       info = self._GetDevInfo(minor)
1314       match_l = self._MatchesLocal(info)
1315       match_r = self._MatchesNet(info)
1316       if match_l and match_r:
1317         break
1318       if match_l and not match_r and "local_addr" not in info:
1319         res_r = self._AssembleNet(minor,
1320                                   (self._lhost, self._lport,
1321                                    self._rhost, self._rport),
1322                                   "C")
1323         if res_r and self._MatchesNet(self._GetDevInfo(minor)):
1324           break
1325     else:
1326       minor = None
1327
1328     self._SetFromMinor(minor)
1329     return minor is not None
1330
1331
1332   def Open(self, force=False):
1333     """Make the local state primary.
1334
1335     If the 'force' parameter is given, the '--do-what-I-say' parameter
1336     is given. Since this is a pottentialy dangerous operation, the
1337     force flag should be only given after creation, when it actually
1338     has to be given.
1339
1340     """
1341     if self.minor is None and not self.Attach():
1342       logger.Error("DRBD cannot attach to a device during open")
1343       return False
1344     cmd = ["drbdsetup", self.dev_path, "primary"]
1345     if force:
1346       cmd.append("--do-what-I-say")
1347     result = utils.RunCmd(cmd)
1348     if result.failed:
1349       logger.Error("Can't make drbd device primary: %s" % result.output)
1350       return False
1351     return True
1352
1353
1354   def Close(self):
1355     """Make the local state secondary.
1356
1357     This will, of course, fail if the device is in use.
1358
1359     """
1360     if self.minor is None and not self.Attach():
1361       logger.Info("Instance not attached to a device")
1362       raise errors.BlockDeviceError("Can't find device")
1363     result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1364     if result.failed:
1365       logger.Error("Can't switch drbd device to secondary: %s" % result.output)
1366       raise errors.BlockDeviceError("Can't switch drbd device to secondary")
1367
1368
1369   def SetSyncSpeed(self, kbytes):
1370     """Set the speed of the DRBD syncer.
1371
1372     """
1373     children_result = super(DRBDev, self).SetSyncSpeed(kbytes)
1374     if self.minor is None:
1375       logger.Info("Instance not attached to a device")
1376       return False
1377     result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
1378                            kbytes])
1379     if result.failed:
1380       logger.Error("Can't change syncer rate: %s " % result.fail_reason)
1381     return not result.failed and children_result
1382
1383
1384   def GetSyncStatus(self):
1385     """Returns the sync status of the device.
1386
1387     Returns:
1388      (sync_percent, estimated_time)
1389
1390     If sync_percent is None, it means all is ok
1391     If estimated_time is None, it means we can't esimate
1392     the time needed, otherwise it's the time left in seconds
1393
1394     """
1395     if self.minor is None and not self.Attach():
1396       raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1397     proc_info = self._MassageProcData(self._GetProcData())
1398     if self.minor not in proc_info:
1399       raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1400                                     self.minor)
1401     line = proc_info[self.minor]
1402     match = re.match("^.*sync'ed: *([0-9.]+)%.*"
1403                      " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
1404     if match:
1405       sync_percent = float(match.group(1))
1406       hours = int(match.group(2))
1407       minutes = int(match.group(3))
1408       seconds = int(match.group(4))
1409       est_time = hours * 3600 + minutes * 60 + seconds
1410     else:
1411       sync_percent = None
1412       est_time = None
1413     match = re.match("^ *[0-9]+: cs:([^ ]+).*$", line)
1414     if not match:
1415       raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
1416                                     self.minor)
1417     client_state = match.group(1)
1418     is_degraded = client_state != "Connected"
1419     return sync_percent, est_time, is_degraded
1420
1421
1422   @staticmethod
1423   def _MassageProcData(data):
1424     """Transform the output of _GetProdData into a nicer form.
1425
1426     Returns:
1427       a dictionary of minor: joined lines from /proc/drbd for that minor
1428
1429     """
1430     lmatch = re.compile("^ *([0-9]+):.*$")
1431     results = {}
1432     old_minor = old_line = None
1433     for line in data:
1434       lresult = lmatch.match(line)
1435       if lresult is not None:
1436         if old_minor is not None:
1437           results[old_minor] = old_line
1438         old_minor = int(lresult.group(1))
1439         old_line = line
1440       else:
1441         if old_minor is not None:
1442           old_line += " " + line.strip()
1443     # add last line
1444     if old_minor is not None:
1445       results[old_minor] = old_line
1446     return results
1447
1448
1449   def GetStatus(self):
1450     """Compute the status of the DRBD device
1451
1452     Note that DRBD devices don't have the STATUS_EXISTING state.
1453
1454     """
1455     if self.minor is None and not self.Attach():
1456       return self.STATUS_UNKNOWN
1457
1458     data = self._GetProcData()
1459     match = re.compile("^ *%d: cs:[^ ]+ st:(Primary|Secondary)/.*$" %
1460                        self.minor)
1461     for line in data:
1462       mresult = match.match(line)
1463       if mresult:
1464         break
1465     else:
1466       logger.Error("Can't find myself!")
1467       return self.STATUS_UNKNOWN
1468
1469     state = mresult.group(2)
1470     if state == "Primary":
1471       result = self.STATUS_ONLINE
1472     else:
1473       result = self.STATUS_STANDBY
1474
1475     return result
1476
1477
1478   @staticmethod
1479   def _ZeroDevice(device):
1480     """Zero a device.
1481
1482     This writes until we get ENOSPC.
1483
1484     """
1485     f = open(device, "w")
1486     buf = "\0" * 1048576
1487     try:
1488       while True:
1489         f.write(buf)
1490     except IOError, err:
1491       if err.errno != errno.ENOSPC:
1492         raise
1493
1494
1495   @classmethod
1496   def Create(cls, unique_id, children, size):
1497     """Create a new DRBD device.
1498
1499     Since DRBD devices are not created per se, just assembled, this
1500     function just zeroes the meta device.
1501
1502     """
1503     if len(children) != 2:
1504       raise errors.ProgrammerError("Invalid setup for the drbd device")
1505     meta = children[1]
1506     meta.Assemble()
1507     if not meta.Attach():
1508       raise errors.BlockDeviceError("Can't attach to meta device")
1509     if not cls._IsValidMeta(meta.dev_path):
1510       raise errors.BlockDeviceError("Invalid meta device")
1511     logger.Info("Started zeroing device %s" % meta.dev_path)
1512     cls._ZeroDevice(meta.dev_path)
1513     logger.Info("Done zeroing device %s" % meta.dev_path)
1514     return cls(unique_id, children)
1515
1516
1517   def Remove(self):
1518     """Stub remove for DRBD devices.
1519
1520     """
1521     return self.Shutdown()
1522
1523
1524 DEV_MAP = {
1525   "lvm": LogicalVolume,
1526   "md_raid1": MDRaid1,
1527   "drbd": DRBDev,
1528   }
1529
1530
1531 def FindDevice(dev_type, unique_id, children):
1532   """Search for an existing, assembled device.
1533
1534   This will succeed only if the device exists and is assembled, but it
1535   does not do any actions in order to activate the device.
1536
1537   """
1538   if dev_type not in DEV_MAP:
1539     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1540   device = DEV_MAP[dev_type](unique_id, children)
1541   if not device.Attach():
1542     return None
1543   return  device
1544
1545
1546 def AttachOrAssemble(dev_type, unique_id, children):
1547   """Try to attach or assemble an existing device.
1548
1549   This will attach to an existing assembled device or will assemble
1550   the device, as needed, to bring it fully up.
1551
1552   """
1553   if dev_type not in DEV_MAP:
1554     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1555   device = DEV_MAP[dev_type](unique_id, children)
1556   if not device.Attach():
1557     device.Assemble()
1558   if not device.Attach():
1559     raise errors.BlockDeviceError("Can't find a valid block device for"
1560                                   " %s/%s/%s" %
1561                                   (dev_type, unique_id, children))
1562   return device
1563
1564
1565 def Create(dev_type, unique_id, children, size):
1566   """Create a device.
1567
1568   """
1569   if dev_type not in DEV_MAP:
1570     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1571   device = DEV_MAP[dev_type].Create(unique_id, children, size)
1572   return device