Package ganeti :: Package rapi :: Module client
[hide private]
[frames] | no frames]

Source Code for Module ganeti.rapi.client

   1  # 
   2  # 
   3   
   4  # Copyright (C) 2010, 2011 Google Inc. 
   5  # 
   6  # This program is free software; you can redistribute it and/or modify 
   7  # it under the terms of the GNU General Public License as published by 
   8  # the Free Software Foundation; either version 2 of the License, or 
   9  # (at your option) any later version. 
  10  # 
  11  # This program is distributed in the hope that it will be useful, but 
  12  # WITHOUT ANY WARRANTY; without even the implied warranty of 
  13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
  14  # General Public License for more details. 
  15  # 
  16  # You should have received a copy of the GNU General Public License 
  17  # along with this program; if not, write to the Free Software 
  18  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 
  19  # 02110-1301, USA. 
  20   
  21   
  22  """Ganeti RAPI client. 
  23   
  24  @attention: To use the RAPI client, the application B{must} call 
  25              C{pycurl.global_init} during initialization and 
  26              C{pycurl.global_cleanup} before exiting the process. This is very 
  27              important in multi-threaded programs. See curl_global_init(3) and 
  28              curl_global_cleanup(3) for details. The decorator L{UsesRapiClient} 
  29              can be used. 
  30   
  31  """ 
  32   
  33  # No Ganeti-specific modules should be imported. The RAPI client is supposed to 
  34  # be standalone. 
  35   
  36  import logging 
  37  import simplejson 
  38  import socket 
  39  import urllib 
  40  import threading 
  41  import pycurl 
  42  import time 
  43   
  44  try: 
  45    from cStringIO import StringIO 
  46  except ImportError: 
  47    from StringIO import StringIO 
  48   
  49   
  50  GANETI_RAPI_PORT = 5080 
  51  GANETI_RAPI_VERSION = 2 
  52   
  53  HTTP_DELETE = "DELETE" 
  54  HTTP_GET = "GET" 
  55  HTTP_PUT = "PUT" 
  56  HTTP_POST = "POST" 
  57  HTTP_OK = 200 
  58  HTTP_NOT_FOUND = 404 
  59  HTTP_APP_JSON = "application/json" 
  60   
  61  REPLACE_DISK_PRI = "replace_on_primary" 
  62  REPLACE_DISK_SECONDARY = "replace_on_secondary" 
  63  REPLACE_DISK_CHG = "replace_new_secondary" 
  64  REPLACE_DISK_AUTO = "replace_auto" 
  65   
  66  NODE_EVAC_PRI = "primary-only" 
  67  NODE_EVAC_SEC = "secondary-only" 
  68  NODE_EVAC_ALL = "all" 
  69   
  70  NODE_ROLE_DRAINED = "drained" 
  71  NODE_ROLE_MASTER_CANDIATE = "master-candidate" 
  72  NODE_ROLE_MASTER = "master" 
  73  NODE_ROLE_OFFLINE = "offline" 
  74  NODE_ROLE_REGULAR = "regular" 
  75   
  76  JOB_STATUS_QUEUED = "queued" 
  77  JOB_STATUS_WAITING = "waiting" 
  78  JOB_STATUS_CANCELING = "canceling" 
  79  JOB_STATUS_RUNNING = "running" 
  80  JOB_STATUS_CANCELED = "canceled" 
  81  JOB_STATUS_SUCCESS = "success" 
  82  JOB_STATUS_ERROR = "error" 
  83  JOB_STATUS_FINALIZED = frozenset([ 
  84    JOB_STATUS_CANCELED, 
  85    JOB_STATUS_SUCCESS, 
  86    JOB_STATUS_ERROR, 
  87    ]) 
  88  JOB_STATUS_ALL = frozenset([ 
  89    JOB_STATUS_QUEUED, 
  90    JOB_STATUS_WAITING, 
  91    JOB_STATUS_CANCELING, 
  92    JOB_STATUS_RUNNING, 
  93    ]) | JOB_STATUS_FINALIZED 
  94   
  95  # Legacy name 
  96  JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING 
  97   
  98  # Internal constants 
  99  _REQ_DATA_VERSION_FIELD = "__version__" 
 100  _QPARAM_DRY_RUN = "dry-run" 
 101  _QPARAM_FORCE = "force" 
 102   
 103  # Feature strings 
 104  INST_CREATE_REQV1 = "instance-create-reqv1" 
 105  INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" 
 106  NODE_MIGRATE_REQV1 = "node-migrate-reqv1" 
 107  NODE_EVAC_RES1 = "node-evac-res1" 
 108   
 109  # Old feature constant names in case they're references by users of this module 
 110  _INST_CREATE_REQV1 = INST_CREATE_REQV1 
 111  _INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1 
 112  _NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1 
 113  _NODE_EVAC_RES1 = NODE_EVAC_RES1 
 114   
 115  # Older pycURL versions don't have all error constants 
 116  try: 
 117    _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT 
 118    _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE 
 119  except AttributeError: 
 120    _CURLE_SSL_CACERT = 60 
 121    _CURLE_SSL_CACERT_BADFILE = 77 
 122   
 123  _CURL_SSL_CERT_ERRORS = frozenset([ 
 124    _CURLE_SSL_CACERT, 
 125    _CURLE_SSL_CACERT_BADFILE, 
 126    ]) 
127 128 129 -class Error(Exception):
130 """Base error class for this module. 131 132 """ 133 pass
134
135 136 -class GanetiApiError(Error):
137 """Generic error raised from Ganeti API. 138 139 """
140 - def __init__(self, msg, code=None):
141 Error.__init__(self, msg) 142 self.code = code
143
144 145 -class CertificateError(GanetiApiError):
146 """Raised when a problem is found with the SSL certificate. 147 148 """ 149 pass
150
151 152 -def _AppendIf(container, condition, value):
153 """Appends to a list if a condition evaluates to truth. 154 155 """ 156 if condition: 157 container.append(value) 158 159 return condition
160
161 162 -def _AppendDryRunIf(container, condition):
163 """Appends a "dry-run" parameter if a condition evaluates to truth. 164 165 """ 166 return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
167
168 169 -def _AppendForceIf(container, condition):
170 """Appends a "force" parameter if a condition evaluates to truth. 171 172 """ 173 return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
174
175 176 -def _SetItemIf(container, condition, item, value):
177 """Sets an item if a condition evaluates to truth. 178 179 """ 180 if condition: 181 container[item] = value 182 183 return condition
184
185 186 -def UsesRapiClient(fn):
187 """Decorator for code using RAPI client to initialize pycURL. 188 189 """ 190 def wrapper(*args, **kwargs): 191 # curl_global_init(3) and curl_global_cleanup(3) must be called with only 192 # one thread running. This check is just a safety measure -- it doesn't 193 # cover all cases. 194 assert threading.activeCount() == 1, \ 195 "Found active threads when initializing pycURL" 196 197 pycurl.global_init(pycurl.GLOBAL_ALL) 198 try: 199 return fn(*args, **kwargs) 200 finally: 201 pycurl.global_cleanup()
202 203 return wrapper 204
205 206 -def GenericCurlConfig(verbose=False, use_signal=False, 207 use_curl_cabundle=False, cafile=None, capath=None, 208 proxy=None, verify_hostname=False, 209 connect_timeout=None, timeout=None, 210 _pycurl_version_fn=pycurl.version_info):
211 """Curl configuration function generator. 212 213 @type verbose: bool 214 @param verbose: Whether to set cURL to verbose mode 215 @type use_signal: bool 216 @param use_signal: Whether to allow cURL to use signals 217 @type use_curl_cabundle: bool 218 @param use_curl_cabundle: Whether to use cURL's default CA bundle 219 @type cafile: string 220 @param cafile: In which file we can find the certificates 221 @type capath: string 222 @param capath: In which directory we can find the certificates 223 @type proxy: string 224 @param proxy: Proxy to use, None for default behaviour and empty string for 225 disabling proxies (see curl_easy_setopt(3)) 226 @type verify_hostname: bool 227 @param verify_hostname: Whether to verify the remote peer certificate's 228 commonName 229 @type connect_timeout: number 230 @param connect_timeout: Timeout for establishing connection in seconds 231 @type timeout: number 232 @param timeout: Timeout for complete transfer in seconds (see 233 curl_easy_setopt(3)). 234 235 """ 236 if use_curl_cabundle and (cafile or capath): 237 raise Error("Can not use default CA bundle when CA file or path is set") 238 239 def _ConfigCurl(curl, logger): 240 """Configures a cURL object 241 242 @type curl: pycurl.Curl 243 @param curl: cURL object 244 245 """ 246 logger.debug("Using cURL version %s", pycurl.version) 247 248 # pycurl.version_info returns a tuple with information about the used 249 # version of libcurl. Item 5 is the SSL library linked to it. 250 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4', 251 # 0, '1.2.3.3', ...) 252 sslver = _pycurl_version_fn()[5] 253 if not sslver: 254 raise Error("No SSL support in cURL") 255 256 lcsslver = sslver.lower() 257 if lcsslver.startswith("openssl/"): 258 pass 259 elif lcsslver.startswith("nss/"): 260 # TODO: investigate compatibility beyond a simple test 261 pass 262 elif lcsslver.startswith("gnutls/"): 263 if capath: 264 raise Error("cURL linked against GnuTLS has no support for a" 265 " CA path (%s)" % (pycurl.version, )) 266 else: 267 raise NotImplementedError("cURL uses unsupported SSL version '%s'" % 268 sslver) 269 270 curl.setopt(pycurl.VERBOSE, verbose) 271 curl.setopt(pycurl.NOSIGNAL, not use_signal) 272 273 # Whether to verify remote peer's CN 274 if verify_hostname: 275 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that 276 # certificate must indicate that the server is the server to which you 277 # meant to connect, or the connection fails. [...] When the value is 1, 278 # the certificate must contain a Common Name field, but it doesn't matter 279 # what name it says. [...]" 280 curl.setopt(pycurl.SSL_VERIFYHOST, 2) 281 else: 282 curl.setopt(pycurl.SSL_VERIFYHOST, 0) 283 284 if cafile or capath or use_curl_cabundle: 285 # Require certificates to be checked 286 curl.setopt(pycurl.SSL_VERIFYPEER, True) 287 if cafile: 288 curl.setopt(pycurl.CAINFO, str(cafile)) 289 if capath: 290 curl.setopt(pycurl.CAPATH, str(capath)) 291 # Not changing anything for using default CA bundle 292 else: 293 # Disable SSL certificate verification 294 curl.setopt(pycurl.SSL_VERIFYPEER, False) 295 296 if proxy is not None: 297 curl.setopt(pycurl.PROXY, str(proxy)) 298 299 # Timeouts 300 if connect_timeout is not None: 301 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout) 302 if timeout is not None: 303 curl.setopt(pycurl.TIMEOUT, timeout)
304 305 return _ConfigCurl 306
307 308 -class GanetiRapiClient(object): # pylint: disable=R0904
309 """Ganeti RAPI client. 310 311 """ 312 USER_AGENT = "Ganeti RAPI Client" 313 _json_encoder = simplejson.JSONEncoder(sort_keys=True) 314
315 - def __init__(self, host, port=GANETI_RAPI_PORT, 316 username=None, password=None, logger=logging, 317 curl_config_fn=None, curl_factory=None):
318 """Initializes this class. 319 320 @type host: string 321 @param host: the ganeti cluster master to interact with 322 @type port: int 323 @param port: the port on which the RAPI is running (default is 5080) 324 @type username: string 325 @param username: the username to connect with 326 @type password: string 327 @param password: the password to connect with 328 @type curl_config_fn: callable 329 @param curl_config_fn: Function to configure C{pycurl.Curl} object 330 @param logger: Logging object 331 332 """ 333 self._username = username 334 self._password = password 335 self._logger = logger 336 self._curl_config_fn = curl_config_fn 337 self._curl_factory = curl_factory 338 339 try: 340 socket.inet_pton(socket.AF_INET6, host) 341 address = "[%s]:%s" % (host, port) 342 except socket.error: 343 address = "%s:%s" % (host, port) 344 345 self._base_url = "https://%s" % address 346 347 if username is not None: 348 if password is None: 349 raise Error("Password not specified") 350 elif password: 351 raise Error("Specified password without username")
352
353 - def _CreateCurl(self):
354 """Creates a cURL object. 355 356 """ 357 # Create pycURL object if no factory is provided 358 if self._curl_factory: 359 curl = self._curl_factory() 360 else: 361 curl = pycurl.Curl() 362 363 # Default cURL settings 364 curl.setopt(pycurl.VERBOSE, False) 365 curl.setopt(pycurl.FOLLOWLOCATION, False) 366 curl.setopt(pycurl.MAXREDIRS, 5) 367 curl.setopt(pycurl.NOSIGNAL, True) 368 curl.setopt(pycurl.USERAGENT, self.USER_AGENT) 369 curl.setopt(pycurl.SSL_VERIFYHOST, 0) 370 curl.setopt(pycurl.SSL_VERIFYPEER, False) 371 curl.setopt(pycurl.HTTPHEADER, [ 372 "Accept: %s" % HTTP_APP_JSON, 373 "Content-type: %s" % HTTP_APP_JSON, 374 ]) 375 376 assert ((self._username is None and self._password is None) ^ 377 (self._username is not None and self._password is not None)) 378 379 if self._username: 380 # Setup authentication 381 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) 382 curl.setopt(pycurl.USERPWD, 383 str("%s:%s" % (self._username, self._password))) 384 385 # Call external configuration function 386 if self._curl_config_fn: 387 self._curl_config_fn(curl, self._logger) 388 389 return curl
390 391 @staticmethod
392 - def _EncodeQuery(query):
393 """Encode query values for RAPI URL. 394 395 @type query: list of two-tuples 396 @param query: Query arguments 397 @rtype: list 398 @return: Query list with encoded values 399 400 """ 401 result = [] 402 403 for name, value in query: 404 if value is None: 405 result.append((name, "")) 406 407 elif isinstance(value, bool): 408 # Boolean values must be encoded as 0 or 1 409 result.append((name, int(value))) 410 411 elif isinstance(value, (list, tuple, dict)): 412 raise ValueError("Invalid query data type %r" % type(value).__name__) 413 414 else: 415 result.append((name, value)) 416 417 return result
418
419 - def _SendRequest(self, method, path, query, content):
420 """Sends an HTTP request. 421 422 This constructs a full URL, encodes and decodes HTTP bodies, and 423 handles invalid responses in a pythonic way. 424 425 @type method: string 426 @param method: HTTP method to use 427 @type path: string 428 @param path: HTTP URL path 429 @type query: list of two-tuples 430 @param query: query arguments to pass to urllib.urlencode 431 @type content: str or None 432 @param content: HTTP body content 433 434 @rtype: str 435 @return: JSON-Decoded response 436 437 @raises CertificateError: If an invalid SSL certificate is found 438 @raises GanetiApiError: If an invalid response is returned 439 440 """ 441 assert path.startswith("/") 442 443 curl = self._CreateCurl() 444 445 if content is not None: 446 encoded_content = self._json_encoder.encode(content) 447 else: 448 encoded_content = "" 449 450 # Build URL 451 urlparts = [self._base_url, path] 452 if query: 453 urlparts.append("?") 454 urlparts.append(urllib.urlencode(self._EncodeQuery(query))) 455 456 url = "".join(urlparts) 457 458 self._logger.debug("Sending request %s %s (content=%r)", 459 method, url, encoded_content) 460 461 # Buffer for response 462 encoded_resp_body = StringIO() 463 464 # Configure cURL 465 curl.setopt(pycurl.CUSTOMREQUEST, str(method)) 466 curl.setopt(pycurl.URL, str(url)) 467 curl.setopt(pycurl.POSTFIELDS, str(encoded_content)) 468 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write) 469 470 try: 471 # Send request and wait for response 472 try: 473 curl.perform() 474 except pycurl.error, err: 475 if err.args[0] in _CURL_SSL_CERT_ERRORS: 476 raise CertificateError("SSL certificate error %s" % err, 477 code=err.args[0]) 478 479 raise GanetiApiError(str(err), code=err.args[0]) 480 finally: 481 # Reset settings to not keep references to large objects in memory 482 # between requests 483 curl.setopt(pycurl.POSTFIELDS, "") 484 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None) 485 486 # Get HTTP response code 487 http_code = curl.getinfo(pycurl.RESPONSE_CODE) 488 489 # Was anything written to the response buffer? 490 if encoded_resp_body.tell(): 491 response_content = simplejson.loads(encoded_resp_body.getvalue()) 492 else: 493 response_content = None 494 495 if http_code != HTTP_OK: 496 if isinstance(response_content, dict): 497 msg = ("%s %s: %s" % 498 (response_content["code"], 499 response_content["message"], 500 response_content["explain"])) 501 else: 502 msg = str(response_content) 503 504 raise GanetiApiError(msg, code=http_code) 505 506 return response_content
507
508 - def GetVersion(self):
509 """Gets the Remote API version running on the cluster. 510 511 @rtype: int 512 @return: Ganeti Remote API version 513 514 """ 515 return self._SendRequest(HTTP_GET, "/version", None, None)
516
517 - def GetFeatures(self):
518 """Gets the list of optional features supported by RAPI server. 519 520 @rtype: list 521 @return: List of optional features 522 523 """ 524 try: 525 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION, 526 None, None) 527 except GanetiApiError, err: 528 # Older RAPI servers don't support this resource 529 if err.code == HTTP_NOT_FOUND: 530 return [] 531 532 raise
533
534 - def GetOperatingSystems(self):
535 """Gets the Operating Systems running in the Ganeti cluster. 536 537 @rtype: list of str 538 @return: operating systems 539 540 """ 541 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION, 542 None, None)
543
544 - def GetInfo(self):
545 """Gets info about the cluster. 546 547 @rtype: dict 548 @return: information about the cluster 549 550 """ 551 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION, 552 None, None)
553
554 - def RedistributeConfig(self):
555 """Tells the cluster to redistribute its configuration files. 556 557 @rtype: string 558 @return: job id 559 560 """ 561 return self._SendRequest(HTTP_PUT, 562 "/%s/redistribute-config" % GANETI_RAPI_VERSION, 563 None, None)
564
565 - def ModifyCluster(self, **kwargs):
566 """Modifies cluster parameters. 567 568 More details for parameters can be found in the RAPI documentation. 569 570 @rtype: string 571 @return: job id 572 573 """ 574 body = kwargs 575 576 return self._SendRequest(HTTP_PUT, 577 "/%s/modify" % GANETI_RAPI_VERSION, None, body)
578
579 - def GetClusterTags(self):
580 """Gets the cluster tags. 581 582 @rtype: list of str 583 @return: cluster tags 584 585 """ 586 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION, 587 None, None)
588
589 - def AddClusterTags(self, tags, dry_run=False):
590 """Adds tags to the cluster. 591 592 @type tags: list of str 593 @param tags: tags to add to the cluster 594 @type dry_run: bool 595 @param dry_run: whether to perform a dry run 596 597 @rtype: string 598 @return: job id 599 600 """ 601 query = [("tag", t) for t in tags] 602 _AppendDryRunIf(query, dry_run) 603 604 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION, 605 query, None)
606
607 - def DeleteClusterTags(self, tags, dry_run=False):
608 """Deletes tags from the cluster. 609 610 @type tags: list of str 611 @param tags: tags to delete 612 @type dry_run: bool 613 @param dry_run: whether to perform a dry run 614 @rtype: string 615 @return: job id 616 617 """ 618 query = [("tag", t) for t in tags] 619 _AppendDryRunIf(query, dry_run) 620 621 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION, 622 query, None)
623
624 - def GetInstances(self, bulk=False):
625 """Gets information about instances on the cluster. 626 627 @type bulk: bool 628 @param bulk: whether to return all information about all instances 629 630 @rtype: list of dict or list of str 631 @return: if bulk is True, info about the instances, else a list of instances 632 633 """ 634 query = [] 635 _AppendIf(query, bulk, ("bulk", 1)) 636 637 instances = self._SendRequest(HTTP_GET, 638 "/%s/instances" % GANETI_RAPI_VERSION, 639 query, None) 640 if bulk: 641 return instances 642 else: 643 return [i["id"] for i in instances]
644
645 - def GetInstance(self, instance):
646 """Gets information about an instance. 647 648 @type instance: str 649 @param instance: instance whose info to return 650 651 @rtype: dict 652 @return: info about the instance 653 654 """ 655 return self._SendRequest(HTTP_GET, 656 ("/%s/instances/%s" % 657 (GANETI_RAPI_VERSION, instance)), None, None)
658
659 - def GetInstanceInfo(self, instance, static=None):
660 """Gets information about an instance. 661 662 @type instance: string 663 @param instance: Instance name 664 @rtype: string 665 @return: Job ID 666 667 """ 668 if static is not None: 669 query = [("static", static)] 670 else: 671 query = None 672 673 return self._SendRequest(HTTP_GET, 674 ("/%s/instances/%s/info" % 675 (GANETI_RAPI_VERSION, instance)), query, None)
676
677 - def CreateInstance(self, mode, name, disk_template, disks, nics, 678 **kwargs):
679 """Creates a new instance. 680 681 More details for parameters can be found in the RAPI documentation. 682 683 @type mode: string 684 @param mode: Instance creation mode 685 @type name: string 686 @param name: Hostname of the instance to create 687 @type disk_template: string 688 @param disk_template: Disk template for instance (e.g. plain, diskless, 689 file, or drbd) 690 @type disks: list of dicts 691 @param disks: List of disk definitions 692 @type nics: list of dicts 693 @param nics: List of NIC definitions 694 @type dry_run: bool 695 @keyword dry_run: whether to perform a dry run 696 697 @rtype: string 698 @return: job id 699 700 """ 701 query = [] 702 703 _AppendDryRunIf(query, kwargs.get("dry_run")) 704 705 if _INST_CREATE_REQV1 in self.GetFeatures(): 706 # All required fields for request data version 1 707 body = { 708 _REQ_DATA_VERSION_FIELD: 1, 709 "mode": mode, 710 "name": name, 711 "disk_template": disk_template, 712 "disks": disks, 713 "nics": nics, 714 } 715 716 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys()) 717 if conflicts: 718 raise GanetiApiError("Required fields can not be specified as" 719 " keywords: %s" % ", ".join(conflicts)) 720 721 body.update((key, value) for key, value in kwargs.iteritems() 722 if key != "dry_run") 723 else: 724 raise GanetiApiError("Server does not support new-style (version 1)" 725 " instance creation requests") 726 727 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION, 728 query, body)
729
730 - def DeleteInstance(self, instance, dry_run=False):
731 """Deletes an instance. 732 733 @type instance: str 734 @param instance: the instance to delete 735 736 @rtype: string 737 @return: job id 738 739 """ 740 query = [] 741 _AppendDryRunIf(query, dry_run) 742 743 return self._SendRequest(HTTP_DELETE, 744 ("/%s/instances/%s" % 745 (GANETI_RAPI_VERSION, instance)), query, None)
746
747 - def ModifyInstance(self, instance, **kwargs):
748 """Modifies an instance. 749 750 More details for parameters can be found in the RAPI documentation. 751 752 @type instance: string 753 @param instance: Instance name 754 @rtype: string 755 @return: job id 756 757 """ 758 body = kwargs 759 760 return self._SendRequest(HTTP_PUT, 761 ("/%s/instances/%s/modify" % 762 (GANETI_RAPI_VERSION, instance)), None, body)
763
764 - def ActivateInstanceDisks(self, instance, ignore_size=None):
765 """Activates an instance's disks. 766 767 @type instance: string 768 @param instance: Instance name 769 @type ignore_size: bool 770 @param ignore_size: Whether to ignore recorded size 771 @rtype: string 772 @return: job id 773 774 """ 775 query = [] 776 _AppendIf(query, ignore_size, ("ignore_size", 1)) 777 778 return self._SendRequest(HTTP_PUT, 779 ("/%s/instances/%s/activate-disks" % 780 (GANETI_RAPI_VERSION, instance)), query, None)
781
782 - def DeactivateInstanceDisks(self, instance):
783 """Deactivates an instance's disks. 784 785 @type instance: string 786 @param instance: Instance name 787 @rtype: string 788 @return: job id 789 790 """ 791 return self._SendRequest(HTTP_PUT, 792 ("/%s/instances/%s/deactivate-disks" % 793 (GANETI_RAPI_VERSION, instance)), None, None)
794
795 - def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
796 """Recreate an instance's disks. 797 798 @type instance: string 799 @param instance: Instance name 800 @type disks: list of int 801 @param disks: List of disk indexes 802 @type nodes: list of string 803 @param nodes: New instance nodes, if relocation is desired 804 @rtype: string 805 @return: job id 806 807 """ 808 body = {} 809 _SetItemIf(body, disks is not None, "disks", disks) 810 _SetItemIf(body, nodes is not None, "nodes", nodes) 811 812 return self._SendRequest(HTTP_POST, 813 ("/%s/instances/%s/recreate-disks" % 814 (GANETI_RAPI_VERSION, instance)), None, body)
815
816 - def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
817 """Grows a disk of an instance. 818 819 More details for parameters can be found in the RAPI documentation. 820 821 @type instance: string 822 @param instance: Instance name 823 @type disk: integer 824 @param disk: Disk index 825 @type amount: integer 826 @param amount: Grow disk by this amount (MiB) 827 @type wait_for_sync: bool 828 @param wait_for_sync: Wait for disk to synchronize 829 @rtype: string 830 @return: job id 831 832 """ 833 body = { 834 "amount": amount, 835 } 836 837 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync) 838 839 return self._SendRequest(HTTP_POST, 840 ("/%s/instances/%s/disk/%s/grow" % 841 (GANETI_RAPI_VERSION, instance, disk)), 842 None, body)
843
844 - def GetInstanceTags(self, instance):
845 """Gets tags for an instance. 846 847 @type instance: str 848 @param instance: instance whose tags to return 849 850 @rtype: list of str 851 @return: tags for the instance 852 853 """ 854 return self._SendRequest(HTTP_GET, 855 ("/%s/instances/%s/tags" % 856 (GANETI_RAPI_VERSION, instance)), None, None)
857
858 - def AddInstanceTags(self, instance, tags, dry_run=False):
859 """Adds tags to an instance. 860 861 @type instance: str 862 @param instance: instance to add tags to 863 @type tags: list of str 864 @param tags: tags to add to the instance 865 @type dry_run: bool 866 @param dry_run: whether to perform a dry run 867 868 @rtype: string 869 @return: job id 870 871 """ 872 query = [("tag", t) for t in tags] 873 _AppendDryRunIf(query, dry_run) 874 875 return self._SendRequest(HTTP_PUT, 876 ("/%s/instances/%s/tags" % 877 (GANETI_RAPI_VERSION, instance)), query, None)
878
879 - def DeleteInstanceTags(self, instance, tags, dry_run=False):
880 """Deletes tags from an instance. 881 882 @type instance: str 883 @param instance: instance to delete tags from 884 @type tags: list of str 885 @param tags: tags to delete 886 @type dry_run: bool 887 @param dry_run: whether to perform a dry run 888 @rtype: string 889 @return: job id 890 891 """ 892 query = [("tag", t) for t in tags] 893 _AppendDryRunIf(query, dry_run) 894 895 return self._SendRequest(HTTP_DELETE, 896 ("/%s/instances/%s/tags" % 897 (GANETI_RAPI_VERSION, instance)), query, None)
898
899 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None, 900 dry_run=False):
901 """Reboots an instance. 902 903 @type instance: str 904 @param instance: instance to rebot 905 @type reboot_type: str 906 @param reboot_type: one of: hard, soft, full 907 @type ignore_secondaries: bool 908 @param ignore_secondaries: if True, ignores errors for the secondary node 909 while re-assembling disks (in hard-reboot mode only) 910 @type dry_run: bool 911 @param dry_run: whether to perform a dry run 912 @rtype: string 913 @return: job id 914 915 """ 916 query = [] 917 _AppendDryRunIf(query, dry_run) 918 _AppendIf(query, reboot_type, ("type", reboot_type)) 919 _AppendIf(query, ignore_secondaries is not None, 920 ("ignore_secondaries", ignore_secondaries)) 921 922 return self._SendRequest(HTTP_POST, 923 ("/%s/instances/%s/reboot" % 924 (GANETI_RAPI_VERSION, instance)), query, None)
925
926 - def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
927 """Shuts down an instance. 928 929 @type instance: str 930 @param instance: the instance to shut down 931 @type dry_run: bool 932 @param dry_run: whether to perform a dry run 933 @type no_remember: bool 934 @param no_remember: if true, will not record the state change 935 @rtype: string 936 @return: job id 937 938 """ 939 query = [] 940 _AppendDryRunIf(query, dry_run) 941 _AppendIf(query, no_remember, ("no-remember", 1)) 942 943 return self._SendRequest(HTTP_PUT, 944 ("/%s/instances/%s/shutdown" % 945 (GANETI_RAPI_VERSION, instance)), query, None)
946
947 - def StartupInstance(self, instance, dry_run=False, no_remember=False):
948 """Starts up an instance. 949 950 @type instance: str 951 @param instance: the instance to start up 952 @type dry_run: bool 953 @param dry_run: whether to perform a dry run 954 @type no_remember: bool 955 @param no_remember: if true, will not record the state change 956 @rtype: string 957 @return: job id 958 959 """ 960 query = [] 961 _AppendDryRunIf(query, dry_run) 962 _AppendIf(query, no_remember, ("no-remember", 1)) 963 964 return self._SendRequest(HTTP_PUT, 965 ("/%s/instances/%s/startup" % 966 (GANETI_RAPI_VERSION, instance)), query, None)
967
968 - def ReinstallInstance(self, instance, os=None, no_startup=False, 969 osparams=None):
970 """Reinstalls an instance. 971 972 @type instance: str 973 @param instance: The instance to reinstall 974 @type os: str or None 975 @param os: The operating system to reinstall. If None, the instance's 976 current operating system will be installed again 977 @type no_startup: bool 978 @param no_startup: Whether to start the instance automatically 979 @rtype: string 980 @return: job id 981 982 """ 983 if _INST_REINSTALL_REQV1 in self.GetFeatures(): 984 body = { 985 "start": not no_startup, 986 } 987 _SetItemIf(body, os is not None, "os", os) 988 _SetItemIf(body, osparams is not None, "osparams", osparams) 989 return self._SendRequest(HTTP_POST, 990 ("/%s/instances/%s/reinstall" % 991 (GANETI_RAPI_VERSION, instance)), None, body) 992 993 # Use old request format 994 if osparams: 995 raise GanetiApiError("Server does not support specifying OS parameters" 996 " for instance reinstallation") 997 998 query = [] 999 _AppendIf(query, os, ("os", os)) 1000 _AppendIf(query, no_startup, ("nostartup", 1)) 1001 1002 return self._SendRequest(HTTP_POST, 1003 ("/%s/instances/%s/reinstall" % 1004 (GANETI_RAPI_VERSION, instance)), query, None)
1005
1006 - def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO, 1007 remote_node=None, iallocator=None):
1008 """Replaces disks on an instance. 1009 1010 @type instance: str 1011 @param instance: instance whose disks to replace 1012 @type disks: list of ints 1013 @param disks: Indexes of disks to replace 1014 @type mode: str 1015 @param mode: replacement mode to use (defaults to replace_auto) 1016 @type remote_node: str or None 1017 @param remote_node: new secondary node to use (for use with 1018 replace_new_secondary mode) 1019 @type iallocator: str or None 1020 @param iallocator: instance allocator plugin to use (for use with 1021 replace_auto mode) 1022 1023 @rtype: string 1024 @return: job id 1025 1026 """ 1027 query = [ 1028 ("mode", mode), 1029 ] 1030 1031 # TODO: Convert to body parameters 1032 1033 if disks is not None: 1034 _AppendIf(query, True, 1035 ("disks", ",".join(str(idx) for idx in disks))) 1036 1037 _AppendIf(query, remote_node is not None, ("remote_node", remote_node)) 1038 _AppendIf(query, iallocator is not None, ("iallocator", iallocator)) 1039 1040 return self._SendRequest(HTTP_POST, 1041 ("/%s/instances/%s/replace-disks" % 1042 (GANETI_RAPI_VERSION, instance)), query, None)
1043
1044 - def PrepareExport(self, instance, mode):
1045 """Prepares an instance for an export. 1046 1047 @type instance: string 1048 @param instance: Instance name 1049 @type mode: string 1050 @param mode: Export mode 1051 @rtype: string 1052 @return: Job ID 1053 1054 """ 1055 query = [("mode", mode)] 1056 return self._SendRequest(HTTP_PUT, 1057 ("/%s/instances/%s/prepare-export" % 1058 (GANETI_RAPI_VERSION, instance)), query, None)
1059
1060 - def ExportInstance(self, instance, mode, destination, shutdown=None, 1061 remove_instance=None, 1062 x509_key_name=None, destination_x509_ca=None):
1063 """Exports an instance. 1064 1065 @type instance: string 1066 @param instance: Instance name 1067 @type mode: string 1068 @param mode: Export mode 1069 @rtype: string 1070 @return: Job ID 1071 1072 """ 1073 body = { 1074 "destination": destination, 1075 "mode": mode, 1076 } 1077 1078 _SetItemIf(body, shutdown is not None, "shutdown", shutdown) 1079 _SetItemIf(body, remove_instance is not None, 1080 "remove_instance", remove_instance) 1081 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name) 1082 _SetItemIf(body, destination_x509_ca is not None, 1083 "destination_x509_ca", destination_x509_ca) 1084 1085 return self._SendRequest(HTTP_PUT, 1086 ("/%s/instances/%s/export" % 1087 (GANETI_RAPI_VERSION, instance)), None, body)
1088
1089 - def MigrateInstance(self, instance, mode=None, cleanup=None):
1090 """Migrates an instance. 1091 1092 @type instance: string 1093 @param instance: Instance name 1094 @type mode: string 1095 @param mode: Migration mode 1096 @type cleanup: bool 1097 @param cleanup: Whether to clean up a previously failed migration 1098 @rtype: string 1099 @return: job id 1100 1101 """ 1102 body = {} 1103 _SetItemIf(body, mode is not None, "mode", mode) 1104 _SetItemIf(body, cleanup is not None, "cleanup", cleanup) 1105 1106 return self._SendRequest(HTTP_PUT, 1107 ("/%s/instances/%s/migrate" % 1108 (GANETI_RAPI_VERSION, instance)), None, body)
1109
1110 - def FailoverInstance(self, instance, iallocator=None, 1111 ignore_consistency=None, target_node=None):
1112 """Does a failover of an instance. 1113 1114 @type instance: string 1115 @param instance: Instance name 1116 @type iallocator: string 1117 @param iallocator: Iallocator for deciding the target node for 1118 shared-storage instances 1119 @type ignore_consistency: bool 1120 @param ignore_consistency: Whether to ignore disk consistency 1121 @type target_node: string 1122 @param target_node: Target node for shared-storage instances 1123 @rtype: string 1124 @return: job id 1125 1126 """ 1127 body = {} 1128 _SetItemIf(body, iallocator is not None, "iallocator", iallocator) 1129 _SetItemIf(body, ignore_consistency is not None, 1130 "ignore_consistency", ignore_consistency) 1131 _SetItemIf(body, target_node is not None, "target_node", target_node) 1132 1133 return self._SendRequest(HTTP_PUT, 1134 ("/%s/instances/%s/failover" % 1135 (GANETI_RAPI_VERSION, instance)), None, body)
1136
1137 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1138 """Changes the name of an instance. 1139 1140 @type instance: string 1141 @param instance: Instance name 1142 @type new_name: string 1143 @param new_name: New instance name 1144 @type ip_check: bool 1145 @param ip_check: Whether to ensure instance's IP address is inactive 1146 @type name_check: bool 1147 @param name_check: Whether to ensure instance's name is resolvable 1148 @rtype: string 1149 @return: job id 1150 1151 """ 1152 body = { 1153 "new_name": new_name, 1154 } 1155 1156 _SetItemIf(body, ip_check is not None, "ip_check", ip_check) 1157 _SetItemIf(body, name_check is not None, "name_check", name_check) 1158 1159 return self._SendRequest(HTTP_PUT, 1160 ("/%s/instances/%s/rename" % 1161 (GANETI_RAPI_VERSION, instance)), None, body)
1162
1163 - def GetInstanceConsole(self, instance):
1164 """Request information for connecting to instance's console. 1165 1166 @type instance: string 1167 @param instance: Instance name 1168 @rtype: dict 1169 @return: dictionary containing information about instance's console 1170 1171 """ 1172 return self._SendRequest(HTTP_GET, 1173 ("/%s/instances/%s/console" % 1174 (GANETI_RAPI_VERSION, instance)), None, None)
1175
1176 - def GetJobs(self):
1177 """Gets all jobs for the cluster. 1178 1179 @rtype: list of int 1180 @return: job ids for the cluster 1181 1182 """ 1183 return [int(j["id"]) 1184 for j in self._SendRequest(HTTP_GET, 1185 "/%s/jobs" % GANETI_RAPI_VERSION, 1186 None, None)]
1187
1188 - def GetJobStatus(self, job_id):
1189 """Gets the status of a job. 1190 1191 @type job_id: string 1192 @param job_id: job id whose status to query 1193 1194 @rtype: dict 1195 @return: job status 1196 1197 """ 1198 return self._SendRequest(HTTP_GET, 1199 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), 1200 None, None)
1201
1202 - def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1203 """Polls cluster for job status until completion. 1204 1205 Completion is defined as any of the following states listed in 1206 L{JOB_STATUS_FINALIZED}. 1207 1208 @type job_id: string 1209 @param job_id: job id to watch 1210 @type period: int 1211 @param period: how often to poll for status (optional, default 5s) 1212 @type retries: int 1213 @param retries: how many time to poll before giving up 1214 (optional, default -1 means unlimited) 1215 1216 @rtype: bool 1217 @return: C{True} if job succeeded or C{False} if failed/status timeout 1218 @deprecated: It is recommended to use L{WaitForJobChange} wherever 1219 possible; L{WaitForJobChange} returns immediately after a job changed and 1220 does not use polling 1221 1222 """ 1223 while retries != 0: 1224 job_result = self.GetJobStatus(job_id) 1225 1226 if job_result and job_result["status"] == JOB_STATUS_SUCCESS: 1227 return True 1228 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED: 1229 return False 1230 1231 if period: 1232 time.sleep(period) 1233 1234 if retries > 0: 1235 retries -= 1 1236 1237 return False
1238
1239 - def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1240 """Waits for job changes. 1241 1242 @type job_id: string 1243 @param job_id: Job ID for which to wait 1244 @return: C{None} if no changes have been detected and a dict with two keys, 1245 C{job_info} and C{log_entries} otherwise. 1246 @rtype: dict 1247 1248 """ 1249 body = { 1250 "fields": fields, 1251 "previous_job_info": prev_job_info, 1252 "previous_log_serial": prev_log_serial, 1253 } 1254 1255 return self._SendRequest(HTTP_GET, 1256 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id), 1257 None, body)
1258
1259 - def CancelJob(self, job_id, dry_run=False):
1260 """Cancels a job. 1261 1262 @type job_id: string 1263 @param job_id: id of the job to delete 1264 @type dry_run: bool 1265 @param dry_run: whether to perform a dry run 1266 @rtype: tuple 1267 @return: tuple containing the result, and a message (bool, string) 1268 1269 """ 1270 query = [] 1271 _AppendDryRunIf(query, dry_run) 1272 1273 return self._SendRequest(HTTP_DELETE, 1274 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), 1275 query, None)
1276
1277 - def GetNodes(self, bulk=False):
1278 """Gets all nodes in the cluster. 1279 1280 @type bulk: bool 1281 @param bulk: whether to return all information about all instances 1282 1283 @rtype: list of dict or str 1284 @return: if bulk is true, info about nodes in the cluster, 1285 else list of nodes in the cluster 1286 1287 """ 1288 query = [] 1289 _AppendIf(query, bulk, ("bulk", 1)) 1290 1291 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION, 1292 query, None) 1293 if bulk: 1294 return nodes 1295 else: 1296 return [n["id"] for n in nodes]
1297
1298 - def GetNode(self, node):
1299 """Gets information about a node. 1300 1301 @type node: str 1302 @param node: node whose info to return 1303 1304 @rtype: dict 1305 @return: info about the node 1306 1307 """ 1308 return self._SendRequest(HTTP_GET, 1309 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node), 1310 None, None)
1311
1312 - def EvacuateNode(self, node, iallocator=None, remote_node=None, 1313 dry_run=False, early_release=None, 1314 mode=None, accept_old=False):
1315 """Evacuates instances from a Ganeti node. 1316 1317 @type node: str 1318 @param node: node to evacuate 1319 @type iallocator: str or None 1320 @param iallocator: instance allocator to use 1321 @type remote_node: str 1322 @param remote_node: node to evaucate to 1323 @type dry_run: bool 1324 @param dry_run: whether to perform a dry run 1325 @type early_release: bool 1326 @param early_release: whether to enable parallelization 1327 @type mode: string 1328 @param mode: Node evacuation mode 1329 @type accept_old: bool 1330 @param accept_old: Whether caller is ready to accept old-style (pre-2.5) 1331 results 1332 1333 @rtype: string, or a list for pre-2.5 results 1334 @return: Job ID or, if C{accept_old} is set and server is pre-2.5, 1335 list of (job ID, instance name, new secondary node); if dry_run was 1336 specified, then the actual move jobs were not submitted and the job IDs 1337 will be C{None} 1338 1339 @raises GanetiApiError: if an iallocator and remote_node are both 1340 specified 1341 1342 """ 1343 if iallocator and remote_node: 1344 raise GanetiApiError("Only one of iallocator or remote_node can be used") 1345 1346 query = [] 1347 _AppendDryRunIf(query, dry_run) 1348 1349 if _NODE_EVAC_RES1 in self.GetFeatures(): 1350 # Server supports body parameters 1351 body = {} 1352 1353 _SetItemIf(body, iallocator is not None, "iallocator", iallocator) 1354 _SetItemIf(body, remote_node is not None, "remote_node", remote_node) 1355 _SetItemIf(body, early_release is not None, 1356 "early_release", early_release) 1357 _SetItemIf(body, mode is not None, "mode", mode) 1358 else: 1359 # Pre-2.5 request format 1360 body = None 1361 1362 if not accept_old: 1363 raise GanetiApiError("Server is version 2.4 or earlier and caller does" 1364 " not accept old-style results (parameter" 1365 " accept_old)") 1366 1367 # Pre-2.5 servers can only evacuate secondaries 1368 if mode is not None and mode != NODE_EVAC_SEC: 1369 raise GanetiApiError("Server can only evacuate secondary instances") 1370 1371 _AppendIf(query, iallocator, ("iallocator", iallocator)) 1372 _AppendIf(query, remote_node, ("remote_node", remote_node)) 1373 _AppendIf(query, early_release, ("early_release", 1)) 1374 1375 return self._SendRequest(HTTP_POST, 1376 ("/%s/nodes/%s/evacuate" % 1377 (GANETI_RAPI_VERSION, node)), query, body)
1378
1379 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None, 1380 target_node=None):
1381 """Migrates all primary instances from a node. 1382 1383 @type node: str 1384 @param node: node to migrate 1385 @type mode: string 1386 @param mode: if passed, it will overwrite the live migration type, 1387 otherwise the hypervisor default will be used 1388 @type dry_run: bool 1389 @param dry_run: whether to perform a dry run 1390 @type iallocator: string 1391 @param iallocator: instance allocator to use 1392 @type target_node: string 1393 @param target_node: Target node for shared-storage instances 1394 1395 @rtype: string 1396 @return: job id 1397 1398 """ 1399 query = [] 1400 _AppendDryRunIf(query, dry_run) 1401 1402 if _NODE_MIGRATE_REQV1 in self.GetFeatures(): 1403 body = {} 1404 1405 _SetItemIf(body, mode is not None, "mode", mode) 1406 _SetItemIf(body, iallocator is not None, "iallocator", iallocator) 1407 _SetItemIf(body, target_node is not None, "target_node", target_node) 1408 1409 assert len(query) <= 1 1410 1411 return self._SendRequest(HTTP_POST, 1412 ("/%s/nodes/%s/migrate" % 1413 (GANETI_RAPI_VERSION, node)), query, body) 1414 else: 1415 # Use old request format 1416 if target_node is not None: 1417 raise GanetiApiError("Server does not support specifying target node" 1418 " for node migration") 1419 1420 _AppendIf(query, mode is not None, ("mode", mode)) 1421 1422 return self._SendRequest(HTTP_POST, 1423 ("/%s/nodes/%s/migrate" % 1424 (GANETI_RAPI_VERSION, node)), query, None)
1425
1426 - def GetNodeRole(self, node):
1427 """Gets the current role for a node. 1428 1429 @type node: str 1430 @param node: node whose role to return 1431 1432 @rtype: str 1433 @return: the current role for a node 1434 1435 """ 1436 return self._SendRequest(HTTP_GET, 1437 ("/%s/nodes/%s/role" % 1438 (GANETI_RAPI_VERSION, node)), None, None)
1439
1440 - def SetNodeRole(self, node, role, force=False, auto_promote=None):
1441 """Sets the role for a node. 1442 1443 @type node: str 1444 @param node: the node whose role to set 1445 @type role: str 1446 @param role: the role to set for the node 1447 @type force: bool 1448 @param force: whether to force the role change 1449 @type auto_promote: bool 1450 @param auto_promote: Whether node(s) should be promoted to master candidate 1451 if necessary 1452 1453 @rtype: string 1454 @return: job id 1455 1456 """ 1457 query = [] 1458 _AppendForceIf(query, force) 1459 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote)) 1460 1461 return self._SendRequest(HTTP_PUT, 1462 ("/%s/nodes/%s/role" % 1463 (GANETI_RAPI_VERSION, node)), query, role)
1464
1465 - def PowercycleNode(self, node, force=False):
1466 """Powercycles a node. 1467 1468 @type node: string 1469 @param node: Node name 1470 @type force: bool 1471 @param force: Whether to force the operation 1472 @rtype: string 1473 @return: job id 1474 1475 """ 1476 query = [] 1477 _AppendForceIf(query, force) 1478 1479 return self._SendRequest(HTTP_POST, 1480 ("/%s/nodes/%s/powercycle" % 1481 (GANETI_RAPI_VERSION, node)), query, None)
1482
1483 - def ModifyNode(self, node, **kwargs):
1484 """Modifies a node. 1485 1486 More details for parameters can be found in the RAPI documentation. 1487 1488 @type node: string 1489 @param node: Node name 1490 @rtype: string 1491 @return: job id 1492 1493 """ 1494 return self._SendRequest(HTTP_POST, 1495 ("/%s/nodes/%s/modify" % 1496 (GANETI_RAPI_VERSION, node)), None, kwargs)
1497
1498 - def GetNodeStorageUnits(self, node, storage_type, output_fields):
1499 """Gets the storage units for a node. 1500 1501 @type node: str 1502 @param node: the node whose storage units to return 1503 @type storage_type: str 1504 @param storage_type: storage type whose units to return 1505 @type output_fields: str 1506 @param output_fields: storage type fields to return 1507 1508 @rtype: string 1509 @return: job id where results can be retrieved 1510 1511 """ 1512 query = [ 1513 ("storage_type", storage_type), 1514 ("output_fields", output_fields), 1515 ] 1516 1517 return self._SendRequest(HTTP_GET, 1518 ("/%s/nodes/%s/storage" % 1519 (GANETI_RAPI_VERSION, node)), query, None)
1520
1521 - def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1522 """Modifies parameters of storage units on the node. 1523 1524 @type node: str 1525 @param node: node whose storage units to modify 1526 @type storage_type: str 1527 @param storage_type: storage type whose units to modify 1528 @type name: str 1529 @param name: name of the storage unit 1530 @type allocatable: bool or None 1531 @param allocatable: Whether to set the "allocatable" flag on the storage 1532 unit (None=no modification, True=set, False=unset) 1533 1534 @rtype: string 1535 @return: job id 1536 1537 """ 1538 query = [ 1539 ("storage_type", storage_type), 1540 ("name", name), 1541 ] 1542 1543 _AppendIf(query, allocatable is not None, ("allocatable", allocatable)) 1544 1545 return self._SendRequest(HTTP_PUT, 1546 ("/%s/nodes/%s/storage/modify" % 1547 (GANETI_RAPI_VERSION, node)), query, None)
1548
1549 - def RepairNodeStorageUnits(self, node, storage_type, name):
1550 """Repairs a storage unit on the node. 1551 1552 @type node: str 1553 @param node: node whose storage units to repair 1554 @type storage_type: str 1555 @param storage_type: storage type to repair 1556 @type name: str 1557 @param name: name of the storage unit to repair 1558 1559 @rtype: string 1560 @return: job id 1561 1562 """ 1563 query = [ 1564 ("storage_type", storage_type), 1565 ("name", name), 1566 ] 1567 1568 return self._SendRequest(HTTP_PUT, 1569 ("/%s/nodes/%s/storage/repair" % 1570 (GANETI_RAPI_VERSION, node)), query, None)
1571
1572 - def GetNodeTags(self, node):
1573 """Gets the tags for a node. 1574 1575 @type node: str 1576 @param node: node whose tags to return 1577 1578 @rtype: list of str 1579 @return: tags for the node 1580 1581 """ 1582 return self._SendRequest(HTTP_GET, 1583 ("/%s/nodes/%s/tags" % 1584 (GANETI_RAPI_VERSION, node)), None, None)
1585
1586 - def AddNodeTags(self, node, tags, dry_run=False):
1587 """Adds tags to a node. 1588 1589 @type node: str 1590 @param node: node to add tags to 1591 @type tags: list of str 1592 @param tags: tags to add to the node 1593 @type dry_run: bool 1594 @param dry_run: whether to perform a dry run 1595 1596 @rtype: string 1597 @return: job id 1598 1599 """ 1600 query = [("tag", t) for t in tags] 1601 _AppendDryRunIf(query, dry_run) 1602 1603 return self._SendRequest(HTTP_PUT, 1604 ("/%s/nodes/%s/tags" % 1605 (GANETI_RAPI_VERSION, node)), query, tags)
1606
1607 - def DeleteNodeTags(self, node, tags, dry_run=False):
1608 """Delete tags from a node. 1609 1610 @type node: str 1611 @param node: node to remove tags from 1612 @type tags: list of str 1613 @param tags: tags to remove from the node 1614 @type dry_run: bool 1615 @param dry_run: whether to perform a dry run 1616 1617 @rtype: string 1618 @return: job id 1619 1620 """ 1621 query = [("tag", t) for t in tags] 1622 _AppendDryRunIf(query, dry_run) 1623 1624 return self._SendRequest(HTTP_DELETE, 1625 ("/%s/nodes/%s/tags" % 1626 (GANETI_RAPI_VERSION, node)), query, None)
1627
1628 - def GetGroups(self, bulk=False):
1629 """Gets all node groups in the cluster. 1630 1631 @type bulk: bool 1632 @param bulk: whether to return all information about the groups 1633 1634 @rtype: list of dict or str 1635 @return: if bulk is true, a list of dictionaries with info about all node 1636 groups in the cluster, else a list of names of those node groups 1637 1638 """ 1639 query = [] 1640 _AppendIf(query, bulk, ("bulk", 1)) 1641 1642 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION, 1643 query, None) 1644 if bulk: 1645 return groups 1646 else: 1647 return [g["name"] for g in groups]
1648
1649 - def GetGroup(self, group):
1650 """Gets information about a node group. 1651 1652 @type group: str 1653 @param group: name of the node group whose info to return 1654 1655 @rtype: dict 1656 @return: info about the node group 1657 1658 """ 1659 return self._SendRequest(HTTP_GET, 1660 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group), 1661 None, None)
1662
1663 - def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1664 """Creates a new node group. 1665 1666 @type name: str 1667 @param name: the name of node group to create 1668 @type alloc_policy: str 1669 @param alloc_policy: the desired allocation policy for the group, if any 1670 @type dry_run: bool 1671 @param dry_run: whether to peform a dry run 1672 1673 @rtype: string 1674 @return: job id 1675 1676 """ 1677 query = [] 1678 _AppendDryRunIf(query, dry_run) 1679 1680 body = { 1681 "name": name, 1682 "alloc_policy": alloc_policy 1683 } 1684 1685 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION, 1686 query, body)
1687
1688 - def ModifyGroup(self, group, **kwargs):
1689 """Modifies a node group. 1690 1691 More details for parameters can be found in the RAPI documentation. 1692 1693 @type group: string 1694 @param group: Node group name 1695 @rtype: string 1696 @return: job id 1697 1698 """ 1699 return self._SendRequest(HTTP_PUT, 1700 ("/%s/groups/%s/modify" % 1701 (GANETI_RAPI_VERSION, group)), None, kwargs)
1702
1703 - def DeleteGroup(self, group, dry_run=False):
1704 """Deletes a node group. 1705 1706 @type group: str 1707 @param group: the node group to delete 1708 @type dry_run: bool 1709 @param dry_run: whether to peform a dry run 1710 1711 @rtype: string 1712 @return: job id 1713 1714 """ 1715 query = [] 1716 _AppendDryRunIf(query, dry_run) 1717 1718 return self._SendRequest(HTTP_DELETE, 1719 ("/%s/groups/%s" % 1720 (GANETI_RAPI_VERSION, group)), query, None)
1721
1722 - def RenameGroup(self, group, new_name):
1723 """Changes the name of a node group. 1724 1725 @type group: string 1726 @param group: Node group name 1727 @type new_name: string 1728 @param new_name: New node group name 1729 1730 @rtype: string 1731 @return: job id 1732 1733 """ 1734 body = { 1735 "new_name": new_name, 1736 } 1737 1738 return self._SendRequest(HTTP_PUT, 1739 ("/%s/groups/%s/rename" % 1740 (GANETI_RAPI_VERSION, group)), None, body)
1741
1742 - def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1743 """Assigns nodes to a group. 1744 1745 @type group: string 1746 @param group: Node gropu name 1747 @type nodes: list of strings 1748 @param nodes: List of nodes to assign to the group 1749 1750 @rtype: string 1751 @return: job id 1752 1753 """ 1754 query = [] 1755 _AppendForceIf(query, force) 1756 _AppendDryRunIf(query, dry_run) 1757 1758 body = { 1759 "nodes": nodes, 1760 } 1761 1762 return self._SendRequest(HTTP_PUT, 1763 ("/%s/groups/%s/assign-nodes" % 1764 (GANETI_RAPI_VERSION, group)), query, body)
1765
1766 - def GetGroupTags(self, group):
1767 """Gets tags for a node group. 1768 1769 @type group: string 1770 @param group: Node group whose tags to return 1771 1772 @rtype: list of strings 1773 @return: tags for the group 1774 1775 """ 1776 return self._SendRequest(HTTP_GET, 1777 ("/%s/groups/%s/tags" % 1778 (GANETI_RAPI_VERSION, group)), None, None)
1779
1780 - def AddGroupTags(self, group, tags, dry_run=False):
1781 """Adds tags to a node group. 1782 1783 @type group: str 1784 @param group: group to add tags to 1785 @type tags: list of string 1786 @param tags: tags to add to the group 1787 @type dry_run: bool 1788 @param dry_run: whether to perform a dry run 1789 1790 @rtype: string 1791 @return: job id 1792 1793 """ 1794 query = [("tag", t) for t in tags] 1795 _AppendDryRunIf(query, dry_run) 1796 1797 return self._SendRequest(HTTP_PUT, 1798 ("/%s/groups/%s/tags" % 1799 (GANETI_RAPI_VERSION, group)), query, None)
1800
1801 - def DeleteGroupTags(self, group, tags, dry_run=False):
1802 """Deletes tags from a node group. 1803 1804 @type group: str 1805 @param group: group to delete tags from 1806 @type tags: list of string 1807 @param tags: tags to delete 1808 @type dry_run: bool 1809 @param dry_run: whether to perform a dry run 1810 @rtype: string 1811 @return: job id 1812 1813 """ 1814 query = [("tag", t) for t in tags] 1815 _AppendDryRunIf(query, dry_run) 1816 1817 return self._SendRequest(HTTP_DELETE, 1818 ("/%s/groups/%s/tags" % 1819 (GANETI_RAPI_VERSION, group)), query, None)
1820
1821 - def Query(self, what, fields, qfilter=None):
1822 """Retrieves information about resources. 1823 1824 @type what: string 1825 @param what: Resource name, one of L{constants.QR_VIA_RAPI} 1826 @type fields: list of string 1827 @param fields: Requested fields 1828 @type qfilter: None or list 1829 @param qfilter: Query filter 1830 1831 @rtype: string 1832 @return: job id 1833 1834 """ 1835 body = { 1836 "fields": fields, 1837 } 1838 1839 _SetItemIf(body, qfilter is not None, "qfilter", qfilter) 1840 # TODO: remove "filter" after 2.7 1841 _SetItemIf(body, qfilter is not None, "filter", qfilter) 1842 1843 return self._SendRequest(HTTP_PUT, 1844 ("/%s/query/%s" % 1845 (GANETI_RAPI_VERSION, what)), None, body)
1846
1847 - def QueryFields(self, what, fields=None):
1848 """Retrieves available fields for a resource. 1849 1850 @type what: string 1851 @param what: Resource name, one of L{constants.QR_VIA_RAPI} 1852 @type fields: list of string 1853 @param fields: Requested fields 1854 1855 @rtype: string 1856 @return: job id 1857 1858 """ 1859 query = [] 1860 1861 if fields is not None: 1862 _AppendIf(query, True, ("fields", ",".join(fields))) 1863 1864 return self._SendRequest(HTTP_GET, 1865 ("/%s/query/%s/fields" % 1866 (GANETI_RAPI_VERSION, what)), query, None)
1867