Remove old "reason" implementation
[ganeti-local] / lib / objects.py
index dd83bd2..9be79ac 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -38,13 +38,14 @@ pass to and from external parties.
 import ConfigParser
 import re
 import copy
+import logging
 import time
 from cStringIO import StringIO
 
 from ganeti import errors
 from ganeti import constants
 from ganeti import netutils
-from ganeti import objectutils
+from ganeti import outils
 from ganeti import utils
 
 from socket import AF_INET
@@ -81,16 +82,28 @@ def FillDict(defaults_dict, custom_dict, skip_keys=None):
   return ret_dict
 
 
-def FillIPolicy(default_ipolicy, custom_ipolicy, skip_keys=None):
+def _FillMinMaxISpecs(default_specs, custom_specs):
+  assert frozenset(default_specs.keys()) == constants.ISPECS_MINMAX_KEYS
+  ret_specs = {}
+  for key in constants.ISPECS_MINMAX_KEYS:
+    ret_specs[key] = FillDict(default_specs[key],
+                              custom_specs.get(key, {}))
+  return ret_specs
+
+
+def FillIPolicy(default_ipolicy, custom_ipolicy):
   """Fills an instance policy with defaults.
 
   """
   assert frozenset(default_ipolicy.keys()) == constants.IPOLICY_ALL_KEYS
   ret_dict = {}
-  for key in constants.IPOLICY_ISPECS:
-    ret_dict[key] = FillDict(default_ipolicy[key],
-                             custom_ipolicy.get(key, {}),
-                             skip_keys=skip_keys)
+  # Instance specs
+  new_mm = _FillMinMaxISpecs(default_ipolicy[constants.ISPECS_MINMAX],
+                             custom_ipolicy.get(constants.ISPECS_MINMAX, {}))
+  ret_dict[constants.ISPECS_MINMAX] = new_mm
+  new_std = FillDict(default_ipolicy[constants.ISPECS_STD],
+                     custom_ipolicy.get(constants.ISPECS_STD, {}))
+  ret_dict[constants.ISPECS_STD] = new_std
   # list items
   for key in [constants.IPOLICY_DTS]:
     ret_dict[key] = list(custom_ipolicy.get(key, default_ipolicy[key]))
@@ -185,14 +198,16 @@ def MakeEmptyIPolicy():
   """Create empty IPolicy dictionary.
 
   """
-  return dict([
-    (constants.ISPECS_MIN, {}),
-    (constants.ISPECS_MAX, {}),
-    (constants.ISPECS_STD, {}),
-    ])
+  return {
+    constants.ISPECS_MINMAX: {
+      constants.ISPECS_MIN: {},
+      constants.ISPECS_MAX: {},
+      },
+    constants.ISPECS_STD: {},
+    }
 
 
-class ConfigObject(objectutils.ValidatedSlots):
+class ConfigObject(outils.ValidatedSlots):
   """A generic config object.
 
   It has the following properties:
@@ -263,47 +278,6 @@ class ConfigObject(objectutils.ValidatedSlots):
     obj = cls(**val_str) # pylint: disable=W0142
     return obj
 
-  @staticmethod
-  def _ContainerToDicts(container):
-    """Convert the elements of a container to standard python types.
-
-    This method converts a container with elements derived from
-    ConfigData to standard python types. If the container is a dict,
-    we don't touch the keys, only the values.
-
-    """
-    if isinstance(container, dict):
-      ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
-    elif isinstance(container, (list, tuple, set, frozenset)):
-      ret = [elem.ToDict() for elem in container]
-    else:
-      raise TypeError("Invalid type %s passed to _ContainerToDicts" %
-                      type(container))
-    return ret
-
-  @staticmethod
-  def _ContainerFromDicts(source, c_type, e_type):
-    """Convert a container from standard python types.
-
-    This method converts a container with standard python types to
-    ConfigData objects. If the container is a dict, we don't touch the
-    keys, only the values.
-
-    """
-    if not isinstance(c_type, type):
-      raise TypeError("Container type %s passed to _ContainerFromDicts is"
-                      " not a type" % type(c_type))
-    if source is None:
-      source = c_type()
-    if c_type is dict:
-      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
-    elif c_type in (list, tuple, set, frozenset):
-      ret = c_type([e_type.FromDict(elem) for elem in source])
-    else:
-      raise TypeError("Invalid container type %s passed to"
-                      " _ContainerFromDicts" % c_type)
-    return ret
-
   def Copy(self):
     """Makes a deep copy of the current object and its children.
 
@@ -446,7 +420,7 @@ class ConfigData(ConfigObject):
     mydict = super(ConfigData, self).ToDict()
     mydict["cluster"] = mydict["cluster"].ToDict()
     for key in "nodes", "instances", "nodegroups", "networks":
-      mydict[key] = self._ContainerToDicts(mydict[key])
+      mydict[key] = outils.ContainerToDicts(mydict[key])
 
     return mydict
 
@@ -457,10 +431,12 @@ class ConfigData(ConfigObject):
     """
     obj = super(ConfigData, cls).FromDict(val)
     obj.cluster = Cluster.FromDict(obj.cluster)
-    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
-    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
-    obj.nodegroups = cls._ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
-    obj.networks = cls._ContainerFromDicts(obj.networks, dict, Network)
+    obj.nodes = outils.ContainerFromDicts(obj.nodes, dict, Node)
+    obj.instances = \
+      outils.ContainerFromDicts(obj.instances, dict, Instance)
+    obj.nodegroups = \
+      outils.ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
+    obj.networks = outils.ContainerFromDicts(obj.networks, dict, Network)
     return obj
 
   def HasAnyDiskOfType(self, dev_type):
@@ -499,11 +475,45 @@ class ConfigData(ConfigObject):
         self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
     if self.networks is None:
       self.networks = {}
+    for network in self.networks.values():
+      network.UpgradeConfig()
+    self._UpgradeEnabledDiskTemplates()
+
+  def _UpgradeEnabledDiskTemplates(self):
+    """Upgrade the cluster's enabled disk templates by inspecting the currently
+       enabled and/or used disk templates.
+
+    """
+    # enabled_disk_templates in the cluster config were introduced in 2.8.
+    # Remove this code once upgrading from earlier versions is deprecated.
+    if not self.cluster.enabled_disk_templates:
+      template_set = \
+        set([inst.disk_template for inst in self.instances.values()])
+      # Add drbd and plain, if lvm is enabled (by specifying a volume group)
+      if self.cluster.volume_group_name:
+        template_set.add(constants.DT_DRBD8)
+        template_set.add(constants.DT_PLAIN)
+      # FIXME: Adapt this when dis/enabling at configure time is removed.
+      # Enable 'file' and 'sharedfile', if they are enabled, even though they
+      # might currently not be used.
+      if constants.ENABLE_FILE_STORAGE:
+        template_set.add(constants.DT_FILE)
+      if constants.ENABLE_SHARED_FILE_STORAGE:
+        template_set.add(constants.DT_SHARED_FILE)
+      # Set enabled_disk_templates to the inferred disk templates. Order them
+      # according to a preference list that is based on Ganeti's history of
+      # supported disk templates.
+      self.cluster.enabled_disk_templates = []
+      for preferred_template in constants.DISK_TEMPLATE_PREFERENCE:
+        if preferred_template in template_set:
+          self.cluster.enabled_disk_templates.append(preferred_template)
+          template_set.remove(preferred_template)
+      self.cluster.enabled_disk_templates.extend(list(template_set))
 
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
-  __slots__ = ["mac", "ip", "network", "nicparams", "netinfo"]
+  __slots__ = ["name", "mac", "ip", "network", "nicparams", "netinfo"] + _UUID
 
   @classmethod
   def CheckParameterSyntax(cls, nicparams):
@@ -514,21 +524,20 @@ class NIC(ConfigObject):
     @raise errors.ConfigurationError: when a parameter is not valid
 
     """
-    if (nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES and
-        nicparams[constants.NIC_MODE] != constants.VALUE_AUTO):
-      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
-      raise errors.ConfigurationError(err)
+    mode = nicparams[constants.NIC_MODE]
+    if (mode not in constants.NIC_VALID_MODES and
+        mode != constants.VALUE_AUTO):
+      raise errors.ConfigurationError("Invalid NIC mode '%s'" % mode)
 
-    if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
+    if (mode == constants.NIC_MODE_BRIDGED and
         not nicparams[constants.NIC_LINK]):
-      err = "Missing bridged nic link"
-      raise errors.ConfigurationError(err)
+      raise errors.ConfigurationError("Missing bridged NIC link")
 
 
 class Disk(ConfigObject):
   """Config object representing a block device."""
-  __slots__ = ["dev_type", "logical_id", "physical_id",
-               "children", "iv_name", "size", "mode", "params"]
+  __slots__ = ["name", "dev_type", "logical_id", "physical_id",
+               "children", "iv_name", "size", "mode", "params"] + _UUID
 
   def CreateOnSecondary(self):
     """Test if this device needs to be created on a secondary node."""
@@ -602,7 +611,8 @@ class Disk(ConfigObject):
 
     """
     if self.dev_type in [constants.LD_LV, constants.LD_FILE,
-                         constants.LD_BLOCKDEV, constants.LD_RBD]:
+                         constants.LD_BLOCKDEV, constants.LD_RBD,
+                         constants.LD_EXT]:
       result = [node]
     elif self.dev_type in constants.LDS_DRBD:
       result = [self.logical_id[0], self.logical_id[1]]
@@ -678,7 +688,7 @@ class Disk(ConfigObject):
 
     """
     if self.dev_type in (constants.LD_LV, constants.LD_FILE,
-                         constants.LD_RBD):
+                         constants.LD_RBD, constants.LD_EXT):
       self.size += amount
     elif self.dev_type == constants.LD_DRBD8:
       if self.children:
@@ -768,7 +778,7 @@ class Disk(ConfigObject):
     for attr in ("children",):
       alist = bo.get(attr, None)
       if alist:
-        bo[attr] = self._ContainerToDicts(alist)
+        bo[attr] = outils.ContainerToDicts(alist)
     return bo
 
   @classmethod
@@ -778,7 +788,7 @@ class Disk(ConfigObject):
     """
     obj = super(Disk, cls).FromDict(val)
     if obj.children:
-      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
+      obj.children = outils.ContainerFromDicts(obj.children, list, Disk)
     if obj.logical_id and isinstance(obj.logical_id, list):
       obj.logical_id = tuple(obj.logical_id)
     if obj.physical_id and isinstance(obj.physical_id, list):
@@ -908,13 +918,15 @@ class Disk(ConfigObject):
         constants.LDP_POOL: dt_params[constants.RBD_POOL],
         }))
 
+    elif disk_template == constants.DT_EXT:
+      result.append(constants.DISK_LD_DEFAULTS[constants.LD_EXT])
+
     return result
 
 
 class InstancePolicy(ConfigObject):
   """Config object representing instance policy limits dictionary.
 
-
   Note that this object is not actually used in the config, it's just
   used as a placeholder for a few functions.
 
@@ -923,9 +935,21 @@ class InstancePolicy(ConfigObject):
   def CheckParameterSyntax(cls, ipolicy, check_std):
     """ Check the instance policy for validity.
 
+    @type ipolicy: dict
+    @param ipolicy: dictionary with min/max/std specs and policies
+    @type check_std: bool
+    @param check_std: Whether to check std value or just assume compliance
+    @raise errors.ConfigurationError: when the policy is not legal
+
     """
-    for param in constants.ISPECS_PARAMETERS:
-      InstancePolicy.CheckISpecSyntax(ipolicy, param, check_std)
+    if constants.ISPECS_MINMAX in ipolicy:
+      if check_std and constants.ISPECS_STD not in ipolicy:
+        msg = "Missing key in ipolicy: %s" % constants.ISPECS_STD
+        raise errors.ConfigurationError(msg)
+      minmaxspecs = ipolicy[constants.ISPECS_MINMAX]
+      stdspec = ipolicy.get(constants.ISPECS_STD)
+      for param in constants.ISPECS_PARAMETERS:
+        InstancePolicy.CheckISpecSyntax(minmaxspecs, stdspec, param, check_std)
     if constants.IPOLICY_DTS in ipolicy:
       InstancePolicy.CheckDiskTemplates(ipolicy[constants.IPOLICY_DTS])
     for key in constants.IPOLICY_PARAMETERS:
@@ -937,37 +961,47 @@ class InstancePolicy(ConfigObject):
                                       utils.CommaJoin(wrong_keys))
 
   @classmethod
-  def CheckISpecSyntax(cls, ipolicy, name, check_std):
-    """Check the instance policy for validity on a given key.
+  def CheckISpecSyntax(cls, minmaxspecs, stdspec, name, check_std):
+    """Check the instance policy specs for validity on a given key.
 
-    We check if the instance policy makes sense for a given key, that is
-    if ipolicy[min][name] <= ipolicy[std][name] <= ipolicy[max][name].
+    We check if the instance specs makes sense for a given key, that is
+    if minmaxspecs[min][name] <= stdspec[name] <= minmaxspec[max][name].
 
-    @type ipolicy: dict
-    @param ipolicy: dictionary with min, max, std specs
+    @type minmaxspecs: dict
+    @param minmaxspecs: dictionary with min and max instance spec
+    @type stdspec: dict
+    @param stdspec: dictionary with standard instance spec
     @type name: string
     @param name: what are the limits for
     @type check_std: bool
     @param check_std: Whether to check std value or just assume compliance
-    @raise errors.ConfigureError: when specs for given name are not valid
+    @raise errors.ConfigurationError: when specs for the given name are not
+        valid
 
     """
-    min_v = ipolicy[constants.ISPECS_MIN].get(name, 0)
+    missing = constants.ISPECS_MINMAX_KEYS - frozenset(minmaxspecs.keys())
+    if missing:
+      msg = "Missing instance specification: %s" % utils.CommaJoin(missing)
+      raise errors.ConfigurationError(msg)
+
+    minspec = minmaxspecs[constants.ISPECS_MIN]
+    maxspec = minmaxspecs[constants.ISPECS_MAX]
+    min_v = minspec.get(name, 0)
 
     if check_std:
-      std_v = ipolicy[constants.ISPECS_STD].get(name, min_v)
+      std_v = stdspec.get(name, min_v)
       std_msg = std_v
     else:
       std_v = min_v
       std_msg = "-"
 
-    max_v = ipolicy[constants.ISPECS_MAX].get(name, std_v)
-    err = ("Invalid specification of min/max/std values for %s: %s/%s/%s" %
-           (name,
-            ipolicy[constants.ISPECS_MIN].get(name, "-"),
-            ipolicy[constants.ISPECS_MAX].get(name, "-"),
-            std_msg))
+    max_v = maxspec.get(name, std_v)
     if min_v > std_v or std_v > max_v:
+      err = ("Invalid specification of min/max/std values for %s: %s/%s/%s" %
+             (name,
+              minspec.get(name, "-"),
+              maxspec.get(name, "-"),
+              std_msg))
       raise errors.ConfigurationError(err)
 
   @classmethod
@@ -975,6 +1009,9 @@ class InstancePolicy(ConfigObject):
     """Checks the disk templates for validity.
 
     """
+    if not disk_templates:
+      raise errors.ConfigurationError("Instance policy must contain" +
+                                      " at least one disk template")
     wrong = frozenset(disk_templates).difference(constants.DISK_TEMPLATES)
     if wrong:
       raise errors.ConfigurationError("Invalid disk template(s) %s" %
@@ -1023,7 +1060,7 @@ class Instance(TaggableObject):
     return tuple(all_nodes)
 
   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
-                             "List of secondary nodes")
+                             "List of names of secondary nodes")
 
   def _ComputeAllNodes(self):
     """Compute the list of all nodes.
@@ -1051,7 +1088,7 @@ class Instance(TaggableObject):
     return tuple(all_nodes)
 
   all_nodes = property(_ComputeAllNodes, None, None,
-                       "List of all nodes of the instance")
+                       "List of names of all the nodes of the instance")
 
   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
     """Provide a mapping of nodes to LVs this instance owns.
@@ -1133,7 +1170,7 @@ class Instance(TaggableObject):
     for attr in "nics", "disks":
       alist = bo.get(attr, None)
       if alist:
-        nlist = self._ContainerToDicts(alist)
+        nlist = outils.ContainerToDicts(alist)
       else:
         nlist = []
       bo[attr] = nlist
@@ -1152,8 +1189,8 @@ class Instance(TaggableObject):
     if "admin_up" in val:
       del val["admin_up"]
     obj = super(Instance, cls).FromDict(val)
-    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
-    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
+    obj.nics = outils.ContainerFromDicts(obj.nics, list, NIC)
+    obj.disks = outils.ContainerFromDicts(obj.disks, list, Disk)
     return obj
 
   def UpgradeConfig(self):
@@ -1235,6 +1272,24 @@ class OS(ConfigObject):
     return cls.SplitNameVariant(name)[1]
 
 
+class ExtStorage(ConfigObject):
+  """Config object representing an External Storage Provider.
+
+  """
+  __slots__ = [
+    "name",
+    "path",
+    "create_script",
+    "remove_script",
+    "grow_script",
+    "attach_script",
+    "detach_script",
+    "setinfo_script",
+    "verify_script",
+    "supported_parameters",
+    ]
+
+
 class NodeHvState(ConfigObject):
   """Hypvervisor state on a node.
 
@@ -1311,6 +1366,12 @@ class Node(TaggableObject):
 
     if self.ndparams is None:
       self.ndparams = {}
+    # And remove any global parameter
+    for key in constants.NDC_GLOBALS:
+      if key in self.ndparams:
+        logging.warning("Ignoring %s node parameter for node %s",
+                        key, self.name)
+        del self.ndparams[key]
 
     if self.powered is None:
       self.powered = True
@@ -1323,12 +1384,12 @@ class Node(TaggableObject):
 
     hv_state = data.get("hv_state", None)
     if hv_state is not None:
-      data["hv_state"] = self._ContainerToDicts(hv_state)
+      data["hv_state"] = outils.ContainerToDicts(hv_state)
 
     disk_state = data.get("disk_state", None)
     if disk_state is not None:
       data["disk_state"] = \
-        dict((key, self._ContainerToDicts(value))
+        dict((key, outils.ContainerToDicts(value))
              for (key, value) in disk_state.items())
 
     return data
@@ -1341,11 +1402,12 @@ class Node(TaggableObject):
     obj = super(Node, cls).FromDict(val)
 
     if obj.hv_state is not None:
-      obj.hv_state = cls._ContainerFromDicts(obj.hv_state, dict, NodeHvState)
+      obj.hv_state = \
+        outils.ContainerFromDicts(obj.hv_state, dict, NodeHvState)
 
     if obj.disk_state is not None:
       obj.disk_state = \
-        dict((key, cls._ContainerFromDicts(value, dict, NodeDiskState))
+        dict((key, outils.ContainerFromDicts(value, dict, NodeDiskState))
              for (key, value) in obj.disk_state.items())
 
     return obj
@@ -1479,6 +1541,7 @@ class Cluster(TaggableObject):
     "prealloc_wipe_disks",
     "hv_state_static",
     "disk_state_static",
+    "enabled_disk_templates",
     ] + _TIMESTAMPS + _UUID
 
   def UpgradeConfig(self):
@@ -1586,6 +1649,12 @@ class Cluster(TaggableObject):
       # we can either make sure to upgrade the ipolicy always, or only
       # do it in some corner cases (e.g. missing keys); note that this
       # will break any removal of keys from the ipolicy dict
+      wrongkeys = frozenset(self.ipolicy.keys()) - constants.IPOLICY_ALL_KEYS
+      if wrongkeys:
+        # These keys would be silently removed by FillIPolicy()
+        msg = ("Cluster instance policy contains spurious keys: %s" %
+               utils.CommaJoin(wrongkeys))
+        raise errors.ConfigurationError(msg)
       self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, self.ipolicy)
 
   @property
@@ -1602,7 +1671,14 @@ class Cluster(TaggableObject):
 
     """
     mydict = super(Cluster, self).ToDict()
-    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
+
+    if self.tcpudp_port_pool is None:
+      tcpudp_port_pool = []
+    else:
+      tcpudp_port_pool = list(self.tcpudp_port_pool)
+
+    mydict["tcpudp_port_pool"] = tcpudp_port_pool
+
     return mydict
 
   @classmethod
@@ -1611,8 +1687,12 @@ class Cluster(TaggableObject):
 
     """
     obj = super(Cluster, cls).FromDict(val)
-    if not isinstance(obj.tcpudp_port_pool, set):
+
+    if obj.tcpudp_port_pool is None:
+      obj.tcpudp_port_pool = set()
+    elif not isinstance(obj.tcpudp_port_pool, set):
       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
+
     return obj
 
   def SimpleFillDP(self, diskparams):
@@ -1903,7 +1983,7 @@ class _QueryResponseBase(ConfigObject):
 
     """
     mydict = super(_QueryResponseBase, self).ToDict()
-    mydict["fields"] = self._ContainerToDicts(mydict["fields"])
+    mydict["fields"] = outils.ContainerToDicts(mydict["fields"])
     return mydict
 
   @classmethod
@@ -1912,7 +1992,8 @@ class _QueryResponseBase(ConfigObject):
 
     """
     obj = super(_QueryResponseBase, cls).FromDict(val)
-    obj.fields = cls._ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
+    obj.fields = \
+      outils.ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
     return obj
 
 
@@ -2004,18 +2085,54 @@ class Network(TaggableObject):
   __slots__ = [
     "name",
     "serial_no",
-    "network_type",
     "mac_prefix",
-    "family",
     "network",
     "network6",
     "gateway",
     "gateway6",
-    "size",
     "reservations",
     "ext_reservations",
     ] + _TIMESTAMPS + _UUID
 
+  def HooksDict(self, prefix=""):
+    """Export a dictionary used by hooks with a network's information.
+
+    @type prefix: String
+    @param prefix: Prefix to prepend to the dict entries
+
+    """
+    result = {
+      "%sNETWORK_NAME" % prefix: self.name,
+      "%sNETWORK_UUID" % prefix: self.uuid,
+      "%sNETWORK_TAGS" % prefix: " ".join(self.GetTags()),
+    }
+    if self.network:
+      result["%sNETWORK_SUBNET" % prefix] = self.network
+    if self.gateway:
+      result["%sNETWORK_GATEWAY" % prefix] = self.gateway
+    if self.network6:
+      result["%sNETWORK_SUBNET6" % prefix] = self.network6
+    if self.gateway6:
+      result["%sNETWORK_GATEWAY6" % prefix] = self.gateway6
+    if self.mac_prefix:
+      result["%sNETWORK_MAC_PREFIX" % prefix] = self.mac_prefix
+
+    return result
+
+  @classmethod
+  def FromDict(cls, val):
+    """Custom function for networks.
+
+    Remove deprecated network_type and family.
+
+    """
+    if "network_type" in val:
+      del val["network_type"]
+    if "family" in val:
+      del val["family"]
+    obj = super(Network, cls).FromDict(val)
+    return obj
+
 
 class SerializableConfigParser(ConfigParser.SafeConfigParser):
   """Simple wrapper over ConfigParse that allows serialization.
@@ -2038,3 +2155,41 @@ class SerializableConfigParser(ConfigParser.SafeConfigParser):
     cfp = cls()
     cfp.readfp(buf)
     return cfp
+
+
+class LvmPvInfo(ConfigObject):
+  """Information about an LVM physical volume (PV).
+
+  @type name: string
+  @ivar name: name of the PV
+  @type vg_name: string
+  @ivar vg_name: name of the volume group containing the PV
+  @type size: float
+  @ivar size: size of the PV in MiB
+  @type free: float
+  @ivar free: free space in the PV, in MiB
+  @type attributes: string
+  @ivar attributes: PV attributes
+  @type lv_list: list of strings
+  @ivar lv_list: names of the LVs hosted on the PV
+  """
+  __slots__ = [
+    "name",
+    "vg_name",
+    "size",
+    "free",
+    "attributes",
+    "lv_list"
+    ]
+
+  def IsEmpty(self):
+    """Is this PV empty?
+
+    """
+    return self.size <= (self.free + 1)
+
+  def IsAllocatable(self):
+    """Is this PV allocatable?
+
+    """
+    return ("a" in self.attributes)