Initial implementation of drbd8 template type
[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 import pyparsing as pyp
28
29 from ganeti import utils
30 from ganeti import logger
31 from ganeti import errors
32 from ganeti import constants
33
34
35 class BlockDev(object):
36   """Block device abstract class.
37
38   A block device can be in the following states:
39     - not existing on the system, and by `Create()` it goes into:
40     - existing but not setup/not active, and by `Assemble()` goes into:
41     - active read-write and by `Open()` it goes into
42     - online (=used, or ready for use)
43
44   A device can also be online but read-only, however we are not using
45   the readonly state (MD and LV have it, if needed in the future)
46   and we are usually looking at this like at a stack, so it's easier
47   to conceptualise the transition from not-existing to online and back
48   like a linear one.
49
50   The many different states of the device are due to the fact that we
51   need to cover many device types:
52     - logical volumes are created, lvchange -a y $lv, and used
53     - md arrays are created or assembled and used
54     - drbd devices are attached to a local disk/remote peer and made primary
55
56   The status of the device can be examined by `GetStatus()`, which
57   returns a numerical value, depending on the position in the
58   transition stack of the device.
59
60   A block device is identified by three items:
61     - the /dev path of the device (dynamic)
62     - a unique ID of the device (static)
63     - it's major/minor pair (dynamic)
64
65   Not all devices implement both the first two as distinct items. LVM
66   logical volumes have their unique ID (the pair volume group, logical
67   volume name) in a 1-to-1 relation to the dev path. For MD devices,
68   the /dev path is dynamic and the unique ID is the UUID generated at
69   array creation plus the slave list. For DRBD devices, the /dev path
70   is again dynamic and the unique id is the pair (host1, dev1),
71   (host2, dev2).
72
73   You can get to a device in two ways:
74     - creating the (real) device, which returns you
75       an attached instance (lvcreate, mdadm --create)
76     - attaching of a python instance to an existing (real) device
77
78   The second point, the attachement to a device, is different
79   depending on whether the device is assembled or not. At init() time,
80   we search for a device with the same unique_id as us. If found,
81   good. It also means that the device is already assembled. If not,
82   after assembly we'll have our correct major/minor.
83
84   """
85   STATUS_UNKNOWN = 0
86   STATUS_EXISTING = 1
87   STATUS_STANDBY = 2
88   STATUS_ONLINE = 3
89
90   STATUS_MAP = {
91     STATUS_UNKNOWN: "unknown",
92     STATUS_EXISTING: "existing",
93     STATUS_STANDBY: "ready for use",
94     STATUS_ONLINE: "online",
95     }
96
97
98   def __init__(self, unique_id, children):
99     self._children = children
100     self.dev_path = None
101     self.unique_id = unique_id
102     self.major = None
103     self.minor = None
104
105
106   def Assemble(self):
107     """Assemble the device from its components.
108
109     If this is a plain block device (e.g. LVM) than assemble does
110     nothing, as the LVM has no children and we don't put logical
111     volumes offline.
112
113     One guarantee is that after the device has been assembled, it
114     knows its major/minor numbers. This allows other devices (usually
115     parents) to probe correctly for their children.
116
117     """
118     status = True
119     for child in self._children:
120       if not isinstance(child, BlockDev):
121         raise TypeError("Invalid child passed of type '%s'" % type(child))
122       if not status:
123         break
124       status = status and child.Assemble()
125       if not status:
126         break
127       status = status and child.Open()
128
129     if not status:
130       for child in self._children:
131         child.Shutdown()
132     return status
133
134
135   def Attach(self):
136     """Find a device which matches our config and attach to it.
137
138     """
139     raise NotImplementedError
140
141
142   def Close(self):
143     """Notifies that the device will no longer be used for I/O.
144
145     """
146     raise NotImplementedError
147
148
149   @classmethod
150   def Create(cls, unique_id, children, size):
151     """Create the device.
152
153     If the device cannot be created, it will return None
154     instead. Error messages go to the logging system.
155
156     Note that for some devices, the unique_id is used, and for other,
157     the children. The idea is that these two, taken together, are
158     enough for both creation and assembly (later).
159
160     """
161     raise NotImplementedError
162
163
164   def Remove(self):
165     """Remove this device.
166
167     This makes sense only for some of the device types: LV and to a
168     lesser degree, md devices. Also note that if the device can't
169     attach, the removal can't be completed.
170
171     """
172     raise NotImplementedError
173
174
175   def GetStatus(self):
176     """Return the status of the device.
177
178     """
179     raise NotImplementedError
180
181
182   def Open(self, force=False):
183     """Make the device ready for use.
184
185     This makes the device ready for I/O. For now, just the DRBD
186     devices need this.
187
188     The force parameter signifies that if the device has any kind of
189     --force thing, it should be used, we know what we are doing.
190
191     """
192     raise NotImplementedError
193
194
195   def Shutdown(self):
196     """Shut down the device, freeing its children.
197
198     This undoes the `Assemble()` work, except for the child
199     assembling; as such, the children on the device are still
200     assembled after this call.
201
202     """
203     raise NotImplementedError
204
205
206   def SetSyncSpeed(self, speed):
207     """Adjust the sync speed of the mirror.
208
209     In case this is not a mirroring device, this is no-op.
210
211     """
212     result = True
213     if self._children:
214       for child in self._children:
215         result = result and child.SetSyncSpeed(speed)
216     return result
217
218
219   def GetSyncStatus(self):
220     """Returns the sync status of the device.
221
222     If this device is a mirroring device, this function returns the
223     status of the mirror.
224
225     Returns:
226      (sync_percent, estimated_time, is_degraded)
227
228     If sync_percent is None, it means all is ok
229     If estimated_time is None, it means we can't estimate
230     the time needed, otherwise it's the time left in seconds
231     If is_degraded is True, it means the device is missing
232     redundancy. This is usually a sign that something went wrong in
233     the device setup, if sync_percent is None.
234
235     """
236     return None, None, False
237
238
239   def CombinedSyncStatus(self):
240     """Calculate the mirror status recursively for our children.
241
242     The return value is the same as for `GetSyncStatus()` except the
243     minimum percent and maximum time are calculated across our
244     children.
245
246     """
247     min_percent, max_time, is_degraded = self.GetSyncStatus()
248     if self._children:
249       for child in self._children:
250         c_percent, c_time, c_degraded = child.GetSyncStatus()
251         if min_percent is None:
252           min_percent = c_percent
253         elif c_percent is not None:
254           min_percent = min(min_percent, c_percent)
255         if max_time is None:
256           max_time = c_time
257         elif c_time is not None:
258           max_time = max(max_time, c_time)
259         is_degraded = is_degraded or c_degraded
260     return min_percent, max_time, is_degraded
261
262
263   def SetInfo(self, text):
264     """Update metadata with info text.
265
266     Only supported for some device types.
267
268     """
269     for child in self._children:
270       child.SetInfo(text)
271
272
273   def __repr__(self):
274     return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
275             (self.__class__, self.unique_id, self._children,
276              self.major, self.minor, self.dev_path))
277
278
279 class LogicalVolume(BlockDev):
280   """Logical Volume block device.
281
282   """
283   def __init__(self, unique_id, children):
284     """Attaches to a LV device.
285
286     The unique_id is a tuple (vg_name, lv_name)
287
288     """
289     super(LogicalVolume, self).__init__(unique_id, children)
290     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
291       raise ValueError("Invalid configuration data %s" % str(unique_id))
292     self._vg_name, self._lv_name = unique_id
293     self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
294     self.Attach()
295
296
297   @classmethod
298   def Create(cls, unique_id, children, size):
299     """Create a new logical volume.
300
301     """
302     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
303       raise ValueError("Invalid configuration data %s" % str(unique_id))
304     vg_name, lv_name = unique_id
305     pvs_info = cls.GetPVInfo(vg_name)
306     if not pvs_info:
307       raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
308                                     vg_name)
309     pvs_info.sort()
310     pvs_info.reverse()
311
312     pvlist = [ pv[1] for pv in pvs_info ]
313     free_size = sum([ pv[0] for pv in pvs_info ])
314
315     # The size constraint should have been checked from the master before
316     # calling the create function.
317     if free_size < size:
318       raise errors.BlockDeviceError("Not enough free space: required %s,"
319                                     " available %s" % (size, free_size))
320     result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
321                            vg_name] + pvlist)
322     if result.failed:
323       raise errors.BlockDeviceError(result.fail_reason)
324     return LogicalVolume(unique_id, children)
325
326   @staticmethod
327   def GetPVInfo(vg_name):
328     """Get the free space info for PVs in a volume group.
329
330     Args:
331       vg_name: the volume group name
332
333     Returns:
334       list of (free_space, name) with free_space in mebibytes
335
336     """
337     command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
338                "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
339                "--separator=:"]
340     result = utils.RunCmd(command)
341     if result.failed:
342       logger.Error("Can't get the PV information: %s" % result.fail_reason)
343       return None
344     data = []
345     for line in result.stdout.splitlines():
346       fields = line.strip().split(':')
347       if len(fields) != 4:
348         logger.Error("Can't parse pvs output: line '%s'" % line)
349         return None
350       # skip over pvs from another vg or ones which are not allocatable
351       if fields[1] != vg_name or fields[3][0] != 'a':
352         continue
353       data.append((float(fields[2]), fields[0]))
354
355     return data
356
357   def Remove(self):
358     """Remove this logical volume.
359
360     """
361     if not self.minor and not self.Attach():
362       # the LV does not exist
363       return True
364     result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
365                            (self._vg_name, self._lv_name)])
366     if result.failed:
367       logger.Error("Can't lvremove: %s" % result.fail_reason)
368
369     return not result.failed
370
371
372   def Attach(self):
373     """Attach to an existing LV.
374
375     This method will try to see if an existing and active LV exists
376     which matches the our name. If so, its major/minor will be
377     recorded.
378
379     """
380     result = utils.RunCmd(["lvdisplay", self.dev_path])
381     if result.failed:
382       logger.Error("Can't find LV %s: %s" %
383                    (self.dev_path, result.fail_reason))
384       return False
385     match = re.compile("^ *Block device *([0-9]+):([0-9]+).*$")
386     for line in result.stdout.splitlines():
387       match_result = match.match(line)
388       if match_result:
389         self.major = int(match_result.group(1))
390         self.minor = int(match_result.group(2))
391         return True
392     return False
393
394
395   def Assemble(self):
396     """Assemble the device.
397
398     This is a no-op for the LV device type. Eventually, we could
399     lvchange -ay here if we see that the LV is not active.
400
401     """
402     return True
403
404
405   def Shutdown(self):
406     """Shutdown the device.
407
408     This is a no-op for the LV device type, as we don't deactivate the
409     volumes on shutdown.
410
411     """
412     return True
413
414
415   def GetStatus(self):
416     """Return the status of the device.
417
418     Logical volumes will can be in all four states, although we don't
419     deactivate (lvchange -an) them when shutdown, so STATUS_EXISTING
420     should not be seen for our devices.
421
422     """
423     result = utils.RunCmd(["lvs", "--noheadings", "-olv_attr", self.dev_path])
424     if result.failed:
425       logger.Error("Can't display lv: %s" % result.fail_reason)
426       return self.STATUS_UNKNOWN
427     out = result.stdout.strip()
428     # format: type/permissions/alloc/fixed_minor/state/open
429     if len(out) != 6:
430       return self.STATUS_UNKNOWN
431     #writable = (out[1] == "w")
432     active = (out[4] == "a")
433     online = (out[5] == "o")
434     if online:
435       retval = self.STATUS_ONLINE
436     elif active:
437       retval = self.STATUS_STANDBY
438     else:
439       retval = self.STATUS_EXISTING
440
441     return retval
442
443
444   def Open(self, force=False):
445     """Make the device ready for I/O.
446
447     This is a no-op for the LV device type.
448
449     """
450     return True
451
452
453   def Close(self):
454     """Notifies that the device will no longer be used for I/O.
455
456     This is a no-op for the LV device type.
457
458     """
459     return True
460
461
462   def Snapshot(self, size):
463     """Create a snapshot copy of an lvm block device.
464
465     """
466     snap_name = self._lv_name + ".snap"
467
468     # remove existing snapshot if found
469     snap = LogicalVolume((self._vg_name, snap_name), None)
470     snap.Remove()
471
472     pvs_info = self.GetPVInfo(self._vg_name)
473     if not pvs_info:
474       raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
475                                     self._vg_name)
476     pvs_info.sort()
477     pvs_info.reverse()
478     free_size, pv_name = pvs_info[0]
479     if free_size < size:
480       raise errors.BlockDeviceError("Not enough free space: required %s,"
481                                     " available %s" % (size, free_size))
482
483     result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
484                            "-n%s" % snap_name, self.dev_path])
485     if result.failed:
486       raise errors.BlockDeviceError("command: %s error: %s" %
487                                     (result.cmd, result.fail_reason))
488
489     return snap_name
490
491
492   def SetInfo(self, text):
493     """Update metadata with info text.
494
495     """
496     BlockDev.SetInfo(self, text)
497
498     # Replace invalid characters
499     text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
500     text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
501
502     # Only up to 128 characters are allowed
503     text = text[:128]
504
505     result = utils.RunCmd(["lvchange", "--addtag", text,
506                            self.dev_path])
507     if result.failed:
508       raise errors.BlockDeviceError("Command: %s error: %s" %
509                                     (result.cmd, result.fail_reason))
510
511
512 class MDRaid1(BlockDev):
513   """raid1 device implemented via md.
514
515   """
516   def __init__(self, unique_id, children):
517     super(MDRaid1, self).__init__(unique_id, children)
518     self.major = 9
519     self.Attach()
520
521
522   def Attach(self):
523     """Find an array which matches our config and attach to it.
524
525     This tries to find a MD array which has the same UUID as our own.
526
527     """
528     minor = self._FindMDByUUID(self.unique_id)
529     if minor is not None:
530       self._SetFromMinor(minor)
531     else:
532       self.minor = None
533       self.dev_path = None
534
535     return (minor is not None)
536
537
538   @staticmethod
539   def _GetUsedDevs():
540     """Compute the list of in-use MD devices.
541
542     It doesn't matter if the used device have other raid level, just
543     that they are in use.
544
545     """
546     mdstat = open("/proc/mdstat", "r")
547     data = mdstat.readlines()
548     mdstat.close()
549
550     used_md = {}
551     valid_line = re.compile("^md([0-9]+) : .*$")
552     for line in data:
553       match = valid_line.match(line)
554       if match:
555         md_no = int(match.group(1))
556         used_md[md_no] = line
557
558     return used_md
559
560
561   @staticmethod
562   def _GetDevInfo(minor):
563     """Get info about a MD device.
564
565     Currently only uuid is returned.
566
567     """
568     result = utils.RunCmd(["mdadm", "-D", "/dev/md%d" % minor])
569     if result.failed:
570       logger.Error("Can't display md: %s" % result.fail_reason)
571       return None
572     retval = {}
573     for line in result.stdout.splitlines():
574       line = line.strip()
575       kv = line.split(" : ", 1)
576       if kv:
577         if kv[0] == "UUID":
578           retval["uuid"] = kv[1].split()[0]
579         elif kv[0] == "State":
580           retval["state"] = kv[1].split(", ")
581     return retval
582
583
584   @staticmethod
585   def _FindUnusedMinor():
586     """Compute an unused MD minor.
587
588     This code assumes that there are 256 minors only.
589
590     """
591     used_md = MDRaid1._GetUsedDevs()
592     i = 0
593     while i < 256:
594       if i not in used_md:
595         break
596       i += 1
597     if i == 256:
598       logger.Error("Critical: Out of md minor numbers.")
599       raise errors.BlockDeviceError("Can't find a free MD minor")
600     return i
601
602
603   @classmethod
604   def _FindMDByUUID(cls, uuid):
605     """Find the minor of an MD array with a given UUID.
606
607     """
608     md_list = cls._GetUsedDevs()
609     for minor in md_list:
610       info = cls._GetDevInfo(minor)
611       if info and info["uuid"] == uuid:
612         return minor
613     return None
614
615
616   @staticmethod
617   def _ZeroSuperblock(dev_path):
618     """Zero the possible locations for an MD superblock.
619
620     The zero-ing can't be done via ``mdadm --zero-superblock`` as that
621     fails in versions 2.x with the same error code as non-writable
622     device.
623
624     The superblocks are located at (negative values are relative to
625     the end of the block device):
626       - -128k to end for version 0.90 superblock
627       - -8k to -12k for version 1.0 superblock (included in the above)
628       - 0k to 4k for version 1.1 superblock
629       - 4k to 8k for version 1.2 superblock
630
631     To cover all situations, the zero-ing will be:
632       - 0k to 128k
633       - -128k to end
634
635     As such, the minimum device size must be 128k, otherwise we'll get
636     I/O errors.
637
638     Note that this function depends on the fact that one can open,
639     read and write block devices normally.
640
641     """
642     overwrite_size = 128 * 1024
643     empty_buf = '\0' * overwrite_size
644     fd = open(dev_path, "r+")
645     try:
646       fd.seek(0, 0)
647       p1 = fd.tell()
648       fd.write(empty_buf)
649       p2 = fd.tell()
650       logger.Debug("Zeroed %s from %d to %d" % (dev_path, p1, p2))
651       fd.seek(-overwrite_size, 2)
652       p1 = fd.tell()
653       fd.write(empty_buf)
654       p2 = fd.tell()
655       logger.Debug("Zeroed %s from %d to %d" % (dev_path, p1, p2))
656     finally:
657       fd.close()
658
659   @classmethod
660   def Create(cls, unique_id, children, size):
661     """Create a new MD raid1 array.
662
663     """
664     if not isinstance(children, (tuple, list)):
665       raise ValueError("Invalid setup data for MDRaid1 dev: %s" %
666                        str(children))
667     for i in children:
668       if not isinstance(i, BlockDev):
669         raise ValueError("Invalid member in MDRaid1 dev: %s" % type(i))
670     for i in children:
671       try:
672         cls._ZeroSuperblock(i.dev_path)
673       except EnvironmentError, err:
674         logger.Error("Can't zero superblock for %s: %s" %
675                      (i.dev_path, str(err)))
676         return None
677     minor = cls._FindUnusedMinor()
678     result = utils.RunCmd(["mdadm", "--create", "/dev/md%d" % minor,
679                            "--auto=yes", "--force", "-l1",
680                            "-n%d" % len(children)] +
681                           [dev.dev_path for dev in children])
682
683     if result.failed:
684       logger.Error("Can't create md: %s: %s" % (result.fail_reason,
685                                                 result.output))
686       return None
687     info = cls._GetDevInfo(minor)
688     if not info or not "uuid" in info:
689       logger.Error("Wrong information returned from mdadm -D: %s" % str(info))
690       return None
691     return MDRaid1(info["uuid"], children)
692
693
694   def Remove(self):
695     """Stub remove function for MD RAID 1 arrays.
696
697     We don't remove the superblock right now. Mark a to do.
698
699     """
700     #TODO: maybe zero superblock on child devices?
701     return self.Shutdown()
702
703
704   def AddChild(self, device):
705     """Add a new member to the md raid1.
706
707     """
708     if self.minor is None and not self.Attach():
709       raise errors.BlockDeviceError("Can't attach to device")
710     if device.dev_path is None:
711       raise errors.BlockDeviceError("New child is not initialised")
712     result = utils.RunCmd(["mdadm", "-a", self.dev_path, device.dev_path])
713     if result.failed:
714       raise errors.BlockDeviceError("Failed to add new device to array: %s" %
715                                     result.output)
716     new_len = len(self._children) + 1
717     result = utils.RunCmd(["mdadm", "--grow", self.dev_path, "-n", new_len])
718     if result.failed:
719       raise errors.BlockDeviceError("Can't grow md array: %s" %
720                                     result.output)
721     self._children.append(device)
722
723
724   def RemoveChild(self, dev_path):
725     """Remove member from the md raid1.
726
727     """
728     if self.minor is None and not self.Attach():
729       raise errors.BlockDeviceError("Can't attach to device")
730     if len(self._children) == 1:
731       raise errors.BlockDeviceError("Can't reduce member when only one"
732                                     " child left")
733     for device in self._children:
734       if device.dev_path == dev_path:
735         break
736     else:
737       raise errors.BlockDeviceError("Can't find child with this path")
738     new_len = len(self._children) - 1
739     result = utils.RunCmd(["mdadm", "-f", self.dev_path, dev_path])
740     if result.failed:
741       raise errors.BlockDeviceError("Failed to mark device as failed: %s" %
742                                     result.output)
743
744     # it seems here we need a short delay for MD to update its
745     # superblocks
746     time.sleep(0.5)
747     result = utils.RunCmd(["mdadm", "-r", self.dev_path, dev_path])
748     if result.failed:
749       raise errors.BlockDeviceError("Failed to remove device from array:"
750                                         " %s" % result.output)
751     result = utils.RunCmd(["mdadm", "--grow", "--force", self.dev_path,
752                            "-n", new_len])
753     if result.failed:
754       raise errors.BlockDeviceError("Can't shrink md array: %s" %
755                                     result.output)
756     self._children.remove(device)
757
758
759   def GetStatus(self):
760     """Return the status of the device.
761
762     """
763     self.Attach()
764     if self.minor is None:
765       retval = self.STATUS_UNKNOWN
766     else:
767       retval = self.STATUS_ONLINE
768     return retval
769
770
771   def _SetFromMinor(self, minor):
772     """Set our parameters based on the given minor.
773
774     This sets our minor variable and our dev_path.
775
776     """
777     self.minor = minor
778     self.dev_path = "/dev/md%d" % minor
779
780
781   def Assemble(self):
782     """Assemble the MD device.
783
784     At this point we should have:
785       - list of children devices
786       - uuid
787
788     """
789     result = super(MDRaid1, self).Assemble()
790     if not result:
791       return result
792     md_list = self._GetUsedDevs()
793     for minor in md_list:
794       info = self._GetDevInfo(minor)
795       if info and info["uuid"] == self.unique_id:
796         self._SetFromMinor(minor)
797         logger.Info("MD array %s already started" % str(self))
798         return True
799     free_minor = self._FindUnusedMinor()
800     result = utils.RunCmd(["mdadm", "-A", "--auto=yes", "--uuid",
801                            self.unique_id, "/dev/md%d" % free_minor] +
802                           [bdev.dev_path for bdev in self._children])
803     if result.failed:
804       logger.Error("Can't assemble MD array: %s: %s" %
805                    (result.fail_reason, result.output))
806       self.minor = None
807     else:
808       self.minor = free_minor
809     return not result.failed
810
811
812   def Shutdown(self):
813     """Tear down the MD array.
814
815     This does a 'mdadm --stop' so after this command, the array is no
816     longer available.
817
818     """
819     if self.minor is None and not self.Attach():
820       logger.Info("MD object not attached to a device")
821       return True
822
823     result = utils.RunCmd(["mdadm", "--stop", "/dev/md%d" % self.minor])
824     if result.failed:
825       logger.Error("Can't stop MD array: %s" % result.fail_reason)
826       return False
827     self.minor = None
828     self.dev_path = None
829     return True
830
831
832   def SetSyncSpeed(self, kbytes):
833     """Set the maximum sync speed for the MD array.
834
835     """
836     result = super(MDRaid1, self).SetSyncSpeed(kbytes)
837     if self.minor is None:
838       logger.Error("MD array not attached to a device")
839       return False
840     f = open("/sys/block/md%d/md/sync_speed_max" % self.minor, "w")
841     try:
842       f.write("%d" % kbytes)
843     finally:
844       f.close()
845     f = open("/sys/block/md%d/md/sync_speed_min" % self.minor, "w")
846     try:
847       f.write("%d" % (kbytes/2))
848     finally:
849       f.close()
850     return result
851
852
853   def GetSyncStatus(self):
854     """Returns the sync status of the device.
855
856     Returns:
857      (sync_percent, estimated_time)
858
859     If sync_percent is None, it means all is ok
860     If estimated_time is None, it means we can't esimate
861     the time needed, otherwise it's the time left in seconds
862
863     """
864     if self.minor is None and not self.Attach():
865       raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
866     dev_info = self._GetDevInfo(self.minor)
867     is_clean = ("state" in dev_info and
868                 len(dev_info["state"]) == 1 and
869                 dev_info["state"][0] in ("clean", "active"))
870     sys_path = "/sys/block/md%s/md/" % self.minor
871     f = file(sys_path + "sync_action")
872     sync_status = f.readline().strip()
873     f.close()
874     if sync_status == "idle":
875       return None, None, not is_clean
876     f = file(sys_path + "sync_completed")
877     sync_completed = f.readline().strip().split(" / ")
878     f.close()
879     if len(sync_completed) != 2:
880       return 0, None, not is_clean
881     sync_done, sync_total = [float(i) for i in sync_completed]
882     sync_percent = 100.0*sync_done/sync_total
883     f = file(sys_path + "sync_speed")
884     sync_speed_k = int(f.readline().strip())
885     if sync_speed_k == 0:
886       time_est = None
887     else:
888       time_est = (sync_total - sync_done) / 2 / sync_speed_k
889     return sync_percent, time_est, not is_clean
890
891
892   def Open(self, force=False):
893     """Make the device ready for I/O.
894
895     This is a no-op for the MDRaid1 device type, although we could use
896     the 2.6.18's new array_state thing.
897
898     """
899     return True
900
901
902   def Close(self):
903     """Notifies that the device will no longer be used for I/O.
904
905     This is a no-op for the MDRaid1 device type, but see comment for
906     `Open()`.
907
908     """
909     return True
910
911
912 class BaseDRBD(BlockDev):
913   """Base DRBD class.
914
915   This class contains a few bits of common functionality between the
916   0.7 and 8.x versions of DRBD.
917
918   """
919   _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
920                            r" \(api:(\d+)/proto:(\d+)\)")
921   _DRBD_MAJOR = 147
922   _ST_UNCONFIGURED = "Unconfigured"
923   _ST_WFCONNECTION = "WFConnection"
924   _ST_CONNECTED = "Connected"
925
926   @staticmethod
927   def _GetProcData():
928     """Return data from /proc/drbd.
929
930     """
931     stat = open("/proc/drbd", "r")
932     try:
933       data = stat.read().splitlines()
934     finally:
935       stat.close()
936     if not data:
937       raise errors.BlockDeviceError("Can't read any data from /proc/drbd")
938     return data
939
940   @staticmethod
941   def _MassageProcData(data):
942     """Transform the output of _GetProdData into a nicer form.
943
944     Returns:
945       a dictionary of minor: joined lines from /proc/drbd for that minor
946
947     """
948     lmatch = re.compile("^ *([0-9]+):.*$")
949     results = {}
950     old_minor = old_line = None
951     for line in data:
952       lresult = lmatch.match(line)
953       if lresult is not None:
954         if old_minor is not None:
955           results[old_minor] = old_line
956         old_minor = int(lresult.group(1))
957         old_line = line
958       else:
959         if old_minor is not None:
960           old_line += " " + line.strip()
961     # add last line
962     if old_minor is not None:
963       results[old_minor] = old_line
964     return results
965
966   @classmethod
967   def _GetVersion(cls):
968     """Return the DRBD version.
969
970     This will return a list [k_major, k_minor, k_point, api, proto].
971
972     """
973     proc_data = cls._GetProcData()
974     first_line = proc_data[0].strip()
975     version = cls._VERSION_RE.match(first_line)
976     if not version:
977       raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
978                                     first_line)
979     return [int(val) for val in version.groups()]
980
981   @staticmethod
982   def _DevPath(minor):
983     """Return the path to a drbd device for a given minor.
984
985     """
986     return "/dev/drbd%d" % minor
987
988   @classmethod
989   def _GetUsedDevs(cls):
990     """Compute the list of used DRBD devices.
991
992     """
993     data = cls._GetProcData()
994
995     used_devs = {}
996     valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
997     for line in data:
998       match = valid_line.match(line)
999       if not match:
1000         continue
1001       minor = int(match.group(1))
1002       state = match.group(2)
1003       if state == cls._ST_UNCONFIGURED:
1004         continue
1005       used_devs[minor] = state, line
1006
1007     return used_devs
1008
1009   def _SetFromMinor(self, minor):
1010     """Set our parameters based on the given minor.
1011
1012     This sets our minor variable and our dev_path.
1013
1014     """
1015     if minor is None:
1016       self.minor = self.dev_path = None
1017     else:
1018       self.minor = minor
1019       self.dev_path = self._DevPath(minor)
1020
1021   @staticmethod
1022   def _CheckMetaSize(meta_device):
1023     """Check if the given meta device looks like a valid one.
1024
1025     This currently only check the size, which must be around
1026     128MiB.
1027
1028     """
1029     result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1030     if result.failed:
1031       logger.Error("Failed to get device size: %s" % result.fail_reason)
1032       return False
1033     try:
1034       sectors = int(result.stdout)
1035     except ValueError:
1036       logger.Error("Invalid output from blockdev: '%s'" % result.stdout)
1037       return False
1038     bytes = sectors * 512
1039     if bytes < 128 * 1024 * 1024: # less than 128MiB
1040       logger.Error("Meta device too small (%.2fMib)" % (bytes / 1024 / 1024))
1041       return False
1042     if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
1043       logger.Error("Meta device too big (%.2fMiB)" % (bytes / 1024 / 1024))
1044       return False
1045     return True
1046
1047
1048 class DRBDev(BaseDRBD):
1049   """DRBD block device.
1050
1051   This implements the local host part of the DRBD device, i.e. it
1052   doesn't do anything to the supposed peer. If you need a fully
1053   connected DRBD pair, you need to use this class on both hosts.
1054
1055   The unique_id for the drbd device is the (local_ip, local_port,
1056   remote_ip, remote_port) tuple, and it must have two children: the
1057   data device and the meta_device. The meta device is checked for
1058   valid size and is zeroed on create.
1059
1060   """
1061   def __init__(self, unique_id, children):
1062     super(DRBDev, self).__init__(unique_id, children)
1063     self.major = self._DRBD_MAJOR
1064     [kmaj, kmin, kfix, api, proto] = self._GetVersion()
1065     if kmaj != 0 and kmin != 7:
1066       raise errors.BlockDeviceError("Mismatch in DRBD kernel version and"
1067                                     " requested ganeti usage: kernel is"
1068                                     " %s.%s, ganeti wants 0.7" % (kmaj, kmin))
1069
1070     if len(children) != 2:
1071       raise ValueError("Invalid configuration data %s" % str(children))
1072     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
1073       raise ValueError("Invalid configuration data %s" % str(unique_id))
1074     self._lhost, self._lport, self._rhost, self._rport = unique_id
1075     self.Attach()
1076
1077   @classmethod
1078   def _FindUnusedMinor(cls):
1079     """Find an unused DRBD device.
1080
1081     """
1082     data = cls._GetProcData()
1083
1084     valid_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
1085     for line in data:
1086       match = valid_line.match(line)
1087       if match:
1088         return int(match.group(1))
1089     logger.Error("Error: no free drbd minors!")
1090     raise errors.BlockDeviceError("Can't find a free DRBD minor")
1091
1092   @classmethod
1093   def _GetDevInfo(cls, minor):
1094     """Get details about a given DRBD minor.
1095
1096     This return, if available, the local backing device in (major,
1097     minor) formant and the local and remote (ip, port) information.
1098
1099     """
1100     data = {}
1101     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1102     if result.failed:
1103       logger.Error("Can't display the drbd config: %s" % result.fail_reason)
1104       return data
1105     out = result.stdout
1106     if out == "Not configured\n":
1107       return data
1108     for line in out.splitlines():
1109       if "local_dev" not in data:
1110         match = re.match("^Lower device: ([0-9]+):([0-9]+) .*$", line)
1111         if match:
1112           data["local_dev"] = (int(match.group(1)), int(match.group(2)))
1113           continue
1114       if "meta_dev" not in data:
1115         match = re.match("^Meta device: (([0-9]+):([0-9]+)|internal).*$", line)
1116         if match:
1117           if match.group(2) is not None and match.group(3) is not None:
1118             # matched on the major/minor
1119             data["meta_dev"] = (int(match.group(2)), int(match.group(3)))
1120           else:
1121             # matched on the "internal" string
1122             data["meta_dev"] = match.group(1)
1123             # in this case, no meta_index is in the output
1124             data["meta_index"] = -1
1125           continue
1126       if "meta_index" not in data:
1127         match = re.match("^Meta index: ([0-9]+).*$", line)
1128         if match:
1129           data["meta_index"] = int(match.group(1))
1130           continue
1131       if "local_addr" not in data:
1132         match = re.match("^Local address: ([0-9.]+):([0-9]+)$", line)
1133         if match:
1134           data["local_addr"] = (match.group(1), int(match.group(2)))
1135           continue
1136       if "remote_addr" not in data:
1137         match = re.match("^Remote address: ([0-9.]+):([0-9]+)$", line)
1138         if match:
1139           data["remote_addr"] = (match.group(1), int(match.group(2)))
1140           continue
1141     return data
1142
1143
1144   def _MatchesLocal(self, info):
1145     """Test if our local config matches with an existing device.
1146
1147     The parameter should be as returned from `_GetDevInfo()`. This
1148     method tests if our local backing device is the same as the one in
1149     the info parameter, in effect testing if we look like the given
1150     device.
1151
1152     """
1153     if not ("local_dev" in info and "meta_dev" in info and
1154             "meta_index" in info):
1155       return False
1156
1157     backend = self._children[0]
1158     if backend is not None:
1159       retval = (info["local_dev"] == (backend.major, backend.minor))
1160     else:
1161       retval = (info["local_dev"] == (0, 0))
1162     meta = self._children[1]
1163     if meta is not None:
1164       retval = retval and (info["meta_dev"] == (meta.major, meta.minor))
1165       retval = retval and (info["meta_index"] == 0)
1166     else:
1167       retval = retval and (info["meta_dev"] == "internal" and
1168                            info["meta_index"] == -1)
1169     return retval
1170
1171
1172   def _MatchesNet(self, info):
1173     """Test if our network config matches with an existing device.
1174
1175     The parameter should be as returned from `_GetDevInfo()`. This
1176     method tests if our network configuration is the same as the one
1177     in the info parameter, in effect testing if we look like the given
1178     device.
1179
1180     """
1181     if (((self._lhost is None and not ("local_addr" in info)) and
1182          (self._rhost is None and not ("remote_addr" in info)))):
1183       return True
1184
1185     if self._lhost is None:
1186       return False
1187
1188     if not ("local_addr" in info and
1189             "remote_addr" in info):
1190       return False
1191
1192     retval = (info["local_addr"] == (self._lhost, self._lport))
1193     retval = (retval and
1194               info["remote_addr"] == (self._rhost, self._rport))
1195     return retval
1196
1197
1198   @classmethod
1199   def _AssembleLocal(cls, minor, backend, meta):
1200     """Configure the local part of a DRBD device.
1201
1202     This is the first thing that must be done on an unconfigured DRBD
1203     device. And it must be done only once.
1204
1205     """
1206     if not cls._CheckMetaSize(meta):
1207       return False
1208     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
1209                            backend, meta, "0", "-e", "detach"])
1210     if result.failed:
1211       logger.Error("Can't attach local disk: %s" % result.output)
1212     return not result.failed
1213
1214
1215   @classmethod
1216   def _ShutdownLocal(cls, minor):
1217     """Detach from the local device.
1218
1219     I/Os will continue to be served from the remote device. If we
1220     don't have a remote device, this operation will fail.
1221
1222     """
1223     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1224     if result.failed:
1225       logger.Error("Can't detach local device: %s" % result.output)
1226     return not result.failed
1227
1228
1229   @staticmethod
1230   def _ShutdownAll(minor):
1231     """Deactivate the device.
1232
1233     This will, of course, fail if the device is in use.
1234
1235     """
1236     result = utils.RunCmd(["drbdsetup", DRBDev._DevPath(minor), "down"])
1237     if result.failed:
1238       logger.Error("Can't shutdown drbd device: %s" % result.output)
1239     return not result.failed
1240
1241
1242   @classmethod
1243   def _AssembleNet(cls, minor, net_info, protocol):
1244     """Configure the network part of the device.
1245
1246     This operation can be, in theory, done multiple times, but there
1247     have been cases (in lab testing) in which the network part of the
1248     device had become stuck and couldn't be shut down because activity
1249     from the new peer (also stuck) triggered a timer re-init and
1250     needed remote peer interface shutdown in order to clear. So please
1251     don't change online the net config.
1252
1253     """
1254     lhost, lport, rhost, rport = net_info
1255     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "net",
1256                            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport),
1257                            protocol])
1258     if result.failed:
1259       logger.Error("Can't setup network for dbrd device: %s" %
1260                    result.fail_reason)
1261       return False
1262
1263     timeout = time.time() + 10
1264     ok = False
1265     while time.time() < timeout:
1266       info = cls._GetDevInfo(minor)
1267       if not "local_addr" in info or not "remote_addr" in info:
1268         time.sleep(1)
1269         continue
1270       if (info["local_addr"] != (lhost, lport) or
1271           info["remote_addr"] != (rhost, rport)):
1272         time.sleep(1)
1273         continue
1274       ok = True
1275       break
1276     if not ok:
1277       logger.Error("Timeout while configuring network")
1278       return False
1279     return True
1280
1281
1282   @classmethod
1283   def _ShutdownNet(cls, minor):
1284     """Disconnect from the remote peer.
1285
1286     This fails if we don't have a local device.
1287
1288     """
1289     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1290     logger.Error("Can't shutdown network: %s" % result.output)
1291     return not result.failed
1292
1293
1294   def Assemble(self):
1295     """Assemble the drbd.
1296
1297     Method:
1298       - if we have a local backing device, we bind to it by:
1299         - checking the list of used drbd devices
1300         - check if the local minor use of any of them is our own device
1301         - if yes, abort?
1302         - if not, bind
1303       - if we have a local/remote net info:
1304         - redo the local backing device step for the remote device
1305         - check if any drbd device is using the local port,
1306           if yes abort
1307         - check if any remote drbd device is using the remote
1308           port, if yes abort (for now)
1309         - bind our net port
1310         - bind the remote net port
1311
1312     """
1313     self.Attach()
1314     if self.minor is not None:
1315       logger.Info("Already assembled")
1316       return True
1317
1318     result = super(DRBDev, self).Assemble()
1319     if not result:
1320       return result
1321
1322     minor = self._FindUnusedMinor()
1323     need_localdev_teardown = False
1324     if self._children[0]:
1325       result = self._AssembleLocal(minor, self._children[0].dev_path,
1326                                    self._children[1].dev_path)
1327       if not result:
1328         return False
1329       need_localdev_teardown = True
1330     if self._lhost and self._lport and self._rhost and self._rport:
1331       result = self._AssembleNet(minor,
1332                                  (self._lhost, self._lport,
1333                                   self._rhost, self._rport),
1334                                  "C")
1335       if not result:
1336         if need_localdev_teardown:
1337           # we will ignore failures from this
1338           logger.Error("net setup failed, tearing down local device")
1339           self._ShutdownAll(minor)
1340         return False
1341     self._SetFromMinor(minor)
1342     return True
1343
1344
1345   def Shutdown(self):
1346     """Shutdown the DRBD device.
1347
1348     """
1349     if self.minor is None and not self.Attach():
1350       logger.Info("DRBD device not attached to a device during Shutdown")
1351       return True
1352     if not self._ShutdownAll(self.minor):
1353       return False
1354     self.minor = None
1355     self.dev_path = None
1356     return True
1357
1358
1359   def Attach(self):
1360     """Find a DRBD device which matches our config and attach to it.
1361
1362     In case of partially attached (local device matches but no network
1363     setup), we perform the network attach. If successful, we re-test
1364     the attach if can return success.
1365
1366     """
1367     for minor in self._GetUsedDevs():
1368       info = self._GetDevInfo(minor)
1369       match_l = self._MatchesLocal(info)
1370       match_r = self._MatchesNet(info)
1371       if match_l and match_r:
1372         break
1373       if match_l and not match_r and "local_addr" not in info:
1374         res_r = self._AssembleNet(minor,
1375                                   (self._lhost, self._lport,
1376                                    self._rhost, self._rport),
1377                                   "C")
1378         if res_r and self._MatchesNet(self._GetDevInfo(minor)):
1379           break
1380     else:
1381       minor = None
1382
1383     self._SetFromMinor(minor)
1384     return minor is not None
1385
1386
1387   def Open(self, force=False):
1388     """Make the local state primary.
1389
1390     If the 'force' parameter is given, the '--do-what-I-say' parameter
1391     is given. Since this is a pottentialy dangerous operation, the
1392     force flag should be only given after creation, when it actually
1393     has to be given.
1394
1395     """
1396     if self.minor is None and not self.Attach():
1397       logger.Error("DRBD cannot attach to a device during open")
1398       return False
1399     cmd = ["drbdsetup", self.dev_path, "primary"]
1400     if force:
1401       cmd.append("--do-what-I-say")
1402     result = utils.RunCmd(cmd)
1403     if result.failed:
1404       logger.Error("Can't make drbd device primary: %s" % result.output)
1405       return False
1406     return True
1407
1408
1409   def Close(self):
1410     """Make the local state secondary.
1411
1412     This will, of course, fail if the device is in use.
1413
1414     """
1415     if self.minor is None and not self.Attach():
1416       logger.Info("Instance not attached to a device")
1417       raise errors.BlockDeviceError("Can't find device")
1418     result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1419     if result.failed:
1420       logger.Error("Can't switch drbd device to secondary: %s" % result.output)
1421       raise errors.BlockDeviceError("Can't switch drbd device to secondary")
1422
1423
1424   def SetSyncSpeed(self, kbytes):
1425     """Set the speed of the DRBD syncer.
1426
1427     """
1428     children_result = super(DRBDev, self).SetSyncSpeed(kbytes)
1429     if self.minor is None:
1430       logger.Info("Instance not attached to a device")
1431       return False
1432     result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
1433                            kbytes])
1434     if result.failed:
1435       logger.Error("Can't change syncer rate: %s " % result.fail_reason)
1436     return not result.failed and children_result
1437
1438
1439   def GetSyncStatus(self):
1440     """Returns the sync status of the device.
1441
1442     Returns:
1443      (sync_percent, estimated_time)
1444
1445     If sync_percent is None, it means all is ok
1446     If estimated_time is None, it means we can't esimate
1447     the time needed, otherwise it's the time left in seconds
1448
1449     """
1450     if self.minor is None and not self.Attach():
1451       raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1452     proc_info = self._MassageProcData(self._GetProcData())
1453     if self.minor not in proc_info:
1454       raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1455                                     self.minor)
1456     line = proc_info[self.minor]
1457     match = re.match("^.*sync'ed: *([0-9.]+)%.*"
1458                      " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
1459     if match:
1460       sync_percent = float(match.group(1))
1461       hours = int(match.group(2))
1462       minutes = int(match.group(3))
1463       seconds = int(match.group(4))
1464       est_time = hours * 3600 + minutes * 60 + seconds
1465     else:
1466       sync_percent = None
1467       est_time = None
1468     match = re.match("^ *[0-9]+: cs:([^ ]+).*$", line)
1469     if not match:
1470       raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
1471                                     self.minor)
1472     client_state = match.group(1)
1473     is_degraded = client_state != "Connected"
1474     return sync_percent, est_time, is_degraded
1475
1476
1477
1478
1479   def GetStatus(self):
1480     """Compute the status of the DRBD device
1481
1482     Note that DRBD devices don't have the STATUS_EXISTING state.
1483
1484     """
1485     if self.minor is None and not self.Attach():
1486       return self.STATUS_UNKNOWN
1487
1488     data = self._GetProcData()
1489     match = re.compile("^ *%d: cs:[^ ]+ st:(Primary|Secondary)/.*$" %
1490                        self.minor)
1491     for line in data:
1492       mresult = match.match(line)
1493       if mresult:
1494         break
1495     else:
1496       logger.Error("Can't find myself!")
1497       return self.STATUS_UNKNOWN
1498
1499     state = mresult.group(2)
1500     if state == "Primary":
1501       result = self.STATUS_ONLINE
1502     else:
1503       result = self.STATUS_STANDBY
1504
1505     return result
1506
1507
1508   @staticmethod
1509   def _ZeroDevice(device):
1510     """Zero a device.
1511
1512     This writes until we get ENOSPC.
1513
1514     """
1515     f = open(device, "w")
1516     buf = "\0" * 1048576
1517     try:
1518       while True:
1519         f.write(buf)
1520     except IOError, err:
1521       if err.errno != errno.ENOSPC:
1522         raise
1523
1524
1525   @classmethod
1526   def Create(cls, unique_id, children, size):
1527     """Create a new DRBD device.
1528
1529     Since DRBD devices are not created per se, just assembled, this
1530     function just zeroes the meta device.
1531
1532     """
1533     if len(children) != 2:
1534       raise errors.ProgrammerError("Invalid setup for the drbd device")
1535     meta = children[1]
1536     meta.Assemble()
1537     if not meta.Attach():
1538       raise errors.BlockDeviceError("Can't attach to meta device")
1539     if not cls._CheckMetaSize(meta.dev_path):
1540       raise errors.BlockDeviceError("Invalid meta device")
1541     logger.Info("Started zeroing device %s" % meta.dev_path)
1542     cls._ZeroDevice(meta.dev_path)
1543     logger.Info("Done zeroing device %s" % meta.dev_path)
1544     return cls(unique_id, children)
1545
1546
1547   def Remove(self):
1548     """Stub remove for DRBD devices.
1549
1550     """
1551     return self.Shutdown()
1552
1553 class DRBD8(BaseDRBD):
1554   """DRBD v8.x block device.
1555
1556   This implements the local host part of the DRBD device, i.e. it
1557   doesn't do anything to the supposed peer. If you need a fully
1558   connected DRBD pair, you need to use this class on both hosts.
1559
1560   The unique_id for the drbd device is the (local_ip, local_port,
1561   remote_ip, remote_port) tuple, and it must have two children: the
1562   data device and the meta_device. The meta device is checked for
1563   valid size and is zeroed on create.
1564
1565   """
1566   _DRBD_MAJOR = 147
1567   _MAX_MINORS = 255
1568   _PARSE_SHOW = None
1569
1570   def __init__(self, unique_id, children):
1571     super(DRBD8, self).__init__(unique_id, children)
1572     self.major = self._DRBD_MAJOR
1573     [kmaj, kmin, kfix, api, proto] = self._GetVersion()
1574     if kmaj != 8:
1575       raise errors.BlockDeviceError("Mismatch in DRBD kernel version and"
1576                                     " requested ganeti usage: kernel is"
1577                                     " %s.%s, ganeti wants 8.x" % (kmaj, kmin))
1578
1579     if len(children) != 2:
1580       raise ValueError("Invalid configuration data %s" % str(children))
1581     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
1582       raise ValueError("Invalid configuration data %s" % str(unique_id))
1583     self._lhost, self._lport, self._rhost, self._rport = unique_id
1584     self.Attach()
1585
1586   @classmethod
1587   def _InitMeta(cls, minor, dev_path):
1588     """Initialize a meta device.
1589
1590     This will not work if the given minor is in use.
1591
1592     """
1593     result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1594                            "v08", dev_path, "0", "create-md"])
1595     if result.failed:
1596       raise errors.BlockDeviceError("Can't initialize meta device: %s" %
1597                                     result.output)
1598
1599   @classmethod
1600   def _FindUnusedMinor(cls):
1601     """Find an unused DRBD device.
1602
1603     This is specific to 8.x as the minors are allocated dynamically,
1604     so non-existing numbers up to a max minor count are actually free.
1605
1606     """
1607     data = cls._GetProcData()
1608
1609     unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
1610     used_line = re.compile("^ *([0-9]+): cs:")
1611     highest = None
1612     for line in data:
1613       match = unused_line.match(line)
1614       if match:
1615         return int(match.group(1))
1616       match = used_line.match(line)
1617       if match:
1618         minor = int(match.group(1))
1619         highest = max(highest, minor)
1620     if highest is None: # there are no minors in use at all
1621       return 0
1622     if highest >= cls._MAX_MINORS:
1623       logger.Error("Error: no free drbd minors!")
1624       raise errors.BlockDeviceError("Can't find a free DRBD minor")
1625     return highest + 1
1626
1627   @classmethod
1628   def _IsValidMeta(cls, meta_device):
1629     """Check if the given meta device looks like a valid one.
1630
1631     """
1632     minor = cls._FindUnusedMinor()
1633     minor_path = cls._DevPath(minor)
1634     result = utils.RunCmd(["drbdmeta", minor_path,
1635                            "v08", meta_device, "0",
1636                            "dstate"])
1637     if result.failed:
1638       logger.Error("Invalid meta device %s: %s" % (meta_device, result.output))
1639       return False
1640     return True
1641
1642   @classmethod
1643   def _GetShowParser(cls):
1644     """Return a parser for `drbd show` output.
1645
1646     This will either create or return an already-create parser for the
1647     output of the command `drbd show`.
1648
1649     """
1650     if cls._PARSE_SHOW is not None:
1651       return cls._PARSE_SHOW
1652
1653     # pyparsing setup
1654     lbrace = pyp.Literal("{").suppress()
1655     rbrace = pyp.Literal("}").suppress()
1656     semi = pyp.Literal(";").suppress()
1657     # this also converts the value to an int
1658     number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t:(l, [int(t[0])]))
1659
1660     comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1661     defa = pyp.Literal("_is_default").suppress()
1662     dbl_quote = pyp.Literal('"').suppress()
1663
1664     keyword = pyp.Word(pyp.alphanums + '-')
1665
1666     # value types
1667     value = pyp.Word(pyp.alphanums + '_-/.:')
1668     quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1669     addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() +
1670                  number)
1671     # meta device, extended syntax
1672     meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1673                   number + pyp.Word(']').suppress())
1674
1675     # a statement
1676     stmt = (~rbrace + keyword + ~lbrace +
1677             (addr_port ^ value ^ quoted ^ meta_value) +
1678             pyp.Optional(defa) + semi +
1679             pyp.Optional(pyp.restOfLine).suppress())
1680
1681     # an entire section
1682     section_name = pyp.Word(pyp.alphas + '_')
1683     section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1684
1685     bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1686     bnf.ignore(comment)
1687
1688     cls._PARSE_SHOW = bnf
1689
1690     return bnf
1691
1692   @classmethod
1693   def _GetDevInfo(cls, minor):
1694     """Get details about a given DRBD minor.
1695
1696     This return, if available, the local backing device (as a path)
1697     and the local and remote (ip, port) information.
1698
1699     """
1700     data = {}
1701     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1702     if result.failed:
1703       logger.Error("Can't display the drbd config: %s" % result.fail_reason)
1704       return data
1705     out = result.stdout
1706     if not out:
1707       return data
1708
1709     bnf = cls._GetShowParser()
1710     # run pyparse
1711
1712     try:
1713       results = bnf.parseString(out)
1714     except pyp.ParseException, err:
1715       raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
1716                                     str(err))
1717
1718     # and massage the results into our desired format
1719     for section in results:
1720       sname = section[0]
1721       if sname == "_this_host":
1722         for lst in section[1:]:
1723           if lst[0] == "disk":
1724             data["local_dev"] = lst[1]
1725           elif lst[0] == "meta-disk":
1726             data["meta_dev"] = lst[1]
1727             data["meta_index"] = lst[2]
1728           elif lst[0] == "address":
1729             data["local_addr"] = tuple(lst[1:])
1730       elif sname == "_remote_host":
1731         for lst in section[1:]:
1732           if lst[0] == "address":
1733             data["remote_addr"] = tuple(lst[1:])
1734     return data
1735
1736   def _MatchesLocal(self, info):
1737     """Test if our local config matches with an existing device.
1738
1739     The parameter should be as returned from `_GetDevInfo()`. This
1740     method tests if our local backing device is the same as the one in
1741     the info parameter, in effect testing if we look like the given
1742     device.
1743
1744     """
1745     backend = self._children[0]
1746     if backend is not None:
1747       retval = (info["local_dev"] == backend.dev_path)
1748     else:
1749       retval = ("local_dev" not in info)
1750     meta = self._children[1]
1751     if meta is not None:
1752       retval = retval and (info["meta_dev"] == meta.dev_path)
1753       retval = retval and (info["meta_index"] == 0)
1754     else:
1755       retval = retval and ("meta_dev" not in info and
1756                            "meta_index" not in info)
1757     return retval
1758
1759   def _MatchesNet(self, info):
1760     """Test if our network config matches with an existing device.
1761
1762     The parameter should be as returned from `_GetDevInfo()`. This
1763     method tests if our network configuration is the same as the one
1764     in the info parameter, in effect testing if we look like the given
1765     device.
1766
1767     """
1768     if (((self._lhost is None and not ("local_addr" in info)) and
1769          (self._rhost is None and not ("remote_addr" in info)))):
1770       return True
1771
1772     if self._lhost is None:
1773       return False
1774
1775     if not ("local_addr" in info and
1776             "remote_addr" in info):
1777       return False
1778
1779     retval = (info["local_addr"] == (self._lhost, self._lport))
1780     retval = (retval and
1781               info["remote_addr"] == (self._rhost, self._rport))
1782     return retval
1783
1784   @classmethod
1785   def _AssembleLocal(cls, minor, backend, meta):
1786     """Configure the local part of a DRBD device.
1787
1788     This is the first thing that must be done on an unconfigured DRBD
1789     device. And it must be done only once.
1790
1791     """
1792     if not cls._IsValidMeta(meta):
1793       return False
1794     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
1795                            backend, meta, "0", "-e", "detach",
1796                            "--create-device"])
1797     if result.failed:
1798       logger.Error("Can't attach local disk: %s" % result.output)
1799     return not result.failed
1800
1801   @classmethod
1802   def _AssembleNet(cls, minor, net_info, protocol,
1803                    dual_pri=False, hmac=None, secret=None):
1804     """Configure the network part of the device.
1805
1806     """
1807     lhost, lport, rhost, rport = net_info
1808     args = ["drbdsetup", cls._DevPath(minor), "net",
1809             "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1810             "-A", "discard-zero-changes",
1811             "-B", "consensus",
1812             ]
1813     if dual_pri:
1814       args.append("-m")
1815     if hmac and secret:
1816       args.extend(["-a", hmac, "-x", secret])
1817     result = utils.RunCmd(args)
1818     if result.failed:
1819       logger.Error("Can't setup network for dbrd device: %s" %
1820                    result.fail_reason)
1821       return False
1822
1823     timeout = time.time() + 10
1824     ok = False
1825     while time.time() < timeout:
1826       info = cls._GetDevInfo(minor)
1827       if not "local_addr" in info or not "remote_addr" in info:
1828         time.sleep(1)
1829         continue
1830       if (info["local_addr"] != (lhost, lport) or
1831           info["remote_addr"] != (rhost, rport)):
1832         time.sleep(1)
1833         continue
1834       ok = True
1835       break
1836     if not ok:
1837       logger.Error("Timeout while configuring network")
1838       return False
1839     return True
1840
1841   def SetSyncSpeed(self, kbytes):
1842     """Set the speed of the DRBD syncer.
1843
1844     """
1845     children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1846     if self.minor is None:
1847       logger.Info("Instance not attached to a device")
1848       return False
1849     result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
1850                            kbytes])
1851     if result.failed:
1852       logger.Error("Can't change syncer rate: %s " % result.fail_reason)
1853     return not result.failed and children_result
1854
1855   def GetSyncStatus(self):
1856     """Returns the sync status of the device.
1857
1858     Returns:
1859      (sync_percent, estimated_time)
1860
1861     If sync_percent is None, it means all is ok
1862     If estimated_time is None, it means we can't esimate
1863     the time needed, otherwise it's the time left in seconds
1864
1865     """
1866     if self.minor is None and not self.Attach():
1867       raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1868     proc_info = self._MassageProcData(self._GetProcData())
1869     if self.minor not in proc_info:
1870       raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1871                                     self.minor)
1872     line = proc_info[self.minor]
1873     match = re.match("^.*sync'ed: *([0-9.]+)%.*"
1874                      " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
1875     if match:
1876       sync_percent = float(match.group(1))
1877       hours = int(match.group(2))
1878       minutes = int(match.group(3))
1879       seconds = int(match.group(4))
1880       est_time = hours * 3600 + minutes * 60 + seconds
1881     else:
1882       sync_percent = None
1883       est_time = None
1884     match = re.match("^ *[0-9]+: cs:([^ ]+).*$", line)
1885     if not match:
1886       raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
1887                                     self.minor)
1888     client_state = match.group(1)
1889     is_degraded = client_state != "Connected"
1890     return sync_percent, est_time, is_degraded
1891
1892   def GetStatus(self):
1893     """Compute the status of the DRBD device
1894
1895     Note that DRBD devices don't have the STATUS_EXISTING state.
1896
1897     """
1898     if self.minor is None and not self.Attach():
1899       return self.STATUS_UNKNOWN
1900
1901     data = self._GetProcData()
1902     match = re.compile("^ *%d: cs:[^ ]+ st:(Primary|Secondary)/.*$" %
1903                        self.minor)
1904     for line in data:
1905       mresult = match.match(line)
1906       if mresult:
1907         break
1908     else:
1909       logger.Error("Can't find myself!")
1910       return self.STATUS_UNKNOWN
1911
1912     state = mresult.group(2)
1913     if state == "Primary":
1914       result = self.STATUS_ONLINE
1915     else:
1916       result = self.STATUS_STANDBY
1917
1918     return result
1919
1920   def Open(self, force=False):
1921     """Make the local state primary.
1922
1923     If the 'force' parameter is given, the '--do-what-I-say' parameter
1924     is given. Since this is a pottentialy dangerous operation, the
1925     force flag should be only given after creation, when it actually
1926     has to be given.
1927
1928     """
1929     if self.minor is None and not self.Attach():
1930       logger.Error("DRBD cannot attach to a device during open")
1931       return False
1932     cmd = ["drbdsetup", self.dev_path, "primary"]
1933     if force:
1934       cmd.append("-o")
1935     result = utils.RunCmd(cmd)
1936     if result.failed:
1937       logger.Error("Can't make drbd device primary: %s" % result.output)
1938       return False
1939     return True
1940
1941   def Close(self):
1942     """Make the local state secondary.
1943
1944     This will, of course, fail if the device is in use.
1945
1946     """
1947     if self.minor is None and not self.Attach():
1948       logger.Info("Instance not attached to a device")
1949       raise errors.BlockDeviceError("Can't find device")
1950     result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1951     if result.failed:
1952       logger.Error("Can't switch drbd device to secondary: %s" % result.output)
1953       raise errors.BlockDeviceError("Can't switch drbd device to secondary")
1954
1955   def Attach(self):
1956     """Find a DRBD device which matches our config and attach to it.
1957
1958     In case of partially attached (local device matches but no network
1959     setup), we perform the network attach. If successful, we re-test
1960     the attach if can return success.
1961
1962     """
1963     for minor in self._GetUsedDevs():
1964       info = self._GetDevInfo(minor)
1965       match_l = self._MatchesLocal(info)
1966       match_r = self._MatchesNet(info)
1967       if match_l and match_r:
1968         break
1969       if match_l and not match_r and "local_addr" not in info:
1970         res_r = self._AssembleNet(minor,
1971                                   (self._lhost, self._lport,
1972                                    self._rhost, self._rport),
1973                                   "C")
1974         if res_r and self._MatchesNet(self._GetDevInfo(minor)):
1975           break
1976     else:
1977       minor = None
1978
1979     self._SetFromMinor(minor)
1980     return minor is not None
1981
1982   def Assemble(self):
1983     """Assemble the drbd.
1984
1985     Method:
1986       - if we have a local backing device, we bind to it by:
1987         - checking the list of used drbd devices
1988         - check if the local minor use of any of them is our own device
1989         - if yes, abort?
1990         - if not, bind
1991       - if we have a local/remote net info:
1992         - redo the local backing device step for the remote device
1993         - check if any drbd device is using the local port,
1994           if yes abort
1995         - check if any remote drbd device is using the remote
1996           port, if yes abort (for now)
1997         - bind our net port
1998         - bind the remote net port
1999
2000     """
2001     self.Attach()
2002     if self.minor is not None:
2003       logger.Info("Already assembled")
2004       return True
2005
2006     result = super(DRBD8, self).Assemble()
2007     if not result:
2008       return result
2009
2010     minor = self._FindUnusedMinor()
2011     need_localdev_teardown = False
2012     if self._children[0]:
2013       result = self._AssembleLocal(minor, self._children[0].dev_path,
2014                                    self._children[1].dev_path)
2015       if not result:
2016         return False
2017       need_localdev_teardown = True
2018     if self._lhost and self._lport and self._rhost and self._rport:
2019       result = self._AssembleNet(minor,
2020                                  (self._lhost, self._lport,
2021                                   self._rhost, self._rport),
2022                                  "C")
2023       if not result:
2024         if need_localdev_teardown:
2025           # we will ignore failures from this
2026           logger.Error("net setup failed, tearing down local device")
2027           self._ShutdownAll(minor)
2028         return False
2029     self._SetFromMinor(minor)
2030     return True
2031
2032   @classmethod
2033   def _ShutdownAll(cls, minor):
2034     """Deactivate the device.
2035
2036     This will, of course, fail if the device is in use.
2037
2038     """
2039     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
2040     if result.failed:
2041       logger.Error("Can't shutdown drbd device: %s" % result.output)
2042     return not result.failed
2043
2044   def Shutdown(self):
2045     """Shutdown the DRBD device.
2046
2047     """
2048     if self.minor is None and not self.Attach():
2049       logger.Info("DRBD device not attached to a device during Shutdown")
2050       return True
2051     if not self._ShutdownAll(self.minor):
2052       return False
2053     self.minor = None
2054     self.dev_path = None
2055     return True
2056
2057   def Remove(self):
2058     """Stub remove for DRBD devices.
2059
2060     """
2061     return self.Shutdown()
2062
2063   @classmethod
2064   def Create(cls, unique_id, children, size):
2065     """Create a new DRBD8 device.
2066
2067     Since DRBD devices are not created per se, just assembled, this
2068     function only initializes the metadata.
2069
2070     """
2071     if len(children) != 2:
2072       raise errors.ProgrammerError("Invalid setup for the drbd device")
2073     meta = children[1]
2074     meta.Assemble()
2075     if not meta.Attach():
2076       raise errors.BlockDeviceError("Can't attach to meta device")
2077     if not cls._CheckMetaSize(meta.dev_path):
2078       raise errors.BlockDeviceError("Invalid meta device size")
2079     cls._InitMeta(cls._FindUnusedMinor(), meta.dev_path)
2080     if not cls._IsValidMeta(meta.dev_path):
2081       raise errors.BlockDeviceError("Cannot initalize meta device")
2082     return cls(unique_id, children)
2083
2084
2085 DEV_MAP = {
2086   constants.LD_LV: LogicalVolume,
2087   constants.LD_MD_R1: MDRaid1,
2088   constants.LD_DRBD7: DRBDev,
2089   constants.LD_DRBD8: DRBD8,
2090   }
2091
2092
2093 def FindDevice(dev_type, unique_id, children):
2094   """Search for an existing, assembled device.
2095
2096   This will succeed only if the device exists and is assembled, but it
2097   does not do any actions in order to activate the device.
2098
2099   """
2100   if dev_type not in DEV_MAP:
2101     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2102   device = DEV_MAP[dev_type](unique_id, children)
2103   if not device.Attach():
2104     return None
2105   return  device
2106
2107
2108 def AttachOrAssemble(dev_type, unique_id, children):
2109   """Try to attach or assemble an existing device.
2110
2111   This will attach to an existing assembled device or will assemble
2112   the device, as needed, to bring it fully up.
2113
2114   """
2115   if dev_type not in DEV_MAP:
2116     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2117   device = DEV_MAP[dev_type](unique_id, children)
2118   if not device.Attach():
2119     device.Assemble()
2120   if not device.Attach():
2121     raise errors.BlockDeviceError("Can't find a valid block device for"
2122                                   " %s/%s/%s" %
2123                                   (dev_type, unique_id, children))
2124   return device
2125
2126
2127 def Create(dev_type, unique_id, children, size):
2128   """Create a device.
2129
2130   """
2131   if dev_type not in DEV_MAP:
2132     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2133   device = DEV_MAP[dev_type].Create(unique_id, children, size)
2134   return device