Merge branch 'stable-2.8' into stable-2.9
[ganeti-local] / qa / qa_config.py
index 6c5e27e..97ccaa7 100644 (file)
@@ -29,12 +29,16 @@ from ganeti import constants
 from ganeti import utils
 from ganeti import serializer
 from ganeti import compat
+from ganeti import ht
 
 import qa_error
 
 
 _INSTANCE_CHECK_KEY = "instance-check"
 _ENABLED_HV_KEY = "enabled-hypervisors"
+_VCLUSTER_MASTER_KEY = "vcluster-master"
+_VCLUSTER_BASEDIR_KEY = "vcluster-basedir"
+_ENABLED_DISK_TEMPLATES_KEY = "enabled-disk-templates"
 
 #: QA configuration (L{_QaConfig})
 _config = None
@@ -44,8 +48,8 @@ class _QaInstance(object):
   __slots__ = [
     "name",
     "nicmac",
-    "used",
-    "disk_template",
+    "_used",
+    "_disk_template",
     ]
 
   def __init__(self, name, nicmac):
@@ -54,8 +58,8 @@ class _QaInstance(object):
     """
     self.name = name
     self.nicmac = nicmac
-    self.used = None
-    self.disk_template = None
+    self._used = None
+    self._disk_template = None
 
   @classmethod
   def FromDict(cls, data):
@@ -70,34 +74,36 @@ class _QaInstance(object):
 
     return cls(name=data["name"], nicmac=nicmac)
 
-  def __getitem__(self, key):
-    """Legacy dict-like interface.
+  def __repr__(self):
+    status = [
+      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
+      "name=%s" % self.name,
+      "nicmac=%s" % self.nicmac,
+      "used=%s" % self._used,
+      "disk_template=%s" % self._disk_template,
+      ]
 
-    """
-    if key == "name":
-      return self.name
-    else:
-      raise KeyError(key)
+    return "<%s at %#x>" % (" ".join(status), id(self))
 
-  def get(self, key, default):
-    """Legacy dict-like interface.
+  def Use(self):
+    """Marks instance as being in use.
 
     """
-    try:
-      return self[key]
-    except KeyError:
-      return default
+    assert not self._used
+    assert self._disk_template is None
+
+    self._used = True
 
   def Release(self):
     """Releases instance and makes it available again.
 
     """
-    assert self.used, \
+    assert self._used, \
       ("Instance '%s' was never acquired or released more than once" %
        self.name)
 
-    self.used = False
-    self.disk_template = None
+    self._used = False
+    self._disk_template = None
 
   def GetNicMacAddr(self, idx, default):
     """Returns MAC address for NIC.
@@ -112,6 +118,28 @@ class _QaInstance(object):
     else:
       return default
 
+  def SetDiskTemplate(self, template):
+    """Set the disk template.
+
+    """
+    assert template in constants.DISK_TEMPLATES
+
+    self._disk_template = template
+
+  @property
+  def used(self):
+    """Returns boolean denoting whether instance is in use.
+
+    """
+    return self._used
+
+  @property
+  def disk_template(self):
+    """Returns the current disk template.
+
+    """
+    return self._disk_template
+
 
 class _QaNode(object):
   __slots__ = [
@@ -137,25 +165,16 @@ class _QaNode(object):
     """
     return cls(primary=data["primary"], secondary=data.get("secondary"))
 
-  def __getitem__(self, key):
-    """Legacy dict-like interface.
-
-    """
-    if key == "primary":
-      return self.primary
-    elif key == "secondary":
-      return self.secondary
-    else:
-      raise KeyError(key)
-
-  def get(self, key, default):
-    """Legacy dict-like interface.
+  def __repr__(self):
+    status = [
+      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
+      "primary=%s" % self.primary,
+      "secondary=%s" % self.secondary,
+      "added=%s" % self._added,
+      "use_count=%s" % self._use_count,
+      ]
 
-    """
-    try:
-      return self[key]
-    except KeyError:
-      return default
+    return "<%s at %#x>" % (" ".join(status), id(self))
 
   def Use(self):
     """Marks a node as being in use.
@@ -252,18 +271,23 @@ class _QaConfig(object):
     """Validates loaded configuration data.
 
     """
+    if not self.get("name"):
+      raise qa_error.Error("Cluster name is required")
+
     if not self.get("nodes"):
       raise qa_error.Error("Need at least one node")
 
     if not self.get("instances"):
       raise qa_error.Error("Need at least one instance")
 
-    if (self.get("disk") is None or
-        self.get("disk-growth") is None or
-        len(self.get("disk")) != len(self.get("disk-growth"))):
-      raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
-                           " and have the same number of items")
-
+    disks = self.GetDiskOptions()
+    if disks is None:
+      raise qa_error.Error("Config option 'disks' must exist")
+    else:
+      for d in disks:
+        if d.get("size") is None or d.get("growth") is None:
+          raise qa_error.Error("Config options `size` and `growth` must exist"
+                               " for all `disks` items")
     check = self.GetInstanceCheckScript()
     if check:
       try:
@@ -281,6 +305,16 @@ class _QaConfig(object):
       raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
                            utils.CommaJoin(difference))
 
+    (vc_master, vc_basedir) = self.GetVclusterSettings()
+    if bool(vc_master) != bool(vc_basedir):
+      raise qa_error.Error("All or none of the config options '%s' and '%s'"
+                           " must be set" %
+                           (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY))
+
+    if vc_basedir and not utils.IsNormAbsPath(vc_basedir):
+      raise qa_error.Error("Path given in option '%s' must be absolute and"
+                           " normalized" % _VCLUSTER_BASEDIR_KEY)
+
   def __getitem__(self, name):
     """Returns configuration value.
 
@@ -290,6 +324,24 @@ class _QaConfig(object):
     """
     return self._data[name]
 
+  def __setitem__(self, key, value):
+    """Sets a configuration value.
+
+    """
+    self._data[key] = value
+
+  def __delitem__(self, key):
+    """Deletes a value from the configuration.
+
+    """
+    del(self._data[key])
+
+  def __len__(self):
+    """Return the number of configuration items.
+
+    """
+    return len(self._data)
+
   def get(self, name, default=None):
     """Returns configuration value.
 
@@ -318,28 +370,67 @@ class _QaConfig(object):
     @rtype: list
 
     """
+    return self._GetStringListParameter(
+      _ENABLED_HV_KEY,
+      [constants.DEFAULT_ENABLED_HYPERVISOR])
+
+  def GetDefaultHypervisor(self):
+    """Returns the default hypervisor to be used.
+
+    """
+    return self.GetEnabledHypervisors()[0]
+
+  def GetEnabledDiskTemplates(self):
+    """Returns the list of enabled disk templates.
+
+    @rtype: list
+
+    """
+    return self._GetStringListParameter(
+      _ENABLED_DISK_TEMPLATES_KEY,
+      constants.DEFAULT_ENABLED_DISK_TEMPLATES)
+
+  def GetEnabledStorageTypes(self):
+    """Returns the list of enabled storage types.
+
+    @rtype: list
+    @returns: the list of storage types enabled for QA
+
+    """
+    enabled_disk_templates = self.GetEnabledDiskTemplates()
+    enabled_storage_types = list(
+        set([constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt]
+             for dt in enabled_disk_templates]))
+    # Storage type 'lvm-pv' cannot be activated via a disk template,
+    # therefore we add it if 'lvm-vg' is present.
+    if constants.ST_LVM_VG in enabled_storage_types:
+      enabled_storage_types.append(constants.ST_LVM_PV)
+    return enabled_storage_types
+
+  def GetDefaultDiskTemplate(self):
+    """Returns the default disk template to be used.
+
+    """
+    return self.GetEnabledDiskTemplates()[0]
+
+  def _GetStringListParameter(self, key, default_values):
+    """Retrieves a parameter's value that is supposed to be a list of strings.
+
+    @rtype: list
+
+    """
     try:
-      value = self._data[_ENABLED_HV_KEY]
+      value = self._data[key]
     except KeyError:
-      return [constants.DEFAULT_ENABLED_HYPERVISOR]
+      return default_values
     else:
       if value is None:
         return []
       elif isinstance(value, basestring):
-        # The configuration key ("enabled-hypervisors") implies there can be
-        # multiple values. Multiple hypervisors are comma-separated on the
-        # command line option to "gnt-cluster init", so we need to handle them
-        # equally here.
         return value.split(",")
       else:
         return value
 
-  def GetDefaultHypervisor(self):
-    """Returns the default hypervisor to be used.
-
-    """
-    return self.GetEnabledHypervisors()[0]
-
   def SetExclusiveStorage(self, value):
     """Set the expected value of the C{exclusive_storage} flag for the cluster.
 
@@ -358,8 +449,64 @@ class _QaConfig(object):
     """Is the given disk template supported by the current configuration?
 
     """
-    return (not self.GetExclusiveStorage() or
-            templ in constants.DTS_EXCL_STORAGE)
+    enabled = templ in self.GetEnabledDiskTemplates()
+    return enabled and (not self.GetExclusiveStorage() or
+                        templ in constants.DTS_EXCL_STORAGE)
+
+  def IsStorageTypeSupported(self, storage_type):
+    """Is the given storage type supported by the current configuration?
+
+    This is determined by looking if at least one of the disk templates
+    which is associated with the storage type is enabled in the configuration.
+
+    """
+    enabled_disk_templates = self.GetEnabledDiskTemplates()
+    if storage_type == constants.ST_LVM_PV:
+      disk_templates = utils.GetDiskTemplatesOfStorageType(constants.ST_LVM_VG)
+    else:
+      disk_templates = utils.GetDiskTemplatesOfStorageType(storage_type)
+    return bool(set(enabled_disk_templates).intersection(set(disk_templates)))
+
+  def AreSpindlesSupported(self):
+    """Are spindles supported by the current configuration?
+
+    """
+    return self.GetExclusiveStorage()
+
+  def GetVclusterSettings(self):
+    """Returns settings for virtual cluster.
+
+    """
+    master = self.get(_VCLUSTER_MASTER_KEY)
+    basedir = self.get(_VCLUSTER_BASEDIR_KEY)
+
+    return (master, basedir)
+
+  def GetDiskOptions(self):
+    """Return options for the disks of the instances.
+
+    Get 'disks' parameter from the configuration data. If 'disks' is missing,
+    try to create it from the legacy 'disk' and 'disk-growth' parameters.
+
+    """
+    try:
+      return self._data["disks"]
+    except KeyError:
+      pass
+
+    # Legacy interface
+    sizes = self._data.get("disk")
+    growths = self._data.get("disk-growth")
+    if sizes or growths:
+      if (sizes is None or growths is None or len(sizes) != len(growths)):
+        raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
+                             " exist and have the same number of items")
+      disks = []
+      for (size, growth) in zip(sizes, growths):
+        disks.append({"size": size, "growth": growth})
+      return disks
+    else:
+      return None
 
 
 def Load(path):
@@ -435,6 +582,8 @@ def _TestEnabledInner(check_fn, names, fn):
       value = _TestEnabledInner(check_fn, name.tests, compat.any)
     elif isinstance(name, (list, tuple)):
       value = _TestEnabledInner(check_fn, name, compat.all)
+    elif callable(name):
+      value = name()
     else:
       value = check_fn(name)
 
@@ -486,11 +635,25 @@ def GetDefaultHypervisor(*args):
   return GetConfig().GetDefaultHypervisor(*args)
 
 
-def GetInstanceNicMac(inst, default=None):
-  """Returns MAC address for instance's network interface.
+def GetEnabledDiskTemplates(*args):
+  """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
+
+  """
+  return GetConfig().GetEnabledDiskTemplates(*args)
+
+
+def GetEnabledStorageTypes(*args):
+  """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.
 
   """
-  return inst.GetNicMacAddr(0, default)
+  return GetConfig().GetEnabledStorageTypes(*args)
+
+
+def GetDefaultDiskTemplate(*args):
+  """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
+
+  """
+  return GetConfig().GetDefaultDiskTemplate(*args)
 
 
 def GetMasterNode():
@@ -515,51 +678,54 @@ def AcquireInstance(_cfg=None):
   if not instances:
     raise qa_error.OutOfInstancesError("No instances left")
 
-  inst = instances[0]
+  instance = instances[0]
+  instance.Use()
 
-  assert not inst.used
-  assert inst.disk_template is None
+  return instance
 
-  inst.used = True
 
-  return inst
+def SetExclusiveStorage(value):
+  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
+
+  """
+  return GetConfig().SetExclusiveStorage(value)
 
 
-def GetInstanceTemplate(inst):
-  """Return the disk template of an instance.
+def GetExclusiveStorage():
+  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
 
   """
-  templ = inst.disk_template
-  assert templ is not None
-  return templ
+  return GetConfig().GetExclusiveStorage()
 
 
-def SetInstanceTemplate(inst, template):
-  """Set the disk template for an instance.
+def IsTemplateSupported(templ):
+  """Wrapper for L{_QaConfig.IsTemplateSupported}.
 
   """
-  inst.disk_template = template
+  return GetConfig().IsTemplateSupported(templ)
 
 
-def SetExclusiveStorage(value):
-  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
+def IsStorageTypeSupported(storage_type):
+  """Wrapper for L{_QaConfig.IsTemplateSupported}.
 
   """
-  return GetConfig().SetExclusiveStorage(value)
+  return GetConfig().IsStorageTypeSupported(storage_type)
 
 
-def GetExclusiveStorage():
-  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
+def AreSpindlesSupported():
+  """Wrapper for L{_QaConfig.AreSpindlesSupported}.
 
   """
-  return GetConfig().GetExclusiveStorage()
+  return GetConfig().AreSpindlesSupported()
 
 
-def IsTemplateSupported(templ):
-  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
+def _NodeSortKey(node):
+  """Returns sort key for a node.
+
+  @type node: L{_QaNode}
 
   """
-  return GetConfig().IsTemplateSupported(templ)
+  return (node.use_count, utils.NiceSortKey(node.primary))
 
 
 def AcquireNode(exclude=None, _cfg=None):
@@ -587,17 +753,8 @@ def AcquireNode(exclude=None, _cfg=None):
   if not nodes:
     raise qa_error.OutOfNodesError("No nodes left")
 
-  # Get node with least number of uses
-  # TODO: Switch to computing sort key instead of comparing directly
-  def compare(a, b):
-    result = cmp(a.use_count, b.use_count)
-    if result == 0:
-      result = cmp(a.primary, b.primary)
-    return result
-
-  nodes.sort(cmp=compare)
-
-  return nodes[0].Use()
+  # Return node with least number of uses
+  return sorted(nodes, key=_NodeSortKey)[0].Use()
 
 
 def AcquireManyNodes(num, exclude=None):
@@ -634,3 +791,41 @@ def AcquireManyNodes(num, exclude=None):
 def ReleaseManyNodes(nodes):
   for node in nodes:
     node.Release()
+
+
+def GetVclusterSettings():
+  """Wrapper for L{_QaConfig.GetVclusterSettings}.
+
+  """
+  return GetConfig().GetVclusterSettings()
+
+
+def UseVirtualCluster(_cfg=None):
+  """Returns whether a virtual cluster is used.
+
+  @rtype: bool
+
+  """
+  if _cfg is None:
+    cfg = GetConfig()
+  else:
+    cfg = _cfg
+
+  (master, _) = cfg.GetVclusterSettings()
+
+  return bool(master)
+
+
+@ht.WithDesc("No virtual cluster")
+def NoVirtualCluster():
+  """Used to disable tests for virtual clusters.
+
+  """
+  return not UseVirtualCluster()
+
+
+def GetDiskOptions():
+  """Wrapper for L{_QaConfig.GetDiskOptions}.
+
+  """
+  return GetConfig().GetDiskOptions()