X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/d654aae119e954e5d48cf2ca9c30e7fd05c58335..26ff6ee297247a9676007f3edb1cb4a50201160d:/lib/rapi/client.py diff --git a/lib/rapi/client.py b/lib/rapi/client.py index 5edbece..a4be072 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2010 Google Inc. +# Copyright (C) 2010, 2011 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 @@ -35,9 +35,11 @@ import logging import simplejson +import socket import urllib import threading import pycurl +import time try: from cStringIO import StringIO @@ -67,10 +69,35 @@ NODE_ROLE_MASTER = "master" NODE_ROLE_OFFLINE = "offline" NODE_ROLE_REGULAR = "regular" +JOB_STATUS_QUEUED = "queued" +JOB_STATUS_WAITING = "waiting" +JOB_STATUS_CANCELING = "canceling" +JOB_STATUS_RUNNING = "running" +JOB_STATUS_CANCELED = "canceled" +JOB_STATUS_SUCCESS = "success" +JOB_STATUS_ERROR = "error" +JOB_STATUS_FINALIZED = frozenset([ + JOB_STATUS_CANCELED, + JOB_STATUS_SUCCESS, + JOB_STATUS_ERROR, + ]) +JOB_STATUS_ALL = frozenset([ + JOB_STATUS_QUEUED, + JOB_STATUS_WAITING, + JOB_STATUS_CANCELING, + JOB_STATUS_RUNNING, + ]) | JOB_STATUS_FINALIZED + +# Legacy name +JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING + # Internal constants _REQ_DATA_VERSION_FIELD = "__version__" _INST_CREATE_REQV1 = "instance-create-reqv1" -_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"]) +_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" +_NODE_MIGRATE_REQV1 = "node-migrate-reqv1" +_NODE_EVAC_RES1 = "node-evac-res1" +_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link"]) _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"]) _INST_CREATE_V0_PARAMS = frozenset([ "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check", @@ -234,7 +261,7 @@ def GenericCurlConfig(verbose=False, use_signal=False, return _ConfigCurl -class GanetiRapiClient(object): +class GanetiRapiClient(object): # pylint: disable-msg=R0904 """Ganeti RAPI client. """ @@ -265,7 +292,13 @@ class GanetiRapiClient(object): 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 if username is not None: if password is None: @@ -473,6 +506,31 @@ 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. + + @rtype: string + @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: string + @return: job id + + """ + body = kwargs + + return self._SendRequest(HTTP_PUT, + "/%s/modify" % GANETI_RAPI_VERSION, None, body) + def GetClusterTags(self): """Gets the cluster tags. @@ -491,7 +549,7 @@ class GanetiRapiClient(object): @type dry_run: bool @param dry_run: whether to perform a dry run - @rtype: int + @rtype: string @return: job id """ @@ -509,6 +567,8 @@ class GanetiRapiClient(object): @param tags: tags to delete @type dry_run: bool @param dry_run: whether to perform a dry run + @rtype: string + @return: job id """ query = [("tag", t) for t in tags] @@ -592,7 +652,7 @@ class GanetiRapiClient(object): @type dry_run: bool @keyword dry_run: whether to perform a dry run - @rtype: int + @rtype: string @return: job id """ @@ -620,82 +680,8 @@ class GanetiRapiClient(object): body.update((key, value) for key, value in kwargs.iteritems() if key != "dry_run") else: - # Old request format (version 0) - - # The following code must make sure that an exception is raised when an - # unsupported setting is requested by the caller. Otherwise this can lead - # to bugs difficult to find. The interface of this function must stay - # exactly the same for version 0 and 1 (e.g. they aren't allowed to - # require different data types). - - # Validate disks - for idx, disk in enumerate(disks): - unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS - if unsupported: - raise GanetiApiError("Server supports request version 0 only, but" - " disk %s specifies the unsupported parameters" - " %s, allowed are %s" % - (idx, unsupported, - list(_INST_CREATE_V0_DISK_PARAMS))) - - assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and - "size" in _INST_CREATE_V0_DISK_PARAMS) - disk_sizes = [disk["size"] for disk in disks] - - # Validate NICs - if not nics: - raise GanetiApiError("Server supports request version 0 only, but" - " no NIC specified") - elif len(nics) > 1: - raise GanetiApiError("Server supports request version 0 only, but" - " more than one NIC specified") - - assert len(nics) == 1 - - unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS - if unsupported: - raise GanetiApiError("Server supports request version 0 only, but" - " NIC 0 specifies the unsupported parameters %s," - " allowed are %s" % - (unsupported, list(_INST_NIC_PARAMS))) - - # Validate other parameters - unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS - - _INST_CREATE_V0_DPARAMS) - if unsupported: - allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS) - raise GanetiApiError("Server supports request version 0 only, but" - " the following unsupported parameters are" - " specified: %s, allowed are %s" % - (unsupported, list(allowed))) - - # All required fields for request data version 0 - body = { - _REQ_DATA_VERSION_FIELD: 0, - "name": name, - "disk_template": disk_template, - "disks": disk_sizes, - } - - # NIC fields - assert len(nics) == 1 - assert not (set(body.keys()) & set(nics[0].keys())) - body.update(nics[0]) - - # Copy supported fields - assert not (set(body.keys()) & set(kwargs.keys())) - body.update(dict((key, value) for key, value in kwargs.items() - if key in _INST_CREATE_V0_PARAMS)) - - # Merge dictionaries - for i in (value for key, value in kwargs.items() - if key in _INST_CREATE_V0_DPARAMS): - assert not (set(body.keys()) & set(i.keys())) - body.update(i) - - assert not (set(kwargs.keys()) - - (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS)) - assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS) + raise GanetiApiError("Server does not support new-style (version 1)" + " instance creation requests") return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION, query, body) @@ -706,7 +692,7 @@ class GanetiRapiClient(object): @type instance: str @param instance: the instance to delete - @rtype: int + @rtype: string @return: job id """ @@ -718,6 +704,84 @@ 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: string + @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 + @rtype: string + @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 + @rtype: string + @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: string + @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. @@ -742,7 +806,7 @@ class GanetiRapiClient(object): @type dry_run: bool @param dry_run: whether to perform a dry run - @rtype: int + @rtype: string @return: job id """ @@ -763,6 +827,8 @@ class GanetiRapiClient(object): @param tags: tags to delete @type dry_run: bool @param dry_run: whether to perform a dry run + @rtype: string + @return: job id """ query = [("tag", t) for t in tags] @@ -786,6 +852,8 @@ class GanetiRapiClient(object): while re-assembling disks (in hard-reboot mode only) @type dry_run: bool @param dry_run: whether to perform a dry run + @rtype: string + @return: job id """ query = [] @@ -800,41 +868,54 @@ class GanetiRapiClient(object): ("/%s/instances/%s/reboot" % (GANETI_RAPI_VERSION, instance)), query, None) - def ShutdownInstance(self, instance, dry_run=False): + def ShutdownInstance(self, instance, dry_run=False, no_remember=False): """Shuts down an instance. @type instance: str @param instance: the instance to shut down @type dry_run: bool @param dry_run: whether to perform a dry run + @type no_remember: bool + @param no_remember: if true, will not record the state change + @rtype: string + @return: job id """ query = [] if dry_run: query.append(("dry-run", 1)) + if no_remember: + query.append(("no-remember", 1)) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/shutdown" % (GANETI_RAPI_VERSION, instance)), query, None) - def StartupInstance(self, instance, dry_run=False): + def StartupInstance(self, instance, dry_run=False, no_remember=False): """Starts up an instance. @type instance: str @param instance: the instance to start up @type dry_run: bool @param dry_run: whether to perform a dry run + @type no_remember: bool + @param no_remember: if true, will not record the state change + @rtype: string + @return: job id """ query = [] if dry_run: query.append(("dry-run", 1)) + if no_remember: + query.append(("no-remember", 1)) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/startup" % (GANETI_RAPI_VERSION, instance)), query, None) - def ReinstallInstance(self, instance, os=None, no_startup=False): + def ReinstallInstance(self, instance, os=None, no_startup=False, + osparams=None): """Reinstalls an instance. @type instance: str @@ -844,8 +925,27 @@ class GanetiRapiClient(object): current operating system will be installed again @type no_startup: bool @param no_startup: Whether to start the instance automatically + @rtype: string + @return: job id """ + 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)) @@ -874,7 +974,7 @@ class GanetiRapiClient(object): @type dry_run: bool @param dry_run: whether to perform a dry run - @rtype: int + @rtype: string @return: job id """ @@ -949,7 +1049,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 @@ -957,6 +1057,8 @@ class GanetiRapiClient(object): @param mode: Migration mode @type cleanup: bool @param cleanup: Whether to clean up a previously failed migration + @rtype: string + @return: job id """ body = {} @@ -971,6 +1073,38 @@ class GanetiRapiClient(object): ("/%s/instances/%s/migrate" % (GANETI_RAPI_VERSION, instance)), None, body) + def FailoverInstance(self, instance, iallocator=None, + ignore_consistency=None, target_node=None): + """Does a failover of an instance. + + @type instance: string + @param instance: Instance name + @type iallocator: string + @param iallocator: Iallocator for deciding the target node for + shared-storage instances + @type ignore_consistency: bool + @param ignore_consistency: Whether to ignore disk consistency + @type target_node: string + @param target_node: Target node for shared-storage instances + @rtype: string + @return: job id + + """ + body = {} + + if iallocator is not None: + body["iallocator"] = iallocator + + if ignore_consistency is not None: + body["ignore_consistency"] = ignore_consistency + + if target_node is not None: + body["target_node"] = target_node + + return self._SendRequest(HTTP_PUT, + ("/%s/instances/%s/failover" % + (GANETI_RAPI_VERSION, instance)), None, body) + def RenameInstance(self, instance, new_name, ip_check=None, name_check=None): """Changes the name of an instance. @@ -982,6 +1116,8 @@ class GanetiRapiClient(object): @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 + @rtype: string + @return: job id """ body = { @@ -998,6 +1134,19 @@ class GanetiRapiClient(object): ("/%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 + @rtype: dict + @return: dictionary containing information about instance's console + + """ + return self._SendRequest(HTTP_GET, + ("/%s/instances/%s/console" % + (GANETI_RAPI_VERSION, instance)), None, None) + def GetJobs(self): """Gets all jobs for the cluster. @@ -1013,7 +1162,7 @@ class GanetiRapiClient(object): def GetJobStatus(self, job_id): """Gets the status of a job. - @type job_id: int + @type job_id: string @param job_id: job id whose status to query @rtype: dict @@ -1024,11 +1173,51 @@ class GanetiRapiClient(object): "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), None, None) + def WaitForJobCompletion(self, job_id, period=5, retries=-1): + """Polls cluster for job status until completion. + + Completion is defined as any of the following states listed in + L{JOB_STATUS_FINALIZED}. + + @type job_id: string + @param job_id: job id to watch + @type period: int + @param period: how often to poll for status (optional, default 5s) + @type retries: int + @param retries: how many time to poll before giving up + (optional, default -1 means unlimited) + + @rtype: bool + @return: C{True} if job succeeded or C{False} if failed/status timeout + @deprecated: It is recommended to use L{WaitForJobChange} wherever + possible; L{WaitForJobChange} returns immediately after a job changed and + does not use polling + + """ + while retries != 0: + job_result = self.GetJobStatus(job_id) + + if job_result and job_result["status"] == JOB_STATUS_SUCCESS: + return True + elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED: + return False + + if period: + time.sleep(period) + + if retries > 0: + retries -= 1 + + return False + def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial): """Waits for job changes. - @type job_id: int + @type job_id: string @param job_id: Job ID for which to wait + @return: C{None} if no changes have been detected and a dict with two keys, + C{job_info} and C{log_entries} otherwise. + @rtype: dict """ body = { @@ -1044,10 +1233,12 @@ class GanetiRapiClient(object): def CancelJob(self, job_id, dry_run=False): """Cancels a job. - @type job_id: int + @type job_id: string @param job_id: id of the job to delete @type dry_run: bool @param dry_run: whether to perform a dry run + @rtype: tuple + @return: tuple containing the result, and a message (bool, string) """ query = [] @@ -1095,7 +1286,8 @@ class GanetiRapiClient(object): None, None) def EvacuateNode(self, node, iallocator=None, remote_node=None, - dry_run=False, early_release=False): + dry_run=False, early_release=None, + primary=None, secondary=None, accept_old=False): """Evacuates instances from a Ganeti node. @type node: str @@ -1108,11 +1300,19 @@ class GanetiRapiClient(object): @param dry_run: whether to perform a dry run @type early_release: bool @param early_release: whether to enable parallelization - - @rtype: list - @return: list of (job ID, instance name, new secondary node); if - dry_run was specified, then the actual move jobs were not - submitted and the job IDs will be C{None} + @type primary: bool + @param primary: Whether to evacuate primary instances + @type secondary: bool + @param secondary: Whether to evacuate secondary instances + @type accept_old: bool + @param accept_old: Whether caller is ready to accept old-style (pre-2.5) + results + + @rtype: string, or a list for pre-2.5 results + @return: Job ID or, if C{accept_old} is set and server is pre-2.5, + list of (job ID, instance name, new secondary node); if dry_run was + specified, then the actual move jobs were not submitted and the job IDs + will be C{None} @raises GanetiApiError: if an iallocator and remote_node are both specified @@ -1122,20 +1322,47 @@ class GanetiRapiClient(object): raise GanetiApiError("Only one of iallocator or remote_node can be used") query = [] - if iallocator: - query.append(("iallocator", iallocator)) - if remote_node: - query.append(("remote_node", remote_node)) if dry_run: query.append(("dry-run", 1)) - if early_release: - query.append(("early_release", 1)) + + if _NODE_EVAC_RES1 in self.GetFeatures(): + body = {} + + if iallocator is not None: + body["iallocator"] = iallocator + if remote_node is not None: + body["remote_node"] = remote_node + if early_release is not None: + body["early_release"] = early_release + if primary is not None: + body["primary"] = primary + if secondary is not None: + body["secondary"] = secondary + else: + # Pre-2.5 request format + body = None + + if not accept_old: + raise GanetiApiError("Server is version 2.4 or earlier and caller does" + " not accept old-style results (parameter" + " accept_old)") + + if primary or primary is None or not (secondary is None or secondary): + raise GanetiApiError("Server can only evacuate secondary instances") + + if iallocator: + query.append(("iallocator", iallocator)) + if remote_node: + query.append(("remote_node", remote_node)) + if early_release: + query.append(("early_release", 1)) return self._SendRequest(HTTP_POST, ("/%s/nodes/%s/evacuate" % - (GANETI_RAPI_VERSION, node)), query, None) + (GANETI_RAPI_VERSION, node)), query, body) - def MigrateNode(self, node, mode=None, dry_run=False): + def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None, + target_node=None): """Migrates all primary instances from a node. @type node: str @@ -1145,20 +1372,46 @@ class GanetiRapiClient(object): otherwise the hypervisor default will be used @type dry_run: bool @param dry_run: whether to perform a dry run + @type iallocator: string + @param iallocator: instance allocator to use + @type target_node: string + @param target_node: Target node for shared-storage instances - @rtype: int + @rtype: string @return: job id """ query = [] - if mode is not None: - query.append(("mode", mode)) if dry_run: query.append(("dry-run", 1)) - return self._SendRequest(HTTP_POST, - ("/%s/nodes/%s/migrate" % - (GANETI_RAPI_VERSION, node)), query, None) + if _NODE_MIGRATE_REQV1 in self.GetFeatures(): + body = {} + + if mode is not None: + body["mode"] = mode + if iallocator is not None: + body["iallocator"] = iallocator + if target_node is not None: + body["target_node"] = target_node + + assert len(query) <= 1 + + return self._SendRequest(HTTP_POST, + ("/%s/nodes/%s/migrate" % + (GANETI_RAPI_VERSION, node)), query, body) + else: + # Use old request format + if target_node is not None: + raise GanetiApiError("Server does not support specifying target node" + " for node migration") + + if mode is not None: + query.append(("mode", mode)) + + return self._SendRequest(HTTP_POST, + ("/%s/nodes/%s/migrate" % + (GANETI_RAPI_VERSION, node)), query, None) def GetNodeRole(self, node): """Gets the current role for a node. @@ -1184,7 +1437,7 @@ class GanetiRapiClient(object): @type force: bool @param force: whether to force the role change - @rtype: int + @rtype: string @return: job id """ @@ -1206,7 +1459,7 @@ class GanetiRapiClient(object): @type output_fields: str @param output_fields: storage type fields to return - @rtype: int + @rtype: string @return: job id where results can be retrieved """ @@ -1232,7 +1485,7 @@ class GanetiRapiClient(object): @param allocatable: Whether to set the "allocatable" flag on the storage unit (None=no modification, True=set, False=unset) - @rtype: int + @rtype: string @return: job id """ @@ -1258,7 +1511,7 @@ class GanetiRapiClient(object): @type name: str @param name: name of the storage unit to repair - @rtype: int + @rtype: string @return: job id """ @@ -1295,7 +1548,7 @@ class GanetiRapiClient(object): @type dry_run: bool @param dry_run: whether to perform a dry run - @rtype: int + @rtype: string @return: job id """ @@ -1317,7 +1570,7 @@ class GanetiRapiClient(object): @type dry_run: bool @param dry_run: whether to perform a dry run - @rtype: int + @rtype: string @return: job id """ @@ -1328,3 +1581,251 @@ 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: string + @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: string + @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: string + @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: string + @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: string + @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) + + def GetGroupTags(self, group): + """Gets tags for a node group. + + @type group: string + @param group: Node group whose tags to return + + @rtype: list of strings + @return: tags for the group + + """ + return self._SendRequest(HTTP_GET, + ("/%s/groups/%s/tags" % + (GANETI_RAPI_VERSION, group)), None, None) + + def AddGroupTags(self, group, tags, dry_run=False): + """Adds tags to a node group. + + @type group: str + @param group: group to add tags to + @type tags: list of string + @param tags: tags to add to the group + @type dry_run: bool + @param dry_run: whether to perform a dry run + + @rtype: string + @return: job id + + """ + query = [("tag", t) for t in tags] + if dry_run: + query.append(("dry-run", 1)) + + return self._SendRequest(HTTP_PUT, + ("/%s/groups/%s/tags" % + (GANETI_RAPI_VERSION, group)), query, None) + + def DeleteGroupTags(self, group, tags, dry_run=False): + """Deletes tags from a node group. + + @type group: str + @param group: group to delete tags from + @type tags: list of string + @param tags: tags to delete + @type dry_run: bool + @param dry_run: whether to perform a dry run + @rtype: string + @return: job id + + """ + query = [("tag", t) for t in tags] + if dry_run: + query.append(("dry-run", 1)) + + return self._SendRequest(HTTP_DELETE, + ("/%s/groups/%s/tags" % + (GANETI_RAPI_VERSION, group)), query, None) + + def Query(self, what, fields, filter_=None): + """Retrieves information about resources. + + @type what: string + @param what: Resource name, one of L{constants.QR_VIA_RAPI} + @type fields: list of string + @param fields: Requested fields + @type filter_: None or list + @param filter_: Query filter + + @rtype: string + @return: job id + + """ + body = { + "fields": fields, + } + + if filter_ is not None: + body["filter"] = filter_ + + return self._SendRequest(HTTP_PUT, + ("/%s/query/%s" % + (GANETI_RAPI_VERSION, what)), None, body) + + def QueryFields(self, what, fields=None): + """Retrieves available fields for a resource. + + @type what: string + @param what: Resource name, one of L{constants.QR_VIA_RAPI} + @type fields: list of string + @param fields: Requested fields + + @rtype: string + @return: job id + + """ + query = [] + + if fields is not None: + query.append(("fields", ",".join(fields))) + + return self._SendRequest(HTTP_GET, + ("/%s/query/%s/fields" % + (GANETI_RAPI_VERSION, what)), query, None)