QA: Convert instances to objects
[ganeti-local] / qa / qa_config.py
index 6c4e3f5..879cff9 100644 (file)
@@ -35,54 +35,254 @@ import qa_error
 
 _INSTANCE_CHECK_KEY = "instance-check"
 _ENABLED_HV_KEY = "enabled-hypervisors"
-# Key to store the cluster-wide run-time value of the exclusive storage flag
-_EXCLUSIVE_STORAGE_KEY = "_exclusive_storage"
 
+#: QA configuration (L{_QaConfig})
+_config = None
 
-cfg = {}
-options = None
 
+class _QaInstance(object):
+  __slots__ = [
+    "name",
+    "nicmac",
+    "used",
+    "disk_template",
+    ]
+
+  def __init__(self, name, nicmac):
+    """Initializes instances of this class.
+
+    """
+    self.name = name
+    self.nicmac = nicmac
+    self.used = None
+    self.disk_template = None
+
+  @classmethod
+  def FromDict(cls, data):
+    """Creates instance object from JSON dictionary.
+
+    """
+    nicmac = []
+
+    macaddr = data.get("nic.mac/0")
+    if macaddr:
+      nicmac.append(macaddr)
+
+    return cls(name=data["name"], nicmac=nicmac)
+
+  def __getitem__(self, key):
+    """Legacy dict-like interface.
+
+    """
+    if key == "name":
+      return self.name
+    else:
+      raise KeyError(key)
+
+  def get(self, key, default):
+    """Legacy dict-like interface.
+
+    """
+    try:
+      return self[key]
+    except KeyError:
+      return default
+
+  def GetNicMacAddr(self, idx, default):
+    """Returns MAC address for NIC.
+
+    @type idx: int
+    @param idx: NIC index
+    @param default: Default value
+
+    """
+    if len(self.nicmac) > idx:
+      return self.nicmac[idx]
+    else:
+      return default
+
+
+_RESOURCE_CONVERTER = {
+  "instances": _QaInstance.FromDict,
+  }
 
-def Load(path):
-  """Loads the passed configuration file.
+
+def _ConvertResources((key, value)):
+  """Converts cluster resources in configuration to Python objects.
 
   """
-  global cfg # pylint: disable=W0603
+  fn = _RESOURCE_CONVERTER.get(key, None)
+  if fn:
+    return (key, map(fn, value))
+  else:
+    return (key, value)
+
+
+class _QaConfig(object):
+  def __init__(self, data):
+    """Initializes instances of this class.
+
+    """
+    self._data = data
+
+    #: Cluster-wide run-time value of the exclusive storage flag
+    self._exclusive_storage = None
+
+  @classmethod
+  def Load(cls, filename):
+    """Loads a configuration file and produces a configuration object.
+
+    @type filename: string
+    @param filename: Path to configuration file
+    @rtype: L{_QaConfig}
+
+    """
+    data = serializer.LoadJson(utils.ReadFile(filename))
+
+    result = cls(dict(map(_ConvertResources,
+                          data.items()))) # pylint: disable=E1103
+    result.Validate()
+
+    return result
+
+  def Validate(self):
+    """Validates loaded configuration data.
+
+    """
+    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")
+
+    check = self.GetInstanceCheckScript()
+    if check:
+      try:
+        os.stat(check)
+      except EnvironmentError, err:
+        raise qa_error.Error("Can't find instance check script '%s': %s" %
+                             (check, err))
+
+    enabled_hv = frozenset(self.GetEnabledHypervisors())
+    if not enabled_hv:
+      raise qa_error.Error("No hypervisor is enabled")
+
+    difference = enabled_hv - constants.HYPER_TYPES
+    if difference:
+      raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
+                           utils.CommaJoin(difference))
+
+  def __getitem__(self, name):
+    """Returns configuration value.
+
+    @type name: string
+    @param name: Name of configuration entry
+
+    """
+    return self._data[name]
+
+  def get(self, name, default=None):
+    """Returns configuration value.
 
-  cfg = serializer.LoadJson(utils.ReadFile(path))
+    @type name: string
+    @param name: Name of configuration entry
+    @param default: Default value
 
-  Validate()
+    """
+    return self._data.get(name, default)
 
+  def GetMasterNode(self):
+    """Returns the default master node for the cluster.
 
-def Validate():
-  if len(cfg["nodes"]) < 1:
-    raise qa_error.Error("Need at least one node")
-  if len(cfg["instances"]) < 1:
-    raise qa_error.Error("Need at least one instance")
-  if len(cfg["disk"]) != len(cfg["disk-growth"]):
-    raise qa_error.Error("Config options 'disk' and 'disk-growth' must have"
-                         " the same number of items")
+    """
+    return self["nodes"][0]
+
+  def GetInstanceCheckScript(self):
+    """Returns path to instance check script or C{None}.
+
+    """
+    return self._data.get(_INSTANCE_CHECK_KEY, None)
 
-  check = GetInstanceCheckScript()
-  if check:
+  def GetEnabledHypervisors(self):
+    """Returns list of enabled hypervisors.
+
+    @rtype: list
+
+    """
     try:
-      os.stat(check)
-    except EnvironmentError, err:
-      raise qa_error.Error("Can't find instance check script '%s': %s" %
-                           (check, err))
+      value = self._data[_ENABLED_HV_KEY]
+    except KeyError:
+      return [constants.DEFAULT_ENABLED_HYPERVISOR]
+    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.
+
+    """
+    self._exclusive_storage = bool(value)
+
+  def GetExclusiveStorage(self):
+    """Get the expected value of the C{exclusive_storage} flag for the cluster.
+
+    """
+    value = self._exclusive_storage
+    assert value is not None
+    return value
+
+  def IsTemplateSupported(self, templ):
+    """Is the given disk template supported by the current configuration?
+
+    """
+    return (not self.GetExclusiveStorage() or
+            templ in constants.DTS_EXCL_STORAGE)
+
+
+def Load(path):
+  """Loads the passed configuration file.
+
+  """
+  global _config # pylint: disable=W0603
 
-  enabled_hv = frozenset(GetEnabledHypervisors())
-  if not enabled_hv:
-    raise qa_error.Error("No hypervisor is enabled")
+  _config = _QaConfig.Load(path)
 
-  difference = enabled_hv - constants.HYPER_TYPES
-  if difference:
-    raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
-                         utils.CommaJoin(difference))
+
+def GetConfig():
+  """Returns the configuration object.
+
+  """
+  if _config is None:
+    raise RuntimeError("Configuration not yet loaded")
+
+  return _config
 
 
 def get(name, default=None):
-  return cfg.get(name, default)
+  """Wrapper for L{_QaConfig.get}.
+
+  """
+  return GetConfig().get(name, default=default)
 
 
 class Either:
@@ -148,10 +348,12 @@ def TestEnabled(tests, _cfg=None):
 
   """
   if _cfg is None:
-    _cfg = cfg
+    cfg = GetConfig()
+  else:
+    cfg = _cfg
 
   # Get settings for all tests
-  cfg_tests = _cfg.get("tests", {})
+  cfg_tests = cfg.get("tests", {})
 
   # Get default setting
   default = cfg_tests.get("default", True)
@@ -160,79 +362,76 @@ def TestEnabled(tests, _cfg=None):
                            tests, compat.all)
 
 
-def GetInstanceCheckScript():
-  """Returns path to instance check script or C{None}.
+def GetInstanceCheckScript(*args):
+  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
 
   """
-  return cfg.get(_INSTANCE_CHECK_KEY, None)
-
+  return GetConfig().GetInstanceCheckScript(*args)
 
-def GetEnabledHypervisors():
-  """Returns list of enabled hypervisors.
 
-  @rtype: list
+def GetEnabledHypervisors(*args):
+  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
 
   """
-  try:
-    value = cfg[_ENABLED_HV_KEY]
-  except KeyError:
-    return [constants.DEFAULT_ENABLED_HYPERVISOR]
-  else:
-    if 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
+  return GetConfig().GetEnabledHypervisors(*args)
 
 
-def GetDefaultHypervisor():
-  """Returns the default hypervisor to be used.
+def GetDefaultHypervisor(*args):
+  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
 
   """
-  return GetEnabledHypervisors()[0]
+  return GetConfig().GetDefaultHypervisor(*args)
 
 
 def GetInstanceNicMac(inst, default=None):
   """Returns MAC address for instance's network interface.
 
   """
-  return inst.get("nic.mac/0", default)
+  return inst.GetNicMacAddr(0, default)
 
 
 def GetMasterNode():
-  return cfg["nodes"][0]
+  """Wrapper for L{_QaConfig.GetMasterNode}.
+
+  """
+  return GetConfig().GetMasterNode()
 
 
-def AcquireInstance():
+def AcquireInstance(_cfg=None):
   """Returns an instance which isn't in use.
 
   """
+  if _cfg is None:
+    cfg = GetConfig()
+  else:
+    cfg = _cfg
+
   # Filter out unwanted instances
-  tmp_flt = lambda inst: not inst.get("_used", False)
-  instances = filter(tmp_flt, cfg["instances"])
-  del tmp_flt
+  instances = filter(lambda inst: not inst.used, cfg["instances"])
 
-  if len(instances) == 0:
+  if not instances:
     raise qa_error.OutOfInstancesError("No instances left")
 
   inst = instances[0]
-  inst["_used"] = True
-  inst["_template"] = None
+
+  assert not inst.used
+  assert inst.disk_template is None
+
+  inst.used = True
+
   return inst
 
 
 def ReleaseInstance(inst):
-  inst["_used"] = False
+  inst.used = False
+  inst.disk_template = None
 
 
 def GetInstanceTemplate(inst):
   """Return the disk template of an instance.
 
   """
-  templ = inst["_template"]
+  templ = inst.disk_template
   assert templ is not None
   return templ
 
@@ -241,33 +440,28 @@ def SetInstanceTemplate(inst, template):
   """Set the disk template for an instance.
 
   """
-  inst["_template"] = template
+  inst.disk_template = template
 
 
 def SetExclusiveStorage(value):
-  """Set the expected value of the exclusive_storage flag for the cluster.
+  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
 
   """
-  cfg[_EXCLUSIVE_STORAGE_KEY] = bool(value)
+  return GetConfig().SetExclusiveStorage(value)
 
 
 def GetExclusiveStorage():
-  """Get the expected value of the exclusive_storage flag for the cluster.
+  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
 
   """
-  val = cfg.get(_EXCLUSIVE_STORAGE_KEY)
-  assert val is not None
-  return val
+  return GetConfig().GetExclusiveStorage()
 
 
 def IsTemplateSupported(templ):
-  """Is the given templated supported by the current configuration?
+  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
 
   """
-  if GetExclusiveStorage():
-    return templ in constants.DTS_EXCL_STORAGE
-  else:
-    return True
+  return GetConfig().IsTemplateSupported(templ)
 
 
 def AcquireNode(exclude=None):
@@ -275,6 +469,7 @@ def AcquireNode(exclude=None):
 
   """
   master = GetMasterNode()
+  cfg = GetConfig()
 
   # Filter out unwanted nodes
   # TODO: Maybe combine filters