Implement 'out' direction on allocator tests
authorIustin Pop <iustin@google.com>
Wed, 16 Apr 2008 13:56:37 +0000 (13:56 +0000)
committerIustin Pop <iustin@google.com>
Wed, 16 Apr 2008 13:56:37 +0000 (13:56 +0000)
This patch adds the paths for searching for instance allocators and
makes the LUTestAllocator code run the allocator and return the results
if the direction specified is 'out'. 'out' means that the opcode will
return the result of the allocator run, instead of the allocator input
file ('in').

The patch unifies all names to refer to 'iallocator' instead of plain
allocator.

The patch also adds an example allocator that can be used for testing
this new functionality.

Reviewed-by: ultrotter

configure.ac
doc/examples/dumb-allocator [new file with mode: 0755]
lib/Makefile.am
lib/cmdlib.py
lib/constants.py

index 6c3fa4a..1a0f90c 100644 (file)
@@ -50,6 +50,17 @@ AC_ARG_WITH([os-search-path],
   [os_search_path="'/srv/ganeti/os'"])
 AC_SUBST(OS_SEARCH_PATH, $os_search_path)
 
+# --with-iallocator-search-path=...
+# do a bit of black sed magic to for quoting of the strings in the list
+AC_ARG_WITH([iallocator-search-path],
+  [AS_HELP_STRING([--with-iallocator-search-path=LIST],
+    [comma separated list of directories to]
+    [ search for instance allocators (default is $libdir/ganeti/iallocators)]
+  )],
+  [iallocator_search_path=`echo -n "$withval" | sed -e "s/\([[^,]]*\)/'\1'/g"`],
+  [iallocator_search_path="'$libdir/$PACKAGE_NAME/iallocators'"])
+AC_SUBST(IALLOCATOR_SEARCH_PATH, $iallocator_search_path)
+
 # --with-xen-kernel=...
 AC_ARG_WITH([xen-kernel],
   [AS_HELP_STRING([--with-xen-kernel=PATH],
diff --git a/doc/examples/dumb-allocator b/doc/examples/dumb-allocator
new file mode 100755 (executable)
index 0000000..f6d6560
--- /dev/null
@@ -0,0 +1,100 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2008 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+"""Simple first-fit allocator for ganeti instance allocation framework.
+
+This allocator just iterates over the nodes and selects the first one
+that fits in both memory and disk space, without any consideration for
+equal spread or VCPU oversubscription.
+
+"""
+import simplejson
+import sys
+
+
+def SelectNode(nodes, request, to_skip):
+  """Select a node for the given instance
+
+  """
+  disk_size = request["disk_space_total"]
+  selected = None
+  for nname, ninfo in nodes.iteritems():
+    if nname in to_skip:
+      continue
+    if request["memory"] > ninfo["free_memory"]:
+      continue
+    if disk_size > ninfo["free_disk"]:
+      continue
+    selected = nname
+    break
+  return selected
+
+
+def OutputError(text):
+  """Builds an error response with a given info message.
+
+  """
+  error = {
+    "success": False,
+    "info": text,
+    }
+  print simplejson.dumps(error, indent=2)
+  return 1
+
+
+def main():
+  """Main function.
+
+  """
+  if len(sys.argv) < 2:
+    print >> sys.stderr, "Usage: %s cluster.json" % (sys.argv[0])
+    return 1
+
+  data = simplejson.load(open(sys.argv[1]))
+
+  nodes =  data["nodes"]
+  request = data["request"]
+  req_type = request["type"]
+  if req_type != "allocate":
+    print >> sys.stderr, "Unsupported allocator mode '%s'" % req_type
+    return 1
+
+  npri = SelectNode(nodes, request, [])
+  if npri is None:
+    return OutputError("Can't find a suitable primary node")
+
+  result_nodes = [npri]
+  if request["disk_template"] == "drbd":
+    nsec = SelectNode(nodes, request, result_nodes)
+    if nsec is None:
+      return OutputError("Can't find a suitable secondary node (%s selected"
+                         " as primary)" % npri)
+    result_nodes.append(nsec)
+
+  result = {
+          "success": True,
+          "info": "Allocation successful",
+          "nodes": result_nodes,
+          }
+  print simplejson.dumps(result, indent=2)
+  return 0
+
+if __name__ == "__main__":
+    sys.exit(main())
index 140ed3a..994b2a3 100644 (file)
@@ -25,6 +25,7 @@ _autoconf.py: Makefile
          echo "XEN_KERNEL = '$(XEN_KERNEL)'"; \
          echo "XEN_INITRD = '$(XEN_INITRD)'"; \
          echo "FILE_STORAGE_DIR = '$(FILE_STORAGE_DIR)'"; \
+         echo "IALLOCATOR_SEARCH_PATH = [$(IALLOCATOR_SEARCH_PATH)]"; \
        } > $@
 
 pre-check: all
index 89a3283..c881665 100644 (file)
@@ -4650,7 +4650,7 @@ class LUTestDelay(NoHooksLU):
                                    " result: %s" % (node, node_result))
 
 
-def _AllocatorGetClusterData(cfg, sstore):
+def _IAllocatorGetClusterData(cfg, sstore):
   """Compute the generic allocator input data.
 
   This is the data that is independent of the actual operation.
@@ -4720,7 +4720,7 @@ def _AllocatorGetClusterData(cfg, sstore):
   return data
 
 
-def _AllocatorAddNewInstance(data, op):
+def _IAllocatorAddNewInstance(data, op):
   """Add new instance data to allocator structure.
 
   This in combination with _AllocatorGetClusterData will create the
@@ -4730,6 +4730,12 @@ def _AllocatorAddNewInstance(data, op):
   done.
 
   """
+  if len(op.disks) != 2:
+    raise errors.OpExecError("Only two-disk configurations supported")
+
+  disk_space = _ComputeDiskSize(op.disk_template,
+                                op.disks[0]["size"], op.disks[1]["size"])
+
   request = {
     "type": "allocate",
     "name": op.name,
@@ -4739,15 +4745,16 @@ def _AllocatorAddNewInstance(data, op):
     "vcpus": op.vcpus,
     "memory": op.mem_size,
     "disks": op.disks,
+    "disk_space_total": disk_space,
     "nics": op.nics,
     }
   data["request"] = request
 
 
-def _AllocatorAddRelocateInstance(data, op):
+def _IAllocatorAddRelocateInstance(data, op):
   """Add relocate instance data to allocator structure.
 
-  This in combination with _AllocatorGetClusterData will create the
+  This in combination with _IAllocatorGetClusterData will create the
   correct structure needed as input for the allocator.
 
   The checks for the completeness of the opcode must have already been
@@ -4761,6 +4768,29 @@ def _AllocatorAddRelocateInstance(data, op):
   data["request"] = request
 
 
+def _IAllocatorRun(name, data):
+  """Run an instance allocator and return the results.
+
+  """
+  alloc_script = utils.FindFile(name, constants.IALLOCATOR_SEARCH_PATH,
+                                os.path.isfile)
+  if alloc_script is None:
+    raise errors.OpExecError("Can't find allocator")
+
+  fd, fin_name = tempfile.mkstemp(prefix="ganeti-iallocator.")
+  try:
+    os.write(fd, data)
+    os.close(fd)
+    result = utils.RunCmd([alloc_script, fin_name])
+    if result.failed:
+      raise errors.OpExecError("Instance allocator call failed: %s,"
+                               " output: %s" %
+                               (result.fail_reason, result.stdout))
+  finally:
+    os.unlink(fin_name)
+  return result.stdout
+
+
 class LUTestAllocator(NoHooksLU):
   """Run allocator tests.
 
@@ -4775,7 +4805,7 @@ class LUTestAllocator(NoHooksLU):
     This checks the opcode parameters depending on the director and mode test.
 
     """
-    if self.op.mode == constants.ALF_MODE_ALLOC:
+    if self.op.mode == constants.IALLOCATOR_MODE_ALLOC:
       for attr in ["name", "mem_size", "disks", "disk_template",
                    "os", "tags", "nics", "vcpus"]:
         if not hasattr(self.op, attr):
@@ -4796,6 +4826,8 @@ class LUTestAllocator(NoHooksLU):
                                      " 'nics' parameter")
       if not isinstance(self.op.disks, list):
         raise errors.OpPrereqError("Invalid parameter 'disks'")
+      if len(self.op.disks) != 2:
+        raise errors.OpPrereqError("Only two-disk configurations supported")
       for row in self.op.disks:
         if (not isinstance(row, dict) or
             "size" not in row or
@@ -4804,7 +4836,7 @@ class LUTestAllocator(NoHooksLU):
             row["mode"] not in ['r', 'w']):
           raise errors.OpPrereqError("Invalid contents of the"
                                      " 'disks' parameter")
-    elif self.op.mode == constants.ALF_MODE_RELOC:
+    elif self.op.mode == constants.IALLOCATOR_MODE_RELOC:
       if not hasattr(self.op, "name"):
         raise errors.OpPrereqError("Missing attribute 'name' on opcode input")
       fname = self.cfg.ExpandInstanceName(self.op.name)
@@ -4816,11 +4848,10 @@ class LUTestAllocator(NoHooksLU):
       raise errors.OpPrereqError("Invalid test allocator mode '%s'" %
                                  self.op.mode)
 
-    if self.op.direction == constants.ALF_DIR_OUT:
-      if not hasattr(self.op, "allocator"):
+    if self.op.direction == constants.IALLOCATOR_DIR_OUT:
+      if not hasattr(self.op, "allocator") or self.op.allocator is None:
         raise errors.OpPrereqError("Missing allocator name")
-      raise errors.OpPrereqError("Allocator out mode not supported yet")
-    elif self.op.direction != constants.ALF_DIR_IN:
+    elif self.op.direction != constants.IALLOCATOR_DIR_IN:
       raise errors.OpPrereqError("Wrong allocator test '%s'" %
                                  self.op.direction)
 
@@ -4828,14 +4859,18 @@ class LUTestAllocator(NoHooksLU):
     """Run the allocator test.
 
     """
-    data = _AllocatorGetClusterData(self.cfg, self.sstore)
-    if self.op.mode == constants.ALF_MODE_ALLOC:
-      _AllocatorAddNewInstance(data, self.op)
+    data = _IAllocatorGetClusterData(self.cfg, self.sstore)
+    if self.op.mode == constants.IALLOCATOR_MODE_ALLOC:
+      _IAllocatorAddNewInstance(data, self.op)
     else:
-      _AllocatorAddRelocateInstance(data, self.op)
+      _IAllocatorAddRelocateInstance(data, self.op)
 
     if _JSON_INDENT is None:
       text = simplejson.dumps(data)
     else:
       text = simplejson.dumps(data, indent=_JSON_INDENT)
-    return text
+    if self.op.direction == constants.IALLOCATOR_DIR_IN:
+      result = text
+    else:
+      result = _IAllocatorRun(self.op.allocator, text)
+    return result
index 7a6e7e7..91fcf81 100644 (file)
@@ -185,7 +185,8 @@ VERIFY_NPLUSONE_MEM = 'nplusone_mem'
 VERIFY_OPTIONAL_CHECKS = frozenset([VERIFY_NPLUSONE_MEM])
 
 # Allocator framework constants
-ALF_DIR_IN = "in"
-ALF_DIR_OUT = "out"
-ALF_MODE_ALLOC = "allocate"
-ALF_MODE_RELOC = "relocate"
+IALLOCATOR_DIR_IN = "in"
+IALLOCATOR_DIR_OUT = "out"
+IALLOCATOR_MODE_ALLOC = "allocate"
+IALLOCATOR_MODE_RELOC = "relocate"
+IALLOCATOR_SEARCH_PATH = _autoconf.IALLOCATOR_SEARCH_PATH