objects.NIC: Look up mode only once, capitalize acronym
[ganeti-local] / lib / objects.py
index ff921d6..e08bd06 100644 (file)
@@ -29,7 +29,7 @@ pass to and from external parties.
 # pylint: disable=E0203,W0201,R0902
 
 # E0203: Access to member %r before its definition, since we use
-# objects.py which doesn't explicitely initialise its members
+# objects.py which doesn't explicitly initialise its members
 
 # W0201: Attribute '%s' defined outside __init__
 
@@ -44,29 +44,18 @@ from cStringIO import StringIO
 from ganeti import errors
 from ganeti import constants
 from ganeti import netutils
+from ganeti import objectutils
 from ganeti import utils
 
 from socket import AF_INET
 
 
 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
-           "OS", "Node", "NodeGroup", "Cluster", "FillDict"]
+           "OS", "Node", "NodeGroup", "Cluster", "FillDict", "Network"]
 
 _TIMESTAMPS = ["ctime", "mtime"]
 _UUID = ["uuid"]
 
-# constants used to create InstancePolicy dictionary
-TISPECS_GROUP_TYPES = {
-  constants.ISPECS_MIN: constants.VTYPE_INT,
-  constants.ISPECS_MAX: constants.VTYPE_INT,
-  }
-
-TISPECS_CLUSTER_TYPES = {
-  constants.ISPECS_MIN: constants.VTYPE_INT,
-  constants.ISPECS_MAX: constants.VTYPE_INT,
-  constants.ISPECS_STD: constants.VTYPE_INT,
-  }
-
 
 def FillDict(defaults_dict, custom_dict, skip_keys=None):
   """Basic function to apply settings on top a default dict.
@@ -105,10 +94,26 @@ def FillIPolicy(default_ipolicy, custom_ipolicy, skip_keys=None):
   # list items
   for key in [constants.IPOLICY_DTS]:
     ret_dict[key] = list(custom_ipolicy.get(key, default_ipolicy[key]))
+  # other items which we know we can directly copy (immutables)
+  for key in constants.IPOLICY_PARAMETERS:
+    ret_dict[key] = custom_ipolicy.get(key, default_ipolicy[key])
 
   return ret_dict
 
 
+def FillDiskParams(default_dparams, custom_dparams, skip_keys=None):
+  """Fills the disk parameter defaults.
+
+  @see: L{FillDict} for parameters and return value
+
+  """
+  assert frozenset(default_dparams.keys()) == constants.DISK_TEMPLATES
+
+  return dict((dt, FillDict(default_dparams[dt], custom_dparams.get(dt, {}),
+                             skip_keys=skip_keys))
+              for dt in constants.DISK_TEMPLATES)
+
+
 def UpgradeGroupedParams(target, defaults):
   """Update all groups for the target parameter.
 
@@ -146,26 +151,36 @@ def UpgradeDiskParams(diskparams):
   @type diskparams: dict
   @param diskparams: disk parameters to upgrade
   @rtype: dict
-  @return: the upgraded disk parameters dit
+  @return: the upgraded disk parameters dict
 
   """
-  result = dict()
-  if diskparams is None:
-    result = constants.DISK_DT_DEFAULTS.copy()
+  if not diskparams:
+    result = {}
   else:
-    # Update the disk parameter values for each disk template.
-    # The code iterates over constants.DISK_TEMPLATES because new templates
-    # might have been added.
-    for template in constants.DISK_TEMPLATES:
-      if template not in diskparams:
-        result[template] = constants.DISK_DT_DEFAULTS[template].copy()
-      else:
-        result[template] = FillDict(constants.DISK_DT_DEFAULTS[template],
-                                    diskparams[template])
+    result = FillDiskParams(constants.DISK_DT_DEFAULTS, diskparams)
 
   return result
 
 
+def UpgradeNDParams(ndparams):
+  """Upgrade ndparams structure.
+
+  @type ndparams: dict
+  @param ndparams: disk parameters to upgrade
+  @rtype: dict
+  @return: the upgraded node parameters dict
+
+  """
+  if ndparams is None:
+    ndparams = {}
+
+  if (constants.ND_OOB_PROGRAM in ndparams and
+      ndparams[constants.ND_OOB_PROGRAM] is None):
+    # will be reset by the line below
+    del ndparams[constants.ND_OOB_PROGRAM]
+  return FillDict(constants.NDC_DEFAULTS, ndparams)
+
+
 def MakeEmptyIPolicy():
   """Create empty IPolicy dictionary.
 
@@ -177,59 +192,7 @@ def MakeEmptyIPolicy():
     ])
 
 
-def CreateIPolicyFromOpts(ispecs_mem_size=None,
-                          ispecs_cpu_count=None,
-                          ispecs_disk_count=None,
-                          ispecs_disk_size=None,
-                          ispecs_nic_count=None,
-                          ipolicy_disk_templates=None,
-                          group_ipolicy=False,
-                          allowed_values=None,
-                          fill_all=False):
-  """Creation of instance policy based on command line options.
-
-  @param fill_all: whether for cluster policies we should ensure that
-    all values are filled
-
-
-  """
-  # prepare ipolicy dict
-  ipolicy_transposed = {
-    constants.ISPEC_MEM_SIZE: ispecs_mem_size,
-    constants.ISPEC_CPU_COUNT: ispecs_cpu_count,
-    constants.ISPEC_DISK_COUNT: ispecs_disk_count,
-    constants.ISPEC_DISK_SIZE: ispecs_disk_size,
-    constants.ISPEC_NIC_COUNT: ispecs_nic_count,
-    }
-
-  # first, check that the values given are correct
-  if group_ipolicy:
-    forced_type = TISPECS_GROUP_TYPES
-  else:
-    forced_type = TISPECS_CLUSTER_TYPES
-
-  for specs in ipolicy_transposed.values():
-    utils.ForceDictType(specs, forced_type, allowed_values=allowed_values)
-
-  # then transpose
-  ipolicy_out = MakeEmptyIPolicy()
-  for name, specs in ipolicy_transposed.iteritems():
-    assert name in constants.ISPECS_PARAMETERS
-    for key, val in specs.items(): # {min: .. ,max: .., std: ..}
-      ipolicy_out[key][name] = val
-
-  # no filldict for lists
-  if not group_ipolicy and fill_all and ipolicy_disk_templates is None:
-    ipolicy_disk_templates = constants.DISK_TEMPLATES
-  if ipolicy_disk_templates is not None:
-    ipolicy_out[constants.IPOLICY_DTS] = list(ipolicy_disk_templates)
-
-  assert not (frozenset(ipolicy_out.keys()) - constants.IPOLICY_ALL_KEYS)
-
-  return ipolicy_out
-
-
-class ConfigObject(object):
+class ConfigObject(objectutils.ValidatedSlots):
   """A generic config object.
 
   It has the following properties:
@@ -244,31 +207,22 @@ class ConfigObject(object):
   """
   __slots__ = []
 
-  def __init__(self, **kwargs):
-    for k, v in kwargs.iteritems():
-      setattr(self, k, v)
-
   def __getattr__(self, name):
-    if name not in self._all_slots():
+    if name not in self.GetAllSlots():
       raise AttributeError("Invalid object attribute %s.%s" %
                            (type(self).__name__, name))
     return None
 
   def __setstate__(self, state):
-    slots = self._all_slots()
+    slots = self.GetAllSlots()
     for name in state:
       if name in slots:
         setattr(self, name, state[name])
 
-  @classmethod
-  def _all_slots(cls):
-    """Compute the list of all declared slots for a class.
+  def Validate(self):
+    """Validates the slots.
 
     """
-    slots = []
-    for parent in cls.__mro__:
-      slots.extend(getattr(parent, "__slots__", []))
-    return slots
 
   def ToDict(self):
     """Convert to a dict holding only standard python types.
@@ -281,7 +235,7 @@ class ConfigObject(object):
 
     """
     result = {}
-    for name in self._all_slots():
+    for name in self.GetAllSlots():
       value = getattr(self, name, None)
       if value is not None:
         result[name] = value
@@ -466,7 +420,7 @@ class MasterNetworkParameters(ConfigObject):
     "ip",
     "netmask",
     "netdev",
-    "ip_family"
+    "ip_family",
     ]
 
 
@@ -478,6 +432,7 @@ class ConfigData(ConfigObject):
     "nodes",
     "nodegroups",
     "instances",
+    "networks",
     "serial_no",
     ] + _TIMESTAMPS
 
@@ -490,7 +445,7 @@ class ConfigData(ConfigObject):
     """
     mydict = super(ConfigData, self).ToDict()
     mydict["cluster"] = mydict["cluster"].ToDict()
-    for key in "nodes", "instances", "nodegroups":
+    for key in "nodes", "instances", "nodegroups", "networks":
       mydict[key] = self._ContainerToDicts(mydict[key])
 
     return mydict
@@ -505,6 +460,7 @@ class ConfigData(ConfigObject):
     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)
     return obj
 
   def HasAnyDiskOfType(self, dev_type):
@@ -541,11 +497,13 @@ class ConfigData(ConfigObject):
       # gives a good approximation.
       if self.HasAnyDiskOfType(constants.LD_DRBD8):
         self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
+    if self.networks is None:
+      self.networks = {}
 
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
-  __slots__ = ["mac", "ip", "nicparams"]
+  __slots__ = ["mac", "ip", "network", "nicparams", "netinfo"]
 
   @classmethod
   def CheckParameterSyntax(cls, nicparams):
@@ -556,15 +514,14 @@ 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):
@@ -644,7 +601,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]]
@@ -720,7 +678,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:
@@ -883,13 +841,78 @@ class Disk(ConfigObject):
       for child in self.children:
         child.UpgradeConfig()
 
-    if not self.params:
-      self.params = constants.DISK_LD_DEFAULTS[self.dev_type].copy()
-    else:
-      self.params = FillDict(constants.DISK_LD_DEFAULTS[self.dev_type],
-                             self.params)
+    # FIXME: Make this configurable in Ganeti 2.7
+    self.params = {}
     # add here config upgrade for this disk
 
+  @staticmethod
+  def ComputeLDParams(disk_template, disk_params):
+    """Computes Logical Disk parameters from Disk Template parameters.
+
+    @type disk_template: string
+    @param disk_template: disk template, one of L{constants.DISK_TEMPLATES}
+    @type disk_params: dict
+    @param disk_params: disk template parameters;
+                        dict(template_name -> parameters
+    @rtype: list(dict)
+    @return: a list of dicts, one for each node of the disk hierarchy. Each dict
+      contains the LD parameters of the node. The tree is flattened in-order.
+
+    """
+    if disk_template not in constants.DISK_TEMPLATES:
+      raise errors.ProgrammerError("Unknown disk template %s" % disk_template)
+
+    assert disk_template in disk_params
+
+    result = list()
+    dt_params = disk_params[disk_template]
+    if disk_template == constants.DT_DRBD8:
+      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_DRBD8], {
+        constants.LDP_RESYNC_RATE: dt_params[constants.DRBD_RESYNC_RATE],
+        constants.LDP_BARRIERS: dt_params[constants.DRBD_DISK_BARRIERS],
+        constants.LDP_NO_META_FLUSH: dt_params[constants.DRBD_META_BARRIERS],
+        constants.LDP_DEFAULT_METAVG: dt_params[constants.DRBD_DEFAULT_METAVG],
+        constants.LDP_DISK_CUSTOM: dt_params[constants.DRBD_DISK_CUSTOM],
+        constants.LDP_NET_CUSTOM: dt_params[constants.DRBD_NET_CUSTOM],
+        constants.LDP_DYNAMIC_RESYNC: dt_params[constants.DRBD_DYNAMIC_RESYNC],
+        constants.LDP_PLAN_AHEAD: dt_params[constants.DRBD_PLAN_AHEAD],
+        constants.LDP_FILL_TARGET: dt_params[constants.DRBD_FILL_TARGET],
+        constants.LDP_DELAY_TARGET: dt_params[constants.DRBD_DELAY_TARGET],
+        constants.LDP_MAX_RATE: dt_params[constants.DRBD_MAX_RATE],
+        constants.LDP_MIN_RATE: dt_params[constants.DRBD_MIN_RATE],
+        }))
+
+      # data LV
+      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV], {
+        constants.LDP_STRIPES: dt_params[constants.DRBD_DATA_STRIPES],
+        }))
+
+      # metadata LV
+      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV], {
+        constants.LDP_STRIPES: dt_params[constants.DRBD_META_STRIPES],
+        }))
+
+    elif disk_template in (constants.DT_FILE, constants.DT_SHARED_FILE):
+      result.append(constants.DISK_LD_DEFAULTS[constants.LD_FILE])
+
+    elif disk_template == constants.DT_PLAIN:
+      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV], {
+        constants.LDP_STRIPES: dt_params[constants.LV_STRIPES],
+        }))
+
+    elif disk_template == constants.DT_BLOCK:
+      result.append(constants.DISK_LD_DEFAULTS[constants.LD_BLOCKDEV])
+
+    elif disk_template == constants.DT_RBD:
+      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_RBD], {
+        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.
@@ -900,21 +923,24 @@ class InstancePolicy(ConfigObject):
 
   """
   @classmethod
-  def CheckParameterSyntax(cls, ipolicy):
+  def CheckParameterSyntax(cls, ipolicy, check_std):
     """ Check the instance policy for validity.
 
     """
     for param in constants.ISPECS_PARAMETERS:
-      InstancePolicy.CheckISpecSyntax(ipolicy, param)
+      InstancePolicy.CheckISpecSyntax(ipolicy, param, check_std)
     if constants.IPOLICY_DTS in ipolicy:
       InstancePolicy.CheckDiskTemplates(ipolicy[constants.IPOLICY_DTS])
+    for key in constants.IPOLICY_PARAMETERS:
+      if key in ipolicy:
+        InstancePolicy.CheckParameter(key, ipolicy[key])
     wrong_keys = frozenset(ipolicy.keys()) - constants.IPOLICY_ALL_KEYS
     if wrong_keys:
       raise errors.ConfigurationError("Invalid keys in ipolicy: %s" %
                                       utils.CommaJoin(wrong_keys))
 
   @classmethod
-  def CheckISpecSyntax(cls, ipolicy, name):
+  def CheckISpecSyntax(cls, ipolicy, name, check_std):
     """Check the instance policy for validity on a given key.
 
     We check if the instance policy makes sense for a given key, that is
@@ -924,17 +950,26 @@ class InstancePolicy(ConfigObject):
     @param ipolicy: dictionary with min, max, std specs
     @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
 
     """
     min_v = ipolicy[constants.ISPECS_MIN].get(name, 0)
-    std_v = ipolicy[constants.ISPECS_STD].get(name, min_v)
+
+    if check_std:
+      std_v = ipolicy[constants.ISPECS_STD].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, "-"),
-            ipolicy[constants.ISPECS_STD].get(name, "-")))
+            std_msg))
     if min_v > std_v or std_v > max_v:
       raise errors.ConfigurationError(err)
 
@@ -948,6 +983,19 @@ class InstancePolicy(ConfigObject):
       raise errors.ConfigurationError("Invalid disk template(s) %s" %
                                       utils.CommaJoin(wrong))
 
+  @classmethod
+  def CheckParameter(cls, key, value):
+    """Checks a parameter.
+
+    Currently we expect all parameters to be float values.
+
+    """
+    try:
+      float(value)
+    except (TypeError, ValueError), err:
+      raise errors.ConfigurationError("Invalid value for key" " '%s':"
+                                      " '%s', error: %s" % (key, value, err))
+
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
@@ -978,7 +1026,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.
@@ -1006,7 +1054,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.
@@ -1023,7 +1071,7 @@ class Instance(TaggableObject):
         GetVolumeList()
 
     """
-    if node == None:
+    if node is None:
       node = self.primary_node
 
     if lvmap is None:
@@ -1190,6 +1238,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.
 
@@ -1318,6 +1384,7 @@ class NodeGroup(TaggableObject):
     "hv_state_static",
     "disk_state_static",
     "alloc_policy",
+    "networks",
     ] + _TIMESTAMPS + _UUID
 
   def ToDict(self):
@@ -1360,10 +1427,14 @@ class NodeGroup(TaggableObject):
     if self.mtime is None:
       self.mtime = time.time()
 
-    self.diskparams = UpgradeDiskParams(self.diskparams)
+    if self.diskparams is None:
+      self.diskparams = {}
     if self.ipolicy is None:
       self.ipolicy = MakeEmptyIPolicy()
 
+    if self.networks is None:
+      self.networks = {}
+
   def FillND(self, node):
     """Return filled out ndparams for L{objects.Node}
 
@@ -1451,8 +1522,7 @@ class Cluster(TaggableObject):
     if self.osparams is None:
       self.osparams = {}
 
-    if self.ndparams is None:
-      self.ndparams = constants.NDC_DEFAULTS
+    self.ndparams = UpgradeNDParams(self.ndparams)
 
     self.beparams = UpgradeGroupedParams(self.beparams,
                                          constants.BEC_DEFAULTS)
@@ -1482,8 +1552,8 @@ class Cluster(TaggableObject):
     # code can be removed once upgrading straight from 2.0 is deprecated.
     if self.default_hypervisor is not None:
       self.enabled_hypervisors = ([self.default_hypervisor] +
-        [hvname for hvname in self.enabled_hypervisors
-         if hvname != self.default_hypervisor])
+                                  [hvname for hvname in self.enabled_hypervisors
+                                   if hvname != self.default_hypervisor])
       self.default_hypervisor = None
 
     # maintain_node_health added after 2.1.1
@@ -1525,7 +1595,10 @@ class Cluster(TaggableObject):
     if self.use_external_mip_script is None:
       self.use_external_mip_script = False
 
-    self.diskparams = UpgradeDiskParams(self.diskparams)
+    if self.diskparams:
+      self.diskparams = UpgradeDiskParams(self.diskparams)
+    else:
+      self.diskparams = constants.DISK_DT_DEFAULTS.copy()
 
     # instance policy added before 2.6
     if self.ipolicy is None:
@@ -1563,6 +1636,15 @@ class Cluster(TaggableObject):
       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
     return obj
 
+  def SimpleFillDP(self, diskparams):
+    """Fill a given diskparams dict with cluster defaults.
+
+    @param diskparams: The diskparams
+    @return: The defaults dict
+
+    """
+    return FillDiskParams(self.diskparams, diskparams)
+
   def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
     """Get the default hypervisor parameters for the cluster.
 
@@ -1855,17 +1937,6 @@ class _QueryResponseBase(ConfigObject):
     return obj
 
 
-class QueryRequest(ConfigObject):
-  """Object holding a query request.
-
-  """
-  __slots__ = [
-    "what",
-    "fields",
-    "qfilter",
-    ]
-
-
 class QueryResponse(_QueryResponseBase):
   """Object holding the response to a query.
 
@@ -1894,8 +1965,7 @@ class QueryFieldsResponse(_QueryResponseBase):
   @ivar fields: List of L{QueryFieldDefinition} objects
 
   """
-  __slots__ = [
-    ]
+  __slots__ = []
 
 
 class MigrationStatus(ConfigObject):
@@ -1948,6 +2018,26 @@ class InstanceConsole(ConfigObject):
     return True
 
 
+class Network(TaggableObject):
+  """Object representing a network definition for ganeti.
+
+  """
+  __slots__ = [
+    "name",
+    "serial_no",
+    "network_type",
+    "mac_prefix",
+    "family",
+    "network",
+    "network6",
+    "gateway",
+    "gateway6",
+    "size",
+    "reservations",
+    "ext_reservations",
+    ] + _TIMESTAMPS + _UUID
+
+
 class SerializableConfigParser(ConfigParser.SafeConfigParser):
   """Simple wrapper over ConfigParse that allows serialization.