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