Replace single- with double-quotes
[ganeti-local] / lib / rapi / baserlib.py
index fbafa27..5710000 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2012 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
@@ -23,7 +23,7 @@
 
 """
 
-# pylint: disable-msg=C0103
+# pylint: disable=C0103
 
 # C0103: Invalid name, since the R_* names are not conforming
 
@@ -32,15 +32,33 @@ import logging
 from ganeti import luxi
 from ganeti import rapi
 from ganeti import http
-from ganeti import ssconf
-from ganeti import constants
-from ganeti import opcodes
 from ganeti import errors
+from ganeti import compat
 
 
 # Dummy value to detect unchanged parameters
 _DEFAULT = object()
 
+#: Supported HTTP methods
+_SUPPORTED_METHODS = frozenset([
+  http.HTTP_DELETE,
+  http.HTTP_GET,
+  http.HTTP_POST,
+  http.HTTP_PUT,
+  ])
+
+
+def _BuildOpcodeAttributes():
+  """Builds list of attributes used for per-handler opcodes.
+
+  """
+  return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
+           "Get%sOpInput" % method.capitalize())
+          for method in _SUPPORTED_METHODS]
+
+
+_OPCODE_ATTRS = _BuildOpcodeAttributes()
+
 
 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
   """Builds a URI list as used by index resources.
@@ -53,7 +71,10 @@ def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
   (field_id, field_uri) = uri_fields
 
   def _MapId(m_id):
-    return { field_id: m_id, field_uri: uri_format % m_id, }
+    return {
+      field_id: m_id,
+      field_uri: uri_format % m_id,
+      }
 
   # Make sure the result is sorted, makes it nicer to look at and simplifies
   # unittests.
@@ -88,45 +109,6 @@ def MapFields(names, data):
   return dict(zip(names, data))
 
 
-def _Tags_GET(kind, name):
-  """Helper function to retrieve tags.
-
-  """
-  if kind == constants.TAG_INSTANCE or kind == constants.TAG_NODE:
-    if not name:
-      raise http.HttpBadRequest("Missing name on tag request")
-    cl = GetClient()
-    if kind == constants.TAG_INSTANCE:
-      fn = cl.QueryInstances
-    else:
-      fn = cl.QueryNodes
-    result = fn(names=[name], fields=["tags"], use_locking=False)
-    if not result or not result[0]:
-      raise http.HttpBadGateway("Invalid response from tag query")
-    tags = result[0][0]
-  elif kind == constants.TAG_CLUSTER:
-    ssc = ssconf.SimpleStore()
-    tags = ssc.GetClusterTags()
-
-  return list(tags)
-
-
-def _Tags_PUT(kind, tags, name, dry_run):
-  """Helper function to set tags.
-
-  """
-  return SubmitJob([opcodes.OpAddTags(kind=kind, name=name,
-                                      tags=tags, dry_run=dry_run)])
-
-
-def _Tags_DELETE(kind, tags, name, dry_run):
-  """Helper function to delete tags.
-
-  """
-  return SubmitJob([opcodes.OpTagsDel(kind=kind, name=name,
-                                      tags=tags, dry_run=dry_run)])
-
-
 def MapBulkFields(itemslist, fields):
   """Map value to field name in to one dictionary.
 
@@ -170,7 +152,7 @@ def MakeParamsDict(opts, params):
   return result
 
 
-def FillOpcode(opcls, body, static):
+def FillOpcode(opcls, body, static, rename=None):
   """Fills an opcode with body parameters.
 
   Parameter types are checked.
@@ -181,28 +163,42 @@ def FillOpcode(opcls, body, static):
   @param body: Body parameters as received from client
   @type static: dict
   @param static: Static parameters which can't be modified by client
+  @type rename: dict
+  @param rename: Renamed parameters, key as old name, value as new name
   @return: Opcode object
 
   """
-  CheckType(body, dict, "Body contents")
+  if body is None:
+    params = {}
+  else:
+    CheckType(body, dict, "Body contents")
+
+    # Make copy to be modified
+    params = body.copy()
+
+  if rename:
+    for old, new in rename.items():
+      if new in params and old in params:
+        raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
+                                  " both are specified" %
+                                  (old, new))
+      if old in params:
+        assert new not in params
+        params[new] = params.pop(old)
 
   if static:
-    overwritten = set(body.keys()) & set(static.keys())
+    overwritten = set(params.keys()) & set(static.keys())
     if overwritten:
       raise http.HttpBadRequest("Can't overwrite static parameters %r" %
                                 overwritten)
 
-  # Combine parameters
-  params = body.copy()
-
-  if static:
     params.update(static)
 
   # Convert keys to strings (simplejson decodes them as unicode)
   params = dict((str(key), value) for (key, value) in params.items())
 
   try:
-    op = opcls(**params) # pylint: disable-msg=W0142
+    op = opcls(**params) # pylint: disable=W0142
     op.Validate(False)
   except (errors.OpPrereqError, TypeError), err:
     raise http.HttpBadRequest("Invalid body parameters: %s" % err)
@@ -210,35 +206,6 @@ def FillOpcode(opcls, body, static):
   return op
 
 
-def SubmitJob(op, cl=None):
-  """Generic wrapper for submit job, for better http compatibility.
-
-  @type op: list
-  @param op: the list of opcodes for the job
-  @type cl: None or luxi.Client
-  @param cl: optional luxi client to use
-  @rtype: string
-  @return: the job ID
-
-  """
-  try:
-    if cl is None:
-      cl = GetClient()
-    return cl.SubmitJob(op)
-  except errors.JobQueueFull:
-    raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
-  except errors.JobQueueDrainError:
-    raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
-  except luxi.NoMasterError, err:
-    raise http.HttpBadGateway("Master seems to be unreachable: %s" % str(err))
-  except luxi.PermissionError:
-    raise http.HttpInternalServerError("Internal error: no permission to"
-                                       " connect to the master daemon")
-  except luxi.TimeoutError, err:
-    raise http.HttpGatewayTimeout("Timeout while talking to the master"
-                                  " daemon. Error: %s" % str(err))
-
-
 def HandleItemQueryErrors(fn, *args, **kwargs):
   """Converts errors when querying a single item.
 
@@ -252,19 +219,6 @@ def HandleItemQueryErrors(fn, *args, **kwargs):
     raise
 
 
-def GetClient():
-  """Geric wrapper for luxi.Client(), for better http compatiblity.
-
-  """
-  try:
-    return luxi.Client()
-  except luxi.NoMasterError, err:
-    raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
-  except luxi.PermissionError:
-    raise http.HttpInternalServerError("Internal error: no permission to"
-                                       " connect to the master daemon")
-
-
 def FeedbackFn(msg):
   """Feedback logging function for jobs.
 
@@ -325,7 +279,7 @@ def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
   return CheckType(value, exptype, "'%s' parameter" % name)
 
 
-class R_Generic(object):
+class ResourceBase(object):
   """Generic class for resources.
 
   """
@@ -335,16 +289,19 @@ class R_Generic(object):
   POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
   DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
 
-  def __init__(self, items, queryargs, req):
+  def __init__(self, items, queryargs, req, _client_cls=luxi.Client):
     """Generic resource constructor.
 
     @param items: a list with variables encoded in the URL
     @param queryargs: a dictionary with additional options from URL
+    @param req: Request context
+    @param _client_cls: L{luxi} client class (unittests only)
 
     """
     self.items = items
     self.queryargs = queryargs
     self._req = req
+    self._client_cls = _client_cls
 
   def _GetRequestBody(self):
     """Returns the body data.
@@ -420,3 +377,135 @@ class R_Generic(object):
 
     """
     return bool(self._checkIntVariable("dry-run"))
+
+  def GetClient(self):
+    """Wrapper for L{luxi.Client} with HTTP-specific error handling.
+
+    """
+    # Could be a function, pylint: disable=R0201
+    try:
+      return self._client_cls()
+    except luxi.NoMasterError, err:
+      raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
+    except luxi.PermissionError:
+      raise http.HttpInternalServerError("Internal error: no permission to"
+                                         " connect to the master daemon")
+
+  def SubmitJob(self, op, cl=None):
+    """Generic wrapper for submit job, for better http compatibility.
+
+    @type op: list
+    @param op: the list of opcodes for the job
+    @type cl: None or luxi.Client
+    @param cl: optional luxi client to use
+    @rtype: string
+    @return: the job ID
+
+    """
+    if cl is None:
+      cl = self.GetClient()
+    try:
+      return cl.SubmitJob(op)
+    except errors.JobQueueFull:
+      raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
+    except errors.JobQueueDrainError:
+      raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
+    except luxi.NoMasterError, err:
+      raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
+    except luxi.PermissionError:
+      raise http.HttpInternalServerError("Internal error: no permission to"
+                                         " connect to the master daemon")
+    except luxi.TimeoutError, err:
+      raise http.HttpGatewayTimeout("Timeout while talking to the master"
+                                    " daemon: %s" % err)
+
+
+def GetResourceOpcodes(cls):
+  """Returns all opcodes used by a resource.
+
+  """
+  return frozenset(filter(None, (getattr(cls, op_attr, None)
+                                 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
+
+
+class _MetaOpcodeResource(type):
+  """Meta class for RAPI resources.
+
+  """
+  def __call__(mcs, *args, **kwargs):
+    """Instantiates class and patches it for use by the RAPI daemon.
+
+    """
+    # Access to private attributes of a client class, pylint: disable=W0212
+    obj = type.__call__(mcs, *args, **kwargs)
+
+    for (method, op_attr, rename_attr, fn_attr) in _OPCODE_ATTRS:
+      if hasattr(obj, method):
+        # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
+        # shouldn't be (they're only used by the automatically generated
+        # handler)
+        assert not hasattr(obj, rename_attr)
+        assert not hasattr(obj, fn_attr)
+      else:
+        # Try to generate handler method on handler instance
+        try:
+          opcode = getattr(obj, op_attr)
+        except AttributeError:
+          pass
+        else:
+          setattr(obj, method,
+                  compat.partial(obj._GenericHandler, opcode,
+                                 getattr(obj, rename_attr, None),
+                                 getattr(obj, fn_attr, obj._GetDefaultData)))
+
+    return obj
+
+
+class OpcodeResource(ResourceBase):
+  """Base class for opcode-based RAPI resources.
+
+  Instances of this class automatically gain handler functions through
+  L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
+  is defined at class level. Subclasses can define a C{Get$Method$OpInput}
+  method to do their own opcode input processing (e.g. for static values). The
+  C{$METHOD$_RENAME} variable defines which values are renamed (see
+  L{baserlib.FillOpcode}).
+
+  @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
+    automatically generate a GET handler submitting the opcode
+  @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
+    L{baserlib.FillOpcode})
+  @ivar GetGetOpInput: Define this to override the default method for
+    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
+
+  @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
+    automatically generate a PUT handler submitting the opcode
+  @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
+    L{baserlib.FillOpcode})
+  @ivar GetPutOpInput: Define this to override the default method for
+    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
+
+  @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
+    automatically generate a POST handler submitting the opcode
+  @cvar POST_RENAME: Set this to rename parameters in the DELETE handler (see
+    L{baserlib.FillOpcode})
+  @ivar GetPostOpInput: Define this to override the default method for
+    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
+
+  @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
+    automatically generate a GET handler submitting the opcode
+  @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
+    L{baserlib.FillOpcode})
+  @ivar GetDeleteOpInput: Define this to override the default method for
+    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
+
+  """
+  __metaclass__ = _MetaOpcodeResource
+
+  def _GetDefaultData(self):
+    return (self.request_body, None)
+
+  def _GenericHandler(self, opcode, rename, fn):
+    (body, static) = fn()
+    op = FillOpcode(opcode, body, static, rename=rename)
+    return self.SubmitJob([op])