NodeQuery: mark live fields as UNAVAIL for non-vm_capable nodes
[ganeti-local] / lib / rapi / client.py
index b7f8dda..cb43784 100644 (file)
@@ -35,6 +35,7 @@
 
 import logging
 import simplejson
+import socket
 import urllib
 import threading
 import pycurl
@@ -70,6 +71,7 @@ NODE_ROLE_REGULAR = "regular"
 # Internal constants
 _REQ_DATA_VERSION_FIELD = "__version__"
 _INST_CREATE_REQV1 = "instance-create-reqv1"
+_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
 _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"])
 _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
 _INST_CREATE_V0_PARAMS = frozenset([
@@ -234,7 +236,7 @@ def GenericCurlConfig(verbose=False, use_signal=False,
   return _ConfigCurl
 
 
-class GanetiRapiClient(object):
+class GanetiRapiClient(object): # pylint: disable-msg=R0904
   """Ganeti RAPI client.
 
   """
@@ -243,7 +245,7 @@ class GanetiRapiClient(object):
 
   def __init__(self, host, port=GANETI_RAPI_PORT,
                username=None, password=None, logger=logging,
-               curl_config_fn=None, curl=None):
+               curl_config_fn=None, curl_factory=None):
     """Initializes this class.
 
     @type host: string
@@ -259,14 +261,34 @@ class GanetiRapiClient(object):
     @param logger: Logging object
 
     """
-    self._host = host
-    self._port = port
+    self._username = username
+    self._password = password
     self._logger = logger
+    self._curl_config_fn = curl_config_fn
+    self._curl_factory = curl_factory
 
-    self._base_url = "https://%s:%s" % (host, port)
+    try:
+      socket.inet_pton(socket.AF_INET6, host)
+      address = "[%s]:%s" % (host, port)
+    except socket.error:
+      address = "%s:%s" % (host, port)
+
+    self._base_url = "https://%s" % address
 
-    # Create pycURL object if not supplied
-    if not curl:
+    if username is not None:
+      if password is None:
+        raise Error("Password not specified")
+    elif password:
+      raise Error("Specified password without username")
+
+  def _CreateCurl(self):
+    """Creates a cURL object.
+
+    """
+    # Create pycURL object if no factory is provided
+    if self._curl_factory:
+      curl = self._curl_factory()
+    else:
       curl = pycurl.Curl()
 
     # Default cURL settings
@@ -282,20 +304,20 @@ class GanetiRapiClient(object):
       "Content-type: %s" % HTTP_APP_JSON,
       ])
 
-    # Setup authentication
-    if username is not None:
-      if password is None:
-        raise Error("Password not specified")
+    assert ((self._username is None and self._password is None) ^
+            (self._username is not None and self._password is not None))
+
+    if self._username:
+      # Setup authentication
       curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
-      curl.setopt(pycurl.USERPWD, str("%s:%s" % (username, password)))
-    elif password:
-      raise Error("Specified password without username")
+      curl.setopt(pycurl.USERPWD,
+                  str("%s:%s" % (self._username, self._password)))
 
     # Call external configuration function
-    if curl_config_fn:
-      curl_config_fn(curl, logger)
+    if self._curl_config_fn:
+      self._curl_config_fn(curl, self._logger)
 
-    self._curl = curl
+    return curl
 
   @staticmethod
   def _EncodeQuery(query):
@@ -349,7 +371,7 @@ class GanetiRapiClient(object):
     """
     assert path.startswith("/")
 
-    curl = self._curl
+    curl = self._CreateCurl()
 
     if content is not None:
       encoded_content = self._json_encoder.encode(content)
@@ -364,8 +386,8 @@ class GanetiRapiClient(object):
 
     url = "".join(urlparts)
 
-    self._logger.debug("Sending request %s %s to %s:%s (content=%r)",
-                       method, url, self._host, self._port, encoded_content)
+    self._logger.debug("Sending request %s %s (content=%r)",
+                       method, url, encoded_content)
 
     # Buffer for response
     encoded_resp_body = StringIO()
@@ -459,6 +481,30 @@ class GanetiRapiClient(object):
     return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
                              None, None)
 
+  def RedistributeConfig(self):
+    """Tells the cluster to redistribute its configuration files.
+
+    @return: job id
+
+    """
+    return self._SendRequest(HTTP_PUT,
+                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
+                             None, None)
+
+  def ModifyCluster(self, **kwargs):
+    """Modifies cluster parameters.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @rtype: int
+    @return: job id
+
+    """
+    body = kwargs
+
+    return self._SendRequest(HTTP_PUT,
+                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
+
   def GetClusterTags(self):
     """Gets the cluster tags.
 
@@ -704,6 +750,82 @@ class GanetiRapiClient(object):
                              ("/%s/instances/%s" %
                               (GANETI_RAPI_VERSION, instance)), query, None)
 
+  def ModifyInstance(self, instance, **kwargs):
+    """Modifies an instance.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type instance: string
+    @param instance: Instance name
+    @rtype: int
+    @return: job id
+
+    """
+    body = kwargs
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/modify" %
+                              (GANETI_RAPI_VERSION, instance)), None, body)
+
+  def ActivateInstanceDisks(self, instance, ignore_size=None):
+    """Activates an instance's disks.
+
+    @type instance: string
+    @param instance: Instance name
+    @type ignore_size: bool
+    @param ignore_size: Whether to ignore recorded size
+    @return: job id
+
+    """
+    query = []
+    if ignore_size:
+      query.append(("ignore_size", 1))
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/activate-disks" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def DeactivateInstanceDisks(self, instance):
+    """Deactivates an instance's disks.
+
+    @type instance: string
+    @param instance: Instance name
+    @return: job id
+
+    """
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/deactivate-disks" %
+                              (GANETI_RAPI_VERSION, instance)), None, None)
+
+  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
+    """Grows a disk of an instance.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type instance: string
+    @param instance: Instance name
+    @type disk: integer
+    @param disk: Disk index
+    @type amount: integer
+    @param amount: Grow disk by this amount (MiB)
+    @type wait_for_sync: bool
+    @param wait_for_sync: Wait for disk to synchronize
+    @rtype: int
+    @return: job id
+
+    """
+    body = {
+      "amount": amount,
+      }
+
+    if wait_for_sync is not None:
+      body["wait_for_sync"] = wait_for_sync
+
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/instances/%s/disk/%s/grow" %
+                              (GANETI_RAPI_VERSION, instance, disk)),
+                             None, body)
+
   def GetInstanceTags(self, instance):
     """Gets tags for an instance.
 
@@ -820,18 +942,39 @@ class GanetiRapiClient(object):
                              ("/%s/instances/%s/startup" %
                               (GANETI_RAPI_VERSION, instance)), query, None)
 
-  def ReinstallInstance(self, instance, os, no_startup=False):
+  def ReinstallInstance(self, instance, os=None, no_startup=False,
+                        osparams=None):
     """Reinstalls an instance.
 
     @type instance: str
-    @param instance: the instance to reinstall
-    @type os: str
-    @param os: the os to reinstall
+    @param instance: The instance to reinstall
+    @type os: str or None
+    @param os: The operating system to reinstall. If None, the instance's
+        current operating system will be installed again
     @type no_startup: bool
-    @param no_startup: whether to start the instance automatically
+    @param no_startup: Whether to start the instance automatically
 
     """
-    query = [("os", os)]
+    if _INST_REINSTALL_REQV1 in self.GetFeatures():
+      body = {
+        "start": not no_startup,
+        }
+      if os is not None:
+        body["os"] = os
+      if osparams is not None:
+        body["osparams"] = osparams
+      return self._SendRequest(HTTP_POST,
+                               ("/%s/instances/%s/reinstall" %
+                                (GANETI_RAPI_VERSION, instance)), None, body)
+
+    # Use old request format
+    if osparams:
+      raise GanetiApiError("Server does not support specifying OS parameters"
+                           " for instance reinstallation")
+
+    query = []
+    if os:
+      query.append(("os", os))
     if no_startup:
       query.append(("nostartup", 1))
     return self._SendRequest(HTTP_POST,
@@ -932,7 +1075,7 @@ class GanetiRapiClient(object):
                               (GANETI_RAPI_VERSION, instance)), None, body)
 
   def MigrateInstance(self, instance, mode=None, cleanup=None):
-    """Starts up an instance.
+    """Migrates an instance.
 
     @type instance: string
     @param instance: Instance name
@@ -954,6 +1097,44 @@ class GanetiRapiClient(object):
                              ("/%s/instances/%s/migrate" %
                               (GANETI_RAPI_VERSION, instance)), None, body)
 
+  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
+    """Changes the name of an instance.
+
+    @type instance: string
+    @param instance: Instance name
+    @type new_name: string
+    @param new_name: New instance name
+    @type ip_check: bool
+    @param ip_check: Whether to ensure instance's IP address is inactive
+    @type name_check: bool
+    @param name_check: Whether to ensure instance's name is resolvable
+
+    """
+    body = {
+      "new_name": new_name,
+      }
+
+    if ip_check is not None:
+      body["ip_check"] = ip_check
+
+    if name_check is not None:
+      body["name_check"] = name_check
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/rename" %
+                              (GANETI_RAPI_VERSION, instance)), None, body)
+
+  def GetInstanceConsole(self, instance):
+    """Request information for connecting to instance's console.
+
+    @type instance: string
+    @param instance: Instance name
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/instances/%s/console" %
+                              (GANETI_RAPI_VERSION, instance)), None, None)
+
   def GetJobs(self):
     """Gets all jobs for the cluster.
 
@@ -1284,3 +1465,149 @@ class GanetiRapiClient(object):
     return self._SendRequest(HTTP_DELETE,
                              ("/%s/nodes/%s/tags" %
                               (GANETI_RAPI_VERSION, node)), query, None)
+
+  def GetGroups(self, bulk=False):
+    """Gets all node groups in the cluster.
+
+    @type bulk: bool
+    @param bulk: whether to return all information about the groups
+
+    @rtype: list of dict or str
+    @return: if bulk is true, a list of dictionaries with info about all node
+        groups in the cluster, else a list of names of those node groups
+
+    """
+    query = []
+    if bulk:
+      query.append(("bulk", 1))
+
+    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
+                               query, None)
+    if bulk:
+      return groups
+    else:
+      return [g["name"] for g in groups]
+
+  def GetGroup(self, group):
+    """Gets information about a node group.
+
+    @type group: str
+    @param group: name of the node group whose info to return
+
+    @rtype: dict
+    @return: info about the node group
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
+                             None, None)
+
+  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
+    """Creates a new node group.
+
+    @type name: str
+    @param name: the name of node group to create
+    @type alloc_policy: str
+    @param alloc_policy: the desired allocation policy for the group, if any
+    @type dry_run: bool
+    @param dry_run: whether to peform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = []
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    body = {
+      "name": name,
+      "alloc_policy": alloc_policy
+      }
+
+    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
+                             query, body)
+
+  def ModifyGroup(self, group, **kwargs):
+    """Modifies a node group.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type group: string
+    @param group: Node group name
+    @rtype: int
+    @return: job id
+
+    """
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/groups/%s/modify" %
+                              (GANETI_RAPI_VERSION, group)), None, kwargs)
+
+  def DeleteGroup(self, group, dry_run=False):
+    """Deletes a node group.
+
+    @type group: str
+    @param group: the node group to delete
+    @type dry_run: bool
+    @param dry_run: whether to peform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = []
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_DELETE,
+                             ("/%s/groups/%s" %
+                              (GANETI_RAPI_VERSION, group)), query, None)
+
+  def RenameGroup(self, group, new_name):
+    """Changes the name of a node group.
+
+    @type group: string
+    @param group: Node group name
+    @type new_name: string
+    @param new_name: New node group name
+
+    @rtype: int
+    @return: job id
+
+    """
+    body = {
+      "new_name": new_name,
+      }
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/groups/%s/rename" %
+                              (GANETI_RAPI_VERSION, group)), None, body)
+
+
+  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
+    """Assigns nodes to a group.
+
+    @type group: string
+    @param group: Node gropu name
+    @type nodes: list of strings
+    @param nodes: List of nodes to assign to the group
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = []
+
+    if force:
+      query.append(("force", 1))
+
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    body = {
+      "nodes": nodes,
+      }
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/groups/%s/assign-nodes" %
+                             (GANETI_RAPI_VERSION, group)), query, body)