cmdlib: Node whitelist support for allocation request
[ganeti-local] / lib / bdev.py
index 310ddf8..6c20b4b 100644 (file)
@@ -36,6 +36,7 @@ from ganeti import constants
 from ganeti import objects
 from ganeti import compat
 from ganeti import netutils
+from ganeti import pathutils
 
 
 # Size of reads in _CanReadDevice
@@ -74,6 +75,17 @@ def _ThrowError(msg, *args):
   raise errors.BlockDeviceError(msg)
 
 
+def _CheckResult(result):
+  """Throws an error if the given result is a failed one.
+
+  @param result: result from RunCmd
+
+  """
+  if result.failed:
+    _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
+                result.output)
+
+
 def _CanReadDevice(path):
   """Check if we can read from the given device.
 
@@ -88,6 +100,116 @@ def _CanReadDevice(path):
     return False
 
 
+def _GetForbiddenFileStoragePaths():
+  """Builds a list of path prefixes which shouldn't be used for file storage.
+
+  @rtype: frozenset
+
+  """
+  paths = set([
+    "/boot",
+    "/dev",
+    "/etc",
+    "/home",
+    "/proc",
+    "/root",
+    "/sys",
+    ])
+
+  for prefix in ["", "/usr", "/usr/local"]:
+    paths.update(map(lambda s: "%s/%s" % (prefix, s),
+                     ["bin", "lib", "lib32", "lib64", "sbin"]))
+
+  return frozenset(map(os.path.normpath, paths))
+
+
+def _ComputeWrongFileStoragePaths(paths,
+                                  _forbidden=_GetForbiddenFileStoragePaths()):
+  """Cross-checks a list of paths for prefixes considered bad.
+
+  Some paths, e.g. "/bin", should not be used for file storage.
+
+  @type paths: list
+  @param paths: List of paths to be checked
+  @rtype: list
+  @return: Sorted list of paths for which the user should be warned
+
+  """
+  def _Check(path):
+    return (not os.path.isabs(path) or
+            path in _forbidden or
+            filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
+
+  return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
+
+
+def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
+  """Returns a list of file storage paths whose prefix is considered bad.
+
+  See L{_ComputeWrongFileStoragePaths}.
+
+  """
+  return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
+
+
+def _CheckFileStoragePath(path, allowed):
+  """Checks if a path is in a list of allowed paths for file storage.
+
+  @type path: string
+  @param path: Path to check
+  @type allowed: list
+  @param allowed: List of allowed paths
+  @raise errors.FileStoragePathError: If the path is not allowed
+
+  """
+  if not os.path.isabs(path):
+    raise errors.FileStoragePathError("File storage path must be absolute,"
+                                      " got '%s'" % path)
+
+  for i in allowed:
+    if not os.path.isabs(i):
+      logging.info("Ignoring relative path '%s' for file storage", i)
+      continue
+
+    if utils.IsBelowDir(i, path):
+      break
+  else:
+    raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
+                                      " storage" % path)
+
+
+def _LoadAllowedFileStoragePaths(filename):
+  """Loads file containing allowed file storage paths.
+
+  @rtype: list
+  @return: List of allowed paths (can be an empty list)
+
+  """
+  try:
+    contents = utils.ReadFile(filename)
+  except EnvironmentError:
+    return []
+  else:
+    return utils.FilterEmptyLinesAndComments(contents)
+
+
+def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
+  """Checks if a path is allowed for file storage.
+
+  @type path: string
+  @param path: Path to check
+  @raise errors.FileStoragePathError: If the path is not allowed
+
+  """
+  allowed = _LoadAllowedFileStoragePaths(_filename)
+
+  if _ComputeWrongFileStoragePaths([path]):
+    raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
+                                      path)
+
+  _CheckFileStoragePath(path, allowed)
+
+
 class BlockDev(object):
   """Block device abstract class.
 
@@ -338,7 +460,7 @@ class BlockDev(object):
     for child in self._children:
       child.SetInfo(text)
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Grow the block device.
 
     @type amount: integer
@@ -346,6 +468,9 @@ class BlockDev(object):
     @type dryrun: boolean
     @param dryrun: whether to execute the operation in simulation mode
         only, without actually increasing the size
+    @param backingstore: whether to execute the operation on backing storage
+        only, or on "logical" storage only; e.g. DRBD is logical storage,
+        whereas LVM, file, RBD are backing storage
 
     """
     raise NotImplementedError
@@ -741,20 +866,33 @@ class LogicalVolume(BlockDev):
       _ThrowError("Not enough free space: required %s,"
                   " available %s", size, free_size)
 
-    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
-                           "-n%s" % snap_name, self.dev_path])
-    if result.failed:
-      _ThrowError("command: %s error: %s - %s",
-                  result.cmd, result.fail_reason, result.output)
+    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
+                               "-n%s" % snap_name, self.dev_path]))
 
     return (self._vg_name, snap_name)
 
+  def _RemoveOldInfo(self):
+    """Try to remove old tags from the lv.
+
+    """
+    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
+                           self.dev_path])
+    _CheckResult(result)
+
+    raw_tags = result.stdout.strip()
+    if raw_tags:
+      for tag in raw_tags.split(","):
+        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
+                                   tag.strip(), self.dev_path]))
+
   def SetInfo(self, text):
     """Update metadata with info text.
 
     """
     BlockDev.SetInfo(self, text)
 
+    self._RemoveOldInfo()
+
     # Replace invalid characters
     text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
     text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
@@ -762,16 +900,14 @@ class LogicalVolume(BlockDev):
     # Only up to 128 characters are allowed
     text = text[:128]
 
-    result = utils.RunCmd(["lvchange", "--addtag", text,
-                           self.dev_path])
-    if result.failed:
-      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
-                  result.output)
+    _CheckResult(utils.RunCmd(["lvchange", "--addtag", text, self.dev_path]))
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Grow the logical volume.
 
     """
+    if not backingstore:
+      return
     if self.pe_size is None or self.stripe_count is None:
       if not self.Attach():
         _ThrowError("Can't attach to LV during Grow()")
@@ -985,12 +1121,13 @@ class BaseDRBD(BlockDev): # pylint: disable=W0223
                                     first_line)
 
     values = version.groups()
-    retval = {"k_major": int(values[0]),
-              "k_minor": int(values[1]),
-              "k_point": int(values[2]),
-              "api": int(values[3]),
-              "proto": int(values[4]),
-             }
+    retval = {
+      "k_major": int(values[0]),
+      "k_minor": int(values[1]),
+      "k_point": int(values[2]),
+      "api": int(values[3]),
+      "proto": int(values[4]),
+      }
     if values[5] is not None:
       retval["proto2"] = values[5]
 
@@ -1058,7 +1195,7 @@ class BaseDRBD(BlockDev): # pylint: disable=W0223
   def _CheckMetaSize(meta_device):
     """Check if the given meta device looks like a valid one.
 
-    This currently only check the size, which must be around
+    This currently only checks the size, which must be around
     128MiB.
 
     """
@@ -1098,10 +1235,10 @@ class DRBD8(BaseDRBD):
   doesn't do anything to the supposed peer. If you need a fully
   connected DRBD pair, you need to use this class on both hosts.
 
-  The unique_id for the drbd device is the (local_ip, local_port,
-  remote_ip, remote_port) tuple, and it must have two children: the
-  data device and the meta_device. The meta device is checked for
-  valid size and is zeroed on create.
+  The unique_id for the drbd device is a (local_ip, local_port,
+  remote_ip, remote_port, local_minor, secret) tuple, and it must have
+  two children: the data device and the meta_device. The meta device
+  is checked for valid size and is zeroed on create.
 
   """
   _MAX_MINORS = 255
@@ -1197,7 +1334,7 @@ class DRBD8(BaseDRBD):
   def _GetShowParser(cls):
     """Return a parser for `drbd show` output.
 
-    This will either create or return an already-create parser for the
+    This will either create or return an already-created parser for the
     output of the command `drbd show`.
 
     """
@@ -1388,7 +1525,7 @@ class DRBD8(BaseDRBD):
 
   @classmethod
   def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
-      disable_meta_flush):
+                              disable_meta_flush):
     """Compute the DRBD command line parameters for disk barriers
 
     Returns a list of the disk barrier parameters as requested via the
@@ -1622,7 +1759,7 @@ class DRBD8(BaseDRBD):
                    "--c-delay-target", params[constants.LDP_DELAY_TARGET],
                    "--c-max-rate", params[constants.LDP_MAX_RATE],
                    "--c-min-rate", params[constants.LDP_MIN_RATE],
-                  ])
+                   ])
 
     else:
       args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]])
@@ -1633,7 +1770,7 @@ class DRBD8(BaseDRBD):
       msg = ("Can't change syncer rate: %s - %s" %
              (result.fail_reason, result.output))
       logging.error(msg)
-      return msg
+      return [msg]
 
     return []
 
@@ -2070,7 +2207,7 @@ class DRBD8(BaseDRBD):
     cls._InitMeta(aminor, meta.dev_path)
     return cls(unique_id, children, size, params)
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Resize the DRBD device and its backing storage.
 
     """
@@ -2078,9 +2215,10 @@ class DRBD8(BaseDRBD):
       _ThrowError("drbd%d: Grow called while not attached", self._aminor)
     if len(self._children) != 2 or None in self._children:
       _ThrowError("drbd%d: cannot grow diskless device", self.minor)
-    self._children[0].Grow(amount, dryrun)
-    if dryrun:
-      # DRBD does not support dry-run mode, so we'll return here
+    self._children[0].Grow(amount, dryrun, backingstore)
+    if dryrun or backingstore:
+      # DRBD does not support dry-run mode and is not backing storage,
+      # so we'll return here
       return
     result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
                            "%dm" % (self.size + amount)])
@@ -2107,6 +2245,9 @@ class FileStorage(BlockDev):
       raise ValueError("Invalid configuration data %s" % str(unique_id))
     self.driver = unique_id[0]
     self.dev_path = unique_id[1]
+
+    CheckFileStoragePath(self.dev_path)
+
     self.Attach()
 
   def Assemble(self):
@@ -2163,12 +2304,14 @@ class FileStorage(BlockDev):
     # TODO: implement rename for file-based storage
     _ThrowError("Rename is not supported for file-based storage")
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Grow the file
 
     @param amount: the amount (in mebibytes) to grow with
 
     """
+    if not backingstore:
+      return
     # Check that the file exists
     self.Assemble()
     current_size = self.GetActualSize()
@@ -2221,7 +2364,11 @@ class FileStorage(BlockDev):
     """
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
       raise ValueError("Invalid configuration data %s" % str(unique_id))
+
     dev_path = unique_id[1]
+
+    CheckFileStoragePath(dev_path)
+
     try:
       fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
       f = os.fdopen(fd, "w")
@@ -2339,7 +2486,7 @@ class PersistentBlockDevice(BlockDev):
     """
     pass
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Grow the logical volume.
 
     """
@@ -2605,7 +2752,7 @@ class RADOSBlockDevice(BlockDev):
     """
     pass
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Grow the Volume.
 
     @type amount: integer
@@ -2615,6 +2762,8 @@ class RADOSBlockDevice(BlockDev):
         only, without actually increasing the size
 
     """
+    if not backingstore:
+      return
     if not self.Attach():
       _ThrowError("Can't attach to rbd device during Grow()")