X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/539d65ba090ea0c10336d40774d59fbf67b07b2d..1b2adaa67411daf827d91f5169e859a1571200b8:/lib/rapi/client.py diff --git a/lib/rapi/client.py b/lib/rapi/client.py index be39202..35b1191 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2010, 2011 Google Inc. +# Copyright (C) 2010, 2011, 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 @@ -63,6 +63,10 @@ REPLACE_DISK_SECONDARY = "replace_on_secondary" REPLACE_DISK_CHG = "replace_new_secondary" REPLACE_DISK_AUTO = "replace_auto" +NODE_EVAC_PRI = "primary-only" +NODE_EVAC_SEC = "secondary-only" +NODE_EVAC_ALL = "all" + NODE_ROLE_DRAINED = "drained" NODE_ROLE_MASTER_CANDIATE = "master-candidate" NODE_ROLE_MASTER = "master" @@ -76,34 +80,83 @@ JOB_STATUS_RUNNING = "running" JOB_STATUS_CANCELED = "canceled" JOB_STATUS_SUCCESS = "success" JOB_STATUS_ERROR = "error" +JOB_STATUS_PENDING = frozenset([ + JOB_STATUS_QUEUED, + JOB_STATUS_WAITING, + JOB_STATUS_CANCELING, + ]) 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 + ]) | JOB_STATUS_PENDING | 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_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", - "hypervisor", "file_storage_dir", "file_driver", "dry_run", +_QPARAM_DRY_RUN = "dry-run" +_QPARAM_FORCE = "force" + +# Feature strings +INST_CREATE_REQV1 = "instance-create-reqv1" +INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" +NODE_MIGRATE_REQV1 = "node-migrate-reqv1" +NODE_EVAC_RES1 = "node-evac-res1" + +# Old feature constant names in case they're references by users of this module +_INST_CREATE_REQV1 = INST_CREATE_REQV1 +_INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1 +_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1 +_NODE_EVAC_RES1 = NODE_EVAC_RES1 + +#: Resolver errors +ECODE_RESOLVER = "resolver_error" + +#: Not enough resources (iallocator failure, disk space, memory, etc.) +ECODE_NORES = "insufficient_resources" + +#: Temporarily out of resources; operation can be tried again +ECODE_TEMP_NORES = "temp_insufficient_resources" + +#: Wrong arguments (at syntax level) +ECODE_INVAL = "wrong_input" + +#: Wrong entity state +ECODE_STATE = "wrong_state" + +#: Entity not found +ECODE_NOENT = "unknown_entity" + +#: Entity already exists +ECODE_EXISTS = "already_exists" + +#: Resource not unique (e.g. MAC or IP duplication) +ECODE_NOTUNIQUE = "resource_not_unique" + +#: Internal cluster error +ECODE_FAULT = "internal_error" + +#: Environment error (e.g. node disk error) +ECODE_ENVIRON = "environment_error" + +#: List of all failure types +ECODE_ALL = frozenset([ + ECODE_RESOLVER, + ECODE_NORES, + ECODE_TEMP_NORES, + ECODE_INVAL, + ECODE_STATE, + ECODE_NOENT, + ECODE_EXISTS, + ECODE_NOTUNIQUE, + ECODE_FAULT, + ECODE_ENVIRON, ]) -_INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"]) # Older pycURL versions don't have all error constants try: @@ -126,20 +179,54 @@ class Error(Exception): pass -class CertificateError(Error): +class GanetiApiError(Error): + """Generic error raised from Ganeti API. + + """ + def __init__(self, msg, code=None): + Error.__init__(self, msg) + self.code = code + + +class CertificateError(GanetiApiError): """Raised when a problem is found with the SSL certificate. """ pass -class GanetiApiError(Error): - """Generic error raised from Ganeti API. +def _AppendIf(container, condition, value): + """Appends to a list if a condition evaluates to truth. """ - def __init__(self, msg, code=None): - Error.__init__(self, msg) - self.code = code + if condition: + container.append(value) + + return condition + + +def _AppendDryRunIf(container, condition): + """Appends a "dry-run" parameter if a condition evaluates to truth. + + """ + return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1)) + + +def _AppendForceIf(container, condition): + """Appends a "force" parameter if a condition evaluates to truth. + + """ + return _AppendIf(container, condition, (_QPARAM_FORCE, 1)) + + +def _SetItemIf(container, condition, item, value): + """Sets an item if a condition evaluates to truth. + + """ + if condition: + container[item] = value + + return condition def UsesRapiClient(fn): @@ -215,6 +302,9 @@ def GenericCurlConfig(verbose=False, use_signal=False, lcsslver = sslver.lower() if lcsslver.startswith("openssl/"): pass + elif lcsslver.startswith("nss/"): + # TODO: investigate compatibility beyond a simple test + pass elif lcsslver.startswith("gnutls/"): if capath: raise Error("cURL linked against GnuTLS has no support for a" @@ -429,9 +519,10 @@ class GanetiRapiClient(object): # pylint: disable=R0904 curl.perform() except pycurl.error, err: if err.args[0] in _CURL_SSL_CERT_ERRORS: - raise CertificateError("SSL certificate error %s" % err) + raise CertificateError("SSL certificate error %s" % err, + code=err.args[0]) - raise GanetiApiError(str(err)) + raise GanetiApiError(str(err), code=err.args[0]) finally: # Reset settings to not keep references to large objects in memory # between requests @@ -554,8 +645,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [("tag", t) for t in tags] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION, query, None) @@ -572,8 +662,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [("tag", t) for t in tags] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION, query, None) @@ -589,8 +678,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if bulk: - query.append(("bulk", 1)) + _AppendIf(query, bulk, ("bulk", 1)) instances = self._SendRequest(HTTP_GET, "/%s/instances" % GANETI_RAPI_VERSION, @@ -632,6 +720,78 @@ class GanetiRapiClient(object): # pylint: disable=R0904 ("/%s/instances/%s/info" % (GANETI_RAPI_VERSION, instance)), query, None) + @staticmethod + def _UpdateWithKwargs(base, **kwargs): + """Updates the base with params from kwargs. + + @param base: The base dict, filled with required fields + + @note: This is an inplace update of base + + """ + conflicts = set(kwargs.iterkeys()) & set(base.iterkeys()) + if conflicts: + raise GanetiApiError("Required fields can not be specified as" + " keywords: %s" % ", ".join(conflicts)) + + base.update((key, value) for key, value in kwargs.iteritems() + if key != "dry_run") + + def InstanceAllocation(self, mode, name, disk_template, disks, nics, + **kwargs): + """Generates an instance allocation as used by multiallocate. + + More details for parameters can be found in the RAPI documentation. + It is the same as used by CreateInstance. + + @type mode: string + @param mode: Instance creation mode + @type name: string + @param name: Hostname of the instance to create + @type disk_template: string + @param disk_template: Disk template for instance (e.g. plain, diskless, + file, or drbd) + @type disks: list of dicts + @param disks: List of disk definitions + @type nics: list of dicts + @param nics: List of NIC definitions + + @return: A dict with the generated entry + + """ + # All required fields for request data version 1 + alloc = { + "mode": mode, + "name": name, + "disk_template": disk_template, + "disks": disks, + "nics": nics, + } + + self._UpdateWithKwargs(alloc, **kwargs) + + return alloc + + def InstancesMultiAlloc(self, instances, **kwargs): + """Tries to allocate multiple instances. + + More details for parameters can be found in the RAPI documentation. + + @param instances: A list of L{InstanceAllocation} results + + """ + query = [] + body = { + "instances": instances, + } + self._UpdateWithKwargs(body, **kwargs) + + _AppendDryRunIf(query, kwargs.get("dry_run")) + + return self._SendRequest(HTTP_POST, + "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION, + query, body) + def CreateInstance(self, mode, name, disk_template, disks, nics, **kwargs): """Creates a new instance. @@ -658,27 +818,12 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if kwargs.get("dry_run"): - query.append(("dry-run", 1)) + _AppendDryRunIf(query, kwargs.get("dry_run")) if _INST_CREATE_REQV1 in self.GetFeatures(): - # All required fields for request data version 1 - body = { - _REQ_DATA_VERSION_FIELD: 1, - "mode": mode, - "name": name, - "disk_template": disk_template, - "disks": disks, - "nics": nics, - } - - conflicts = set(kwargs.iterkeys()) & set(body.iterkeys()) - if conflicts: - raise GanetiApiError("Required fields can not be specified as" - " keywords: %s" % ", ".join(conflicts)) - - body.update((key, value) for key, value in kwargs.iteritems() - if key != "dry_run") + body = self.InstanceAllocation(mode, name, disk_template, disks, nics, + **kwargs) + body[_REQ_DATA_VERSION_FIELD] = 1 else: raise GanetiApiError("Server does not support new-style (version 1)" " instance creation requests") @@ -697,8 +842,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_DELETE, ("/%s/instances/%s" % @@ -733,8 +877,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if ignore_size: - query.append(("ignore_size", 1)) + _AppendIf(query, ignore_size, ("ignore_size", 1)) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/activate-disks" % @@ -753,6 +896,27 @@ class GanetiRapiClient(object): # pylint: disable=R0904 ("/%s/instances/%s/deactivate-disks" % (GANETI_RAPI_VERSION, instance)), None, None) + def RecreateInstanceDisks(self, instance, disks=None, nodes=None): + """Recreate an instance's disks. + + @type instance: string + @param instance: Instance name + @type disks: list of int + @param disks: List of disk indexes + @type nodes: list of string + @param nodes: New instance nodes, if relocation is desired + @rtype: string + @return: job id + + """ + body = {} + _SetItemIf(body, disks is not None, "disks", disks) + _SetItemIf(body, nodes is not None, "nodes", nodes) + + return self._SendRequest(HTTP_POST, + ("/%s/instances/%s/recreate-disks" % + (GANETI_RAPI_VERSION, instance)), None, body) + def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None): """Grows a disk of an instance. @@ -774,8 +938,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 "amount": amount, } - if wait_for_sync is not None: - body["wait_for_sync"] = wait_for_sync + _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync) return self._SendRequest(HTTP_POST, ("/%s/instances/%s/disk/%s/grow" % @@ -811,8 +974,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [("tag", t) for t in tags] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/tags" % @@ -832,19 +994,18 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [("tag", t) for t in tags] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_DELETE, ("/%s/instances/%s/tags" % (GANETI_RAPI_VERSION, instance)), query, None) def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None, - dry_run=False): + dry_run=False, reason=None): """Reboots an instance. @type instance: str - @param instance: instance to rebot + @param instance: instance to reboot @type reboot_type: str @param reboot_type: one of: hard, soft, full @type ignore_secondaries: bool @@ -852,23 +1013,25 @@ class GanetiRapiClient(object): # pylint: disable=R0904 while re-assembling disks (in hard-reboot mode only) @type dry_run: bool @param dry_run: whether to perform a dry run + @type reason: string + @param reason: the reason for the reboot @rtype: string @return: job id """ query = [] - if reboot_type: - query.append(("type", reboot_type)) - if ignore_secondaries is not None: - query.append(("ignore_secondaries", ignore_secondaries)) - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) + _AppendIf(query, reboot_type, ("type", reboot_type)) + _AppendIf(query, ignore_secondaries is not None, + ("ignore_secondaries", ignore_secondaries)) + _AppendIf(query, reason, ("reason", reason)) return self._SendRequest(HTTP_POST, ("/%s/instances/%s/reboot" % (GANETI_RAPI_VERSION, instance)), query, None) - def ShutdownInstance(self, instance, dry_run=False, no_remember=False): + def ShutdownInstance(self, instance, dry_run=False, no_remember=False, + reason=None, **kwargs): """Shuts down an instance. @type instance: str @@ -877,21 +1040,25 @@ class GanetiRapiClient(object): # pylint: disable=R0904 @param dry_run: whether to perform a dry run @type no_remember: bool @param no_remember: if true, will not record the state change + @type reason: string + @param reason: the reason for the shutdown @rtype: string @return: job id """ query = [] - if dry_run: - query.append(("dry-run", 1)) - if no_remember: - query.append(("no-remember", 1)) + body = kwargs + + _AppendDryRunIf(query, dry_run) + _AppendIf(query, no_remember, ("no_remember", 1)) + _AppendIf(query, reason, ("reason", reason)) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/shutdown" % - (GANETI_RAPI_VERSION, instance)), query, None) + (GANETI_RAPI_VERSION, instance)), query, body) - def StartupInstance(self, instance, dry_run=False, no_remember=False): + def StartupInstance(self, instance, dry_run=False, no_remember=False, + reason=None): """Starts up an instance. @type instance: str @@ -900,15 +1067,16 @@ class GanetiRapiClient(object): # pylint: disable=R0904 @param dry_run: whether to perform a dry run @type no_remember: bool @param no_remember: if true, will not record the state change + @type reason: string + @param reason: the reason for the startup @rtype: string @return: job id """ query = [] - if dry_run: - query.append(("dry-run", 1)) - if no_remember: - query.append(("no-remember", 1)) + _AppendDryRunIf(query, dry_run) + _AppendIf(query, no_remember, ("no_remember", 1)) + _AppendIf(query, reason, ("reason", reason)) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/startup" % @@ -933,10 +1101,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904 body = { "start": not no_startup, } - if os is not None: - body["os"] = os - if osparams is not None: - body["osparams"] = osparams + _SetItemIf(body, os is not None, "os", os) + _SetItemIf(body, osparams is not None, "osparams", osparams) return self._SendRequest(HTTP_POST, ("/%s/instances/%s/reinstall" % (GANETI_RAPI_VERSION, instance)), None, body) @@ -947,10 +1113,9 @@ class GanetiRapiClient(object): # pylint: disable=R0904 " for instance reinstallation") query = [] - if os: - query.append(("os", os)) - if no_startup: - query.append(("nostartup", 1)) + _AppendIf(query, os, ("os", os)) + _AppendIf(query, no_startup, ("nostartup", 1)) + return self._SendRequest(HTTP_POST, ("/%s/instances/%s/reinstall" % (GANETI_RAPI_VERSION, instance)), query, None) @@ -983,13 +1148,11 @@ class GanetiRapiClient(object): # pylint: disable=R0904 # TODO: Convert to body parameters if disks is not None: - query.append(("disks", ",".join(str(idx) for idx in disks))) - - if remote_node is not None: - query.append(("remote_node", remote_node)) + _AppendIf(query, True, + ("disks", ",".join(str(idx) for idx in disks))) - if iallocator is not None: - query.append(("iallocator", iallocator)) + _AppendIf(query, remote_node is not None, ("remote_node", remote_node)) + _AppendIf(query, iallocator is not None, ("iallocator", iallocator)) return self._SendRequest(HTTP_POST, ("/%s/instances/%s/replace-disks" % @@ -1029,23 +1192,19 @@ class GanetiRapiClient(object): # pylint: disable=R0904 "mode": mode, } - if shutdown is not None: - body["shutdown"] = shutdown - - if remove_instance is not None: - body["remove_instance"] = remove_instance - - if x509_key_name is not None: - body["x509_key_name"] = x509_key_name - - if destination_x509_ca is not None: - body["destination_x509_ca"] = destination_x509_ca + _SetItemIf(body, shutdown is not None, "shutdown", shutdown) + _SetItemIf(body, remove_instance is not None, + "remove_instance", remove_instance) + _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name) + _SetItemIf(body, destination_x509_ca is not None, + "destination_x509_ca", destination_x509_ca) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/export" % (GANETI_RAPI_VERSION, instance)), None, body) - def MigrateInstance(self, instance, mode=None, cleanup=None): + def MigrateInstance(self, instance, mode=None, cleanup=None, + target_node=None): """Migrates an instance. @type instance: string @@ -1054,17 +1213,16 @@ class GanetiRapiClient(object): # pylint: disable=R0904 @param mode: Migration mode @type cleanup: bool @param cleanup: Whether to clean up a previously failed migration + @type target_node: string + @param target_node: Target Node for externally mirrored instances @rtype: string @return: job id """ body = {} - - if mode is not None: - body["mode"] = mode - - if cleanup is not None: - body["cleanup"] = cleanup + _SetItemIf(body, mode is not None, "mode", mode) + _SetItemIf(body, cleanup is not None, "cleanup", cleanup) + _SetItemIf(body, target_node is not None, "target_node", target_node) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/migrate" % @@ -1088,15 +1246,10 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ 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 + _SetItemIf(body, iallocator is not None, "iallocator", iallocator) + _SetItemIf(body, ignore_consistency is not None, + "ignore_consistency", ignore_consistency) + _SetItemIf(body, target_node is not None, "target_node", target_node) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/failover" % @@ -1121,11 +1274,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904 "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 + _SetItemIf(body, ip_check is not None, "ip_check", ip_check) + _SetItemIf(body, name_check is not None, "name_check", name_check) return self._SendRequest(HTTP_PUT, ("/%s/instances/%s/rename" % @@ -1144,17 +1294,28 @@ class GanetiRapiClient(object): # pylint: disable=R0904 ("/%s/instances/%s/console" % (GANETI_RAPI_VERSION, instance)), None, None) - def GetJobs(self): + def GetJobs(self, bulk=False): """Gets all jobs for the cluster. + @type bulk: bool + @param bulk: Whether to return detailed information about jobs. @rtype: list of int - @return: job ids for the cluster + @return: List of job ids for the cluster or list of dicts with detailed + information about the jobs if bulk parameter was true. """ - return [int(j["id"]) - for j in self._SendRequest(HTTP_GET, - "/%s/jobs" % GANETI_RAPI_VERSION, - None, None)] + query = [] + _AppendIf(query, bulk, ("bulk", 1)) + + if bulk: + return self._SendRequest(HTTP_GET, + "/%s/jobs" % GANETI_RAPI_VERSION, + query, None) + else: + return [int(j["id"]) + for j in self._SendRequest(HTTP_GET, + "/%s/jobs" % GANETI_RAPI_VERSION, + None, None)] def GetJobStatus(self, job_id): """Gets the status of a job. @@ -1239,8 +1400,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_DELETE, "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), @@ -1258,8 +1418,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if bulk: - query.append(("bulk", 1)) + _AppendIf(query, bulk, ("bulk", 1)) nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION, query, None) @@ -1284,7 +1443,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 def EvacuateNode(self, node, iallocator=None, remote_node=None, dry_run=False, early_release=None, - primary=None, secondary=None, accept_old=False): + mode=None, accept_old=False): """Evacuates instances from a Ganeti node. @type node: str @@ -1297,10 +1456,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904 @param dry_run: whether to perform a dry run @type early_release: bool @param early_release: whether to enable parallelization - @type primary: bool - @param primary: Whether to evacuate primary instances - @type secondary: bool - @param secondary: Whether to evacuate secondary instances + @type mode: string + @param mode: Node evacuation mode @type accept_old: bool @param accept_old: Whether caller is ready to accept old-style (pre-2.5) results @@ -1319,22 +1476,17 @@ class GanetiRapiClient(object): # pylint: disable=R0904 raise GanetiApiError("Only one of iallocator or remote_node can be used") query = [] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) if _NODE_EVAC_RES1 in self.GetFeatures(): + # Server supports body parameters 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 + _SetItemIf(body, iallocator is not None, "iallocator", iallocator) + _SetItemIf(body, remote_node is not None, "remote_node", remote_node) + _SetItemIf(body, early_release is not None, + "early_release", early_release) + _SetItemIf(body, mode is not None, "mode", mode) else: # Pre-2.5 request format body = None @@ -1344,15 +1496,13 @@ class GanetiRapiClient(object): # pylint: disable=R0904 " not accept old-style results (parameter" " accept_old)") - if primary or primary is None or not (secondary is None or secondary): + # Pre-2.5 servers can only evacuate secondaries + if mode is not None and mode != NODE_EVAC_SEC: 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)) + _AppendIf(query, iallocator, ("iallocator", iallocator)) + _AppendIf(query, remote_node, ("remote_node", remote_node)) + _AppendIf(query, early_release, ("early_release", 1)) return self._SendRequest(HTTP_POST, ("/%s/nodes/%s/evacuate" % @@ -1379,18 +1529,14 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) 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 + _SetItemIf(body, mode is not None, "mode", mode) + _SetItemIf(body, iallocator is not None, "iallocator", iallocator) + _SetItemIf(body, target_node is not None, "target_node", target_node) assert len(query) <= 1 @@ -1403,8 +1549,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 raise GanetiApiError("Server does not support specifying target node" " for node migration") - if mode is not None: - query.append(("mode", mode)) + _AppendIf(query, mode is not None, ("mode", mode)) return self._SendRequest(HTTP_POST, ("/%s/nodes/%s/migrate" % @@ -1424,7 +1569,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 ("/%s/nodes/%s/role" % (GANETI_RAPI_VERSION, node)), None, None) - def SetNodeRole(self, node, role, force=False): + def SetNodeRole(self, node, role, force=False, auto_promote=None): """Sets the role for a node. @type node: str @@ -1433,19 +1578,55 @@ class GanetiRapiClient(object): # pylint: disable=R0904 @param role: the role to set for the node @type force: bool @param force: whether to force the role change + @type auto_promote: bool + @param auto_promote: Whether node(s) should be promoted to master candidate + if necessary @rtype: string @return: job id """ - query = [ - ("force", force), - ] + query = [] + _AppendForceIf(query, force) + _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote)) return self._SendRequest(HTTP_PUT, ("/%s/nodes/%s/role" % (GANETI_RAPI_VERSION, node)), query, role) + def PowercycleNode(self, node, force=False): + """Powercycles a node. + + @type node: string + @param node: Node name + @type force: bool + @param force: Whether to force the operation + @rtype: string + @return: job id + + """ + query = [] + _AppendForceIf(query, force) + + return self._SendRequest(HTTP_POST, + ("/%s/nodes/%s/powercycle" % + (GANETI_RAPI_VERSION, node)), query, None) + + def ModifyNode(self, node, **kwargs): + """Modifies a node. + + More details for parameters can be found in the RAPI documentation. + + @type node: string + @param node: Node name + @rtype: string + @return: job id + + """ + return self._SendRequest(HTTP_POST, + ("/%s/nodes/%s/modify" % + (GANETI_RAPI_VERSION, node)), None, kwargs) + def GetNodeStorageUnits(self, node, storage_type, output_fields): """Gets the storage units for a node. @@ -1491,8 +1672,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 ("name", name), ] - if allocatable is not None: - query.append(("allocatable", allocatable)) + _AppendIf(query, allocatable is not None, ("allocatable", allocatable)) return self._SendRequest(HTTP_PUT, ("/%s/nodes/%s/storage/modify" % @@ -1550,8 +1730,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [("tag", t) for t in tags] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_PUT, ("/%s/nodes/%s/tags" % @@ -1572,13 +1751,205 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [("tag", t) for t in tags] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_DELETE, ("/%s/nodes/%s/tags" % (GANETI_RAPI_VERSION, node)), query, None) + def GetNetworks(self, bulk=False): + """Gets all networks in the cluster. + + @type bulk: bool + @param bulk: whether to return all information about the networks + + @rtype: list of dict or str + @return: if bulk is true, a list of dictionaries with info about all + networks in the cluster, else a list of names of those networks + + """ + query = [] + _AppendIf(query, bulk, ("bulk", 1)) + + networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION, + query, None) + if bulk: + return networks + else: + return [n["name"] for n in networks] + + def GetNetwork(self, network): + """Gets information about a network. + + @type network: str + @param network: name of the network whose info to return + + @rtype: dict + @return: info about the network + + """ + return self._SendRequest(HTTP_GET, + "/%s/networks/%s" % (GANETI_RAPI_VERSION, network), + None, None) + + def CreateNetwork(self, network_name, network, gateway=None, network6=None, + gateway6=None, mac_prefix=None, + add_reserved_ips=None, tags=None, dry_run=False): + """Creates a new network. + + @type network_name: str + @param network_name: the name of network to create + @type dry_run: bool + @param dry_run: whether to peform a dry run + + @rtype: string + @return: job id + + """ + query = [] + _AppendDryRunIf(query, dry_run) + + if add_reserved_ips: + add_reserved_ips = add_reserved_ips.split(",") + + if tags: + tags = tags.split(",") + + body = { + "network_name": network_name, + "gateway": gateway, + "network": network, + "gateway6": gateway6, + "network6": network6, + "mac_prefix": mac_prefix, + "add_reserved_ips": add_reserved_ips, + "tags": tags, + } + + return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION, + query, body) + + def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False): + """Connects a Network to a NodeGroup with the given netparams + + """ + body = { + "group_name": group_name, + "network_mode": mode, + "network_link": link, + } + + query = [] + _AppendDryRunIf(query, dry_run) + + return self._SendRequest(HTTP_PUT, + ("/%s/networks/%s/connect" % + (GANETI_RAPI_VERSION, network_name)), query, body) + + def DisconnectNetwork(self, network_name, group_name, dry_run=False): + """Connects a Network to a NodeGroup with the given netparams + + """ + body = { + "group_name": group_name, + } + + query = [] + _AppendDryRunIf(query, dry_run) + + return self._SendRequest(HTTP_PUT, + ("/%s/networks/%s/disconnect" % + (GANETI_RAPI_VERSION, network_name)), query, body) + + def ModifyNetwork(self, network, **kwargs): + """Modifies a network. + + More details for parameters can be found in the RAPI documentation. + + @type network: string + @param network: Network name + @rtype: string + @return: job id + + """ + return self._SendRequest(HTTP_PUT, + ("/%s/networks/%s/modify" % + (GANETI_RAPI_VERSION, network)), None, kwargs) + + def DeleteNetwork(self, network, dry_run=False): + """Deletes a network. + + @type network: str + @param network: the network to delete + @type dry_run: bool + @param dry_run: whether to peform a dry run + + @rtype: string + @return: job id + + """ + query = [] + _AppendDryRunIf(query, dry_run) + + return self._SendRequest(HTTP_DELETE, + ("/%s/networks/%s" % + (GANETI_RAPI_VERSION, network)), query, None) + + def GetNetworkTags(self, network): + """Gets tags for a network. + + @type network: string + @param network: Node group whose tags to return + + @rtype: list of strings + @return: tags for the network + + """ + return self._SendRequest(HTTP_GET, + ("/%s/networks/%s/tags" % + (GANETI_RAPI_VERSION, network)), None, None) + + def AddNetworkTags(self, network, tags, dry_run=False): + """Adds tags to a network. + + @type network: str + @param network: network to add tags to + @type tags: list of string + @param tags: tags to add to the network + @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] + _AppendDryRunIf(query, dry_run) + + return self._SendRequest(HTTP_PUT, + ("/%s/networks/%s/tags" % + (GANETI_RAPI_VERSION, network)), query, None) + + def DeleteNetworkTags(self, network, tags, dry_run=False): + """Deletes tags from a network. + + @type network: str + @param network: network 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] + _AppendDryRunIf(query, dry_run) + + return self._SendRequest(HTTP_DELETE, + ("/%s/networks/%s/tags" % + (GANETI_RAPI_VERSION, network)), query, None) + def GetGroups(self, bulk=False): """Gets all node groups in the cluster. @@ -1591,8 +1962,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if bulk: - query.append(("bulk", 1)) + _AppendIf(query, bulk, ("bulk", 1)) groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION, query, None) @@ -1630,12 +2000,11 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) body = { "name": name, - "alloc_policy": alloc_policy + "alloc_policy": alloc_policy, } return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION, @@ -1669,8 +2038,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_DELETE, ("/%s/groups/%s" % @@ -1700,7 +2068,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """Assigns nodes to a group. @type group: string - @param group: Node gropu name + @param group: Node group name @type nodes: list of strings @param nodes: List of nodes to assign to the group @@ -1709,12 +2077,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [] - - if force: - query.append(("force", 1)) - - if dry_run: - query.append(("dry-run", 1)) + _AppendForceIf(query, force) + _AppendDryRunIf(query, dry_run) body = { "nodes": nodes, @@ -1753,8 +2117,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [("tag", t) for t in tags] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_PUT, ("/%s/groups/%s/tags" % @@ -1774,22 +2137,21 @@ class GanetiRapiClient(object): # pylint: disable=R0904 """ query = [("tag", t) for t in tags] - if dry_run: - query.append(("dry-run", 1)) + _AppendDryRunIf(query, dry_run) return self._SendRequest(HTTP_DELETE, ("/%s/groups/%s/tags" % (GANETI_RAPI_VERSION, group)), query, None) - def Query(self, what, fields, filter_=None): + def Query(self, what, fields, qfilter=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 + @type qfilter: None or list + @param qfilter: Query filter @rtype: string @return: job id @@ -1799,8 +2161,9 @@ class GanetiRapiClient(object): # pylint: disable=R0904 "fields": fields, } - if filter_ is not None: - body["filter"] = filter_ + _SetItemIf(body, qfilter is not None, "qfilter", qfilter) + # TODO: remove "filter" after 2.7 + _SetItemIf(body, qfilter is not None, "filter", qfilter) return self._SendRequest(HTTP_PUT, ("/%s/query/%s" % @@ -1821,7 +2184,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 query = [] if fields is not None: - query.append(("fields", ",".join(fields))) + _AppendIf(query, True, ("fields", ",".join(fields))) return self._SendRequest(HTTP_GET, ("/%s/query/%s/fields" %