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 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 urllib 
  39  import threading 
  40  import pycurl 
  41   
  42  try: 
  43    from cStringIO import StringIO 
  44  except ImportError: 
  45    from StringIO import StringIO 
  46   
  47   
  48  GANETI_RAPI_PORT = 5080 
  49  GANETI_RAPI_VERSION = 2 
  50   
  51  HTTP_DELETE = "DELETE" 
  52  HTTP_GET = "GET" 
  53  HTTP_PUT = "PUT" 
  54  HTTP_POST = "POST" 
  55  HTTP_OK = 200 
  56  HTTP_NOT_FOUND = 404 
  57  HTTP_APP_JSON = "application/json" 
  58   
  59  REPLACE_DISK_PRI = "replace_on_primary" 
  60  REPLACE_DISK_SECONDARY = "replace_on_secondary" 
  61  REPLACE_DISK_CHG = "replace_new_secondary" 
  62  REPLACE_DISK_AUTO = "replace_auto" 
  63   
  64  NODE_ROLE_DRAINED = "drained" 
  65  NODE_ROLE_MASTER_CANDIATE = "master-candidate" 
  66  NODE_ROLE_MASTER = "master" 
  67  NODE_ROLE_OFFLINE = "offline" 
  68  NODE_ROLE_REGULAR = "regular" 
  69   
  70  # Internal constants 
  71  _REQ_DATA_VERSION_FIELD = "__version__" 
  72  _INST_CREATE_REQV1 = "instance-create-reqv1" 
  73  _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"]) 
  74  _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"]) 
  75  _INST_CREATE_V0_PARAMS = frozenset([ 
  76    "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check", 
  77    "hypervisor", "file_storage_dir", "file_driver", "dry_run", 
  78    ]) 
  79  _INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"]) 
  80   
  81  # Older pycURL versions don't have all error constants 
  82  try: 
  83    _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT 
  84    _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE 
  85  except AttributeError: 
  86    _CURLE_SSL_CACERT = 60 
  87    _CURLE_SSL_CACERT_BADFILE = 77 
  88   
  89  _CURL_SSL_CERT_ERRORS = frozenset([ 
  90    _CURLE_SSL_CACERT, 
  91    _CURLE_SSL_CACERT_BADFILE, 
  92    ]) 
93 94 95 -class Error(Exception):
96 """Base error class for this module. 97 98 """ 99 pass
100
101 102 -class CertificateError(Error):
103 """Raised when a problem is found with the SSL certificate. 104 105 """ 106 pass
107
108 109 -class GanetiApiError(Error):
110 """Generic error raised from Ganeti API. 111 112 """
113 - def __init__(self, msg, code=None):
114 Error.__init__(self, msg) 115 self.code = code
116
117 118 -def UsesRapiClient(fn):
119 """Decorator for code using RAPI client to initialize pycURL. 120 121 """ 122 def wrapper(*args, **kwargs): 123 # curl_global_init(3) and curl_global_cleanup(3) must be called with only 124 # one thread running. This check is just a safety measure -- it doesn't 125 # cover all cases. 126 assert threading.activeCount() == 1, \ 127 "Found active threads when initializing pycURL" 128 129 pycurl.global_init(pycurl.GLOBAL_ALL) 130 try: 131 return fn(*args, **kwargs) 132 finally: 133 pycurl.global_cleanup()
134 135 return wrapper 136
137 138 -def GenericCurlConfig(verbose=False, use_signal=False, 139 use_curl_cabundle=False, cafile=None, capath=None, 140 proxy=None, verify_hostname=False, 141 connect_timeout=None, timeout=None, 142 _pycurl_version_fn=pycurl.version_info):
143 """Curl configuration function generator. 144 145 @type verbose: bool 146 @param verbose: Whether to set cURL to verbose mode 147 @type use_signal: bool 148 @param use_signal: Whether to allow cURL to use signals 149 @type use_curl_cabundle: bool 150 @param use_curl_cabundle: Whether to use cURL's default CA bundle 151 @type cafile: string 152 @param cafile: In which file we can find the certificates 153 @type capath: string 154 @param capath: In which directory we can find the certificates 155 @type proxy: string 156 @param proxy: Proxy to use, None for default behaviour and empty string for 157 disabling proxies (see curl_easy_setopt(3)) 158 @type verify_hostname: bool 159 @param verify_hostname: Whether to verify the remote peer certificate's 160 commonName 161 @type connect_timeout: number 162 @param connect_timeout: Timeout for establishing connection in seconds 163 @type timeout: number 164 @param timeout: Timeout for complete transfer in seconds (see 165 curl_easy_setopt(3)). 166 167 """ 168 if use_curl_cabundle and (cafile or capath): 169 raise Error("Can not use default CA bundle when CA file or path is set") 170 171 def _ConfigCurl(curl, logger): 172 """Configures a cURL object 173 174 @type curl: pycurl.Curl 175 @param curl: cURL object 176 177 """ 178 logger.debug("Using cURL version %s", pycurl.version) 179 180 # pycurl.version_info returns a tuple with information about the used 181 # version of libcurl. Item 5 is the SSL library linked to it. 182 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4', 183 # 0, '1.2.3.3', ...) 184 sslver = _pycurl_version_fn()[5] 185 if not sslver: 186 raise Error("No SSL support in cURL") 187 188 lcsslver = sslver.lower() 189 if lcsslver.startswith("openssl/"): 190 pass 191 elif lcsslver.startswith("gnutls/"): 192 if capath: 193 raise Error("cURL linked against GnuTLS has no support for a" 194 " CA path (%s)" % (pycurl.version, )) 195 else: 196 raise NotImplementedError("cURL uses unsupported SSL version '%s'" % 197 sslver) 198 199 curl.setopt(pycurl.VERBOSE, verbose) 200 curl.setopt(pycurl.NOSIGNAL, not use_signal) 201 202 # Whether to verify remote peer's CN 203 if verify_hostname: 204 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that 205 # certificate must indicate that the server is the server to which you 206 # meant to connect, or the connection fails. [...] When the value is 1, 207 # the certificate must contain a Common Name field, but it doesn't matter 208 # what name it says. [...]" 209 curl.setopt(pycurl.SSL_VERIFYHOST, 2) 210 else: 211 curl.setopt(pycurl.SSL_VERIFYHOST, 0) 212 213 if cafile or capath or use_curl_cabundle: 214 # Require certificates to be checked 215 curl.setopt(pycurl.SSL_VERIFYPEER, True) 216 if cafile: 217 curl.setopt(pycurl.CAINFO, str(cafile)) 218 if capath: 219 curl.setopt(pycurl.CAPATH, str(capath)) 220 # Not changing anything for using default CA bundle 221 else: 222 # Disable SSL certificate verification 223 curl.setopt(pycurl.SSL_VERIFYPEER, False) 224 225 if proxy is not None: 226 curl.setopt(pycurl.PROXY, str(proxy)) 227 228 # Timeouts 229 if connect_timeout is not None: 230 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout) 231 if timeout is not None: 232 curl.setopt(pycurl.TIMEOUT, timeout)
233 234 return _ConfigCurl 235
236 237 -class GanetiRapiClient(object):
238 """Ganeti RAPI client. 239 240 """ 241 USER_AGENT = "Ganeti RAPI Client" 242 _json_encoder = simplejson.JSONEncoder(sort_keys=True) 243
244 - def __init__(self, host, port=GANETI_RAPI_PORT, 245 username=None, password=None, logger=logging, 246 curl_config_fn=None, curl_factory=None):
247 """Initializes this class. 248 249 @type host: string 250 @param host: the ganeti cluster master to interact with 251 @type port: int 252 @param port: the port on which the RAPI is running (default is 5080) 253 @type username: string 254 @param username: the username to connect with 255 @type password: string 256 @param password: the password to connect with 257 @type curl_config_fn: callable 258 @param curl_config_fn: Function to configure C{pycurl.Curl} object 259 @param logger: Logging object 260 261 """ 262 self._username = username 263 self._password = password 264 self._logger = logger 265 self._curl_config_fn = curl_config_fn 266 self._curl_factory = curl_factory 267 268 self._base_url = "https://%s:%s" % (host, port) 269 270 if username is not None: 271 if password is None: 272 raise Error("Password not specified") 273 elif password: 274 raise Error("Specified password without username")
275
276 - def _CreateCurl(self):
277 """Creates a cURL object. 278 279 """ 280 # Create pycURL object if no factory is provided 281 if self._curl_factory: 282 curl = self._curl_factory() 283 else: 284 curl = pycurl.Curl() 285 286 # Default cURL settings 287 curl.setopt(pycurl.VERBOSE, False) 288 curl.setopt(pycurl.FOLLOWLOCATION, False) 289 curl.setopt(pycurl.MAXREDIRS, 5) 290 curl.setopt(pycurl.NOSIGNAL, True) 291 curl.setopt(pycurl.USERAGENT, self.USER_AGENT) 292 curl.setopt(pycurl.SSL_VERIFYHOST, 0) 293 curl.setopt(pycurl.SSL_VERIFYPEER, False) 294 curl.setopt(pycurl.HTTPHEADER, [ 295 "Accept: %s" % HTTP_APP_JSON, 296 "Content-type: %s" % HTTP_APP_JSON, 297 ]) 298 299 assert ((self._username is None and self._password is None) ^ 300 (self._username is not None and self._password is not None)) 301 302 if self._username: 303 # Setup authentication 304 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) 305 curl.setopt(pycurl.USERPWD, 306 str("%s:%s" % (self._username, self._password))) 307 308 # Call external configuration function 309 if self._curl_config_fn: 310 self._curl_config_fn(curl, self._logger) 311 312 return curl
313 314 @staticmethod
315 - def _EncodeQuery(query):
316 """Encode query values for RAPI URL. 317 318 @type query: list of two-tuples 319 @param query: Query arguments 320 @rtype: list 321 @return: Query list with encoded values 322 323 """ 324 result = [] 325 326 for name, value in query: 327 if value is None: 328 result.append((name, "")) 329 330 elif isinstance(value, bool): 331 # Boolean values must be encoded as 0 or 1 332 result.append((name, int(value))) 333 334 elif isinstance(value, (list, tuple, dict)): 335 raise ValueError("Invalid query data type %r" % type(value).__name__) 336 337 else: 338 result.append((name, value)) 339 340 return result
341
342 - def _SendRequest(self, method, path, query, content):
343 """Sends an HTTP request. 344 345 This constructs a full URL, encodes and decodes HTTP bodies, and 346 handles invalid responses in a pythonic way. 347 348 @type method: string 349 @param method: HTTP method to use 350 @type path: string 351 @param path: HTTP URL path 352 @type query: list of two-tuples 353 @param query: query arguments to pass to urllib.urlencode 354 @type content: str or None 355 @param content: HTTP body content 356 357 @rtype: str 358 @return: JSON-Decoded response 359 360 @raises CertificateError: If an invalid SSL certificate is found 361 @raises GanetiApiError: If an invalid response is returned 362 363 """ 364 assert path.startswith("/") 365 366 curl = self._CreateCurl() 367 368 if content is not None: 369 encoded_content = self._json_encoder.encode(content) 370 else: 371 encoded_content = "" 372 373 # Build URL 374 urlparts = [self._base_url, path] 375 if query: 376 urlparts.append("?") 377 urlparts.append(urllib.urlencode(self._EncodeQuery(query))) 378 379 url = "".join(urlparts) 380 381 self._logger.debug("Sending request %s %s (content=%r)", 382 method, url, encoded_content) 383 384 # Buffer for response 385 encoded_resp_body = StringIO() 386 387 # Configure cURL 388 curl.setopt(pycurl.CUSTOMREQUEST, str(method)) 389 curl.setopt(pycurl.URL, str(url)) 390 curl.setopt(pycurl.POSTFIELDS, str(encoded_content)) 391 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write) 392 393 try: 394 # Send request and wait for response 395 try: 396 curl.perform() 397 except pycurl.error, err: 398 if err.args[0] in _CURL_SSL_CERT_ERRORS: 399 raise CertificateError("SSL certificate error %s" % err) 400 401 raise GanetiApiError(str(err)) 402 finally: 403 # Reset settings to not keep references to large objects in memory 404 # between requests 405 curl.setopt(pycurl.POSTFIELDS, "") 406 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None) 407 408 # Get HTTP response code 409 http_code = curl.getinfo(pycurl.RESPONSE_CODE) 410 411 # Was anything written to the response buffer? 412 if encoded_resp_body.tell(): 413 response_content = simplejson.loads(encoded_resp_body.getvalue()) 414 else: 415 response_content = None 416 417 if http_code != HTTP_OK: 418 if isinstance(response_content, dict): 419 msg = ("%s %s: %s" % 420 (response_content["code"], 421 response_content["message"], 422 response_content["explain"])) 423 else: 424 msg = str(response_content) 425 426 raise GanetiApiError(msg, code=http_code) 427 428 return response_content
429
430 - def GetVersion(self):
431 """Gets the Remote API version running on the cluster. 432 433 @rtype: int 434 @return: Ganeti Remote API version 435 436 """ 437 return self._SendRequest(HTTP_GET, "/version", None, None)
438
439 - def GetFeatures(self):
440 """Gets the list of optional features supported by RAPI server. 441 442 @rtype: list 443 @return: List of optional features 444 445 """ 446 try: 447 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION, 448 None, None) 449 except GanetiApiError, err: 450 # Older RAPI servers don't support this resource 451 if err.code == HTTP_NOT_FOUND: 452 return [] 453 454 raise
455
456 - def GetOperatingSystems(self):
457 """Gets the Operating Systems running in the Ganeti cluster. 458 459 @rtype: list of str 460 @return: operating systems 461 462 """ 463 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION, 464 None, None)
465
466 - def GetInfo(self):
467 """Gets info about the cluster. 468 469 @rtype: dict 470 @return: information about the cluster 471 472 """ 473 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION, 474 None, None)
475
476 - def GetClusterTags(self):
477 """Gets the cluster tags. 478 479 @rtype: list of str 480 @return: cluster tags 481 482 """ 483 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION, 484 None, None)
485
486 - def AddClusterTags(self, tags, dry_run=False):
487 """Adds tags to the cluster. 488 489 @type tags: list of str 490 @param tags: tags to add to the cluster 491 @type dry_run: bool 492 @param dry_run: whether to perform a dry run 493 494 @rtype: int 495 @return: job id 496 497 """ 498 query = [("tag", t) for t in tags] 499 if dry_run: 500 query.append(("dry-run", 1)) 501 502 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION, 503 query, None)
504
505 - def DeleteClusterTags(self, tags, dry_run=False):
506 """Deletes tags from the cluster. 507 508 @type tags: list of str 509 @param tags: tags to delete 510 @type dry_run: bool 511 @param dry_run: whether to perform a dry run 512 513 """ 514 query = [("tag", t) for t in tags] 515 if dry_run: 516 query.append(("dry-run", 1)) 517 518 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION, 519 query, None)
520
521 - def GetInstances(self, bulk=False):
522 """Gets information about instances on the cluster. 523 524 @type bulk: bool 525 @param bulk: whether to return all information about all instances 526 527 @rtype: list of dict or list of str 528 @return: if bulk is True, info about the instances, else a list of instances 529 530 """ 531 query = [] 532 if bulk: 533 query.append(("bulk", 1)) 534 535 instances = self._SendRequest(HTTP_GET, 536 "/%s/instances" % GANETI_RAPI_VERSION, 537 query, None) 538 if bulk: 539 return instances 540 else: 541 return [i["id"] for i in instances]
542
543 - def GetInstance(self, instance):
544 """Gets information about an instance. 545 546 @type instance: str 547 @param instance: instance whose info to return 548 549 @rtype: dict 550 @return: info about the instance 551 552 """ 553 return self._SendRequest(HTTP_GET, 554 ("/%s/instances/%s" % 555 (GANETI_RAPI_VERSION, instance)), None, None)
556
557 - def GetInstanceInfo(self, instance, static=None):
558 """Gets information about an instance. 559 560 @type instance: string 561 @param instance: Instance name 562 @rtype: string 563 @return: Job ID 564 565 """ 566 if static is not None: 567 query = [("static", static)] 568 else: 569 query = None 570 571 return self._SendRequest(HTTP_GET, 572 ("/%s/instances/%s/info" % 573 (GANETI_RAPI_VERSION, instance)), query, None)
574
575 - def CreateInstance(self, mode, name, disk_template, disks, nics, 576 **kwargs):
577 """Creates a new instance. 578 579 More details for parameters can be found in the RAPI documentation. 580 581 @type mode: string 582 @param mode: Instance creation mode 583 @type name: string 584 @param name: Hostname of the instance to create 585 @type disk_template: string 586 @param disk_template: Disk template for instance (e.g. plain, diskless, 587 file, or drbd) 588 @type disks: list of dicts 589 @param disks: List of disk definitions 590 @type nics: list of dicts 591 @param nics: List of NIC definitions 592 @type dry_run: bool 593 @keyword dry_run: whether to perform a dry run 594 595 @rtype: int 596 @return: job id 597 598 """ 599 query = [] 600 601 if kwargs.get("dry_run"): 602 query.append(("dry-run", 1)) 603 604 if _INST_CREATE_REQV1 in self.GetFeatures(): 605 # All required fields for request data version 1 606 body = { 607 _REQ_DATA_VERSION_FIELD: 1, 608 "mode": mode, 609 "name": name, 610 "disk_template": disk_template, 611 "disks": disks, 612 "nics": nics, 613 } 614 615 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys()) 616 if conflicts: 617 raise GanetiApiError("Required fields can not be specified as" 618 " keywords: %s" % ", ".join(conflicts)) 619 620 body.update((key, value) for key, value in kwargs.iteritems() 621 if key != "dry_run") 622 else: 623 # Old request format (version 0) 624 625 # The following code must make sure that an exception is raised when an 626 # unsupported setting is requested by the caller. Otherwise this can lead 627 # to bugs difficult to find. The interface of this function must stay 628 # exactly the same for version 0 and 1 (e.g. they aren't allowed to 629 # require different data types). 630 631 # Validate disks 632 for idx, disk in enumerate(disks): 633 unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS 634 if unsupported: 635 raise GanetiApiError("Server supports request version 0 only, but" 636 " disk %s specifies the unsupported parameters" 637 " %s, allowed are %s" % 638 (idx, unsupported, 639 list(_INST_CREATE_V0_DISK_PARAMS))) 640 641 assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and 642 "size" in _INST_CREATE_V0_DISK_PARAMS) 643 disk_sizes = [disk["size"] for disk in disks] 644 645 # Validate NICs 646 if not nics: 647 raise GanetiApiError("Server supports request version 0 only, but" 648 " no NIC specified") 649 elif len(nics) > 1: 650 raise GanetiApiError("Server supports request version 0 only, but" 651 " more than one NIC specified") 652 653 assert len(nics) == 1 654 655 unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS 656 if unsupported: 657 raise GanetiApiError("Server supports request version 0 only, but" 658 " NIC 0 specifies the unsupported parameters %s," 659 " allowed are %s" % 660 (unsupported, list(_INST_NIC_PARAMS))) 661 662 # Validate other parameters 663 unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS - 664 _INST_CREATE_V0_DPARAMS) 665 if unsupported: 666 allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS) 667 raise GanetiApiError("Server supports request version 0 only, but" 668 " the following unsupported parameters are" 669 " specified: %s, allowed are %s" % 670 (unsupported, list(allowed))) 671 672 # All required fields for request data version 0 673 body = { 674 _REQ_DATA_VERSION_FIELD: 0, 675 "name": name, 676 "disk_template": disk_template, 677 "disks": disk_sizes, 678 } 679 680 # NIC fields 681 assert len(nics) == 1 682 assert not (set(body.keys()) & set(nics[0].keys())) 683 body.update(nics[0]) 684 685 # Copy supported fields 686 assert not (set(body.keys()) & set(kwargs.keys())) 687 body.update(dict((key, value) for key, value in kwargs.items() 688 if key in _INST_CREATE_V0_PARAMS)) 689 690 # Merge dictionaries 691 for i in (value for key, value in kwargs.items() 692 if key in _INST_CREATE_V0_DPARAMS): 693 assert not (set(body.keys()) & set(i.keys())) 694 body.update(i) 695 696 assert not (set(kwargs.keys()) - 697 (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS)) 698 assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS) 699 700 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION, 701 query, body)
702
703 - def DeleteInstance(self, instance, dry_run=False):
704 """Deletes an instance. 705 706 @type instance: str 707 @param instance: the instance to delete 708 709 @rtype: int 710 @return: job id 711 712 """ 713 query = [] 714 if dry_run: 715 query.append(("dry-run", 1)) 716 717 return self._SendRequest(HTTP_DELETE, 718 ("/%s/instances/%s" % 719 (GANETI_RAPI_VERSION, instance)), query, None)
720
721 - def ModifyInstance(self, instance, **kwargs):
722 """Modifies an instance. 723 724 More details for parameters can be found in the RAPI documentation. 725 726 @type instance: string 727 @param instance: Instance name 728 @rtype: int 729 @return: job id 730 731 """ 732 body = kwargs 733 734 return self._SendRequest(HTTP_PUT, 735 ("/%s/instances/%s/modify" % 736 (GANETI_RAPI_VERSION, instance)), None, body)
737
738 - def GetInstanceTags(self, instance):
739 """Gets tags for an instance. 740 741 @type instance: str 742 @param instance: instance whose tags to return 743 744 @rtype: list of str 745 @return: tags for the instance 746 747 """ 748 return self._SendRequest(HTTP_GET, 749 ("/%s/instances/%s/tags" % 750 (GANETI_RAPI_VERSION, instance)), None, None)
751
752 - def AddInstanceTags(self, instance, tags, dry_run=False):
753 """Adds tags to an instance. 754 755 @type instance: str 756 @param instance: instance to add tags to 757 @type tags: list of str 758 @param tags: tags to add to the instance 759 @type dry_run: bool 760 @param dry_run: whether to perform a dry run 761 762 @rtype: int 763 @return: job id 764 765 """ 766 query = [("tag", t) for t in tags] 767 if dry_run: 768 query.append(("dry-run", 1)) 769 770 return self._SendRequest(HTTP_PUT, 771 ("/%s/instances/%s/tags" % 772 (GANETI_RAPI_VERSION, instance)), query, None)
773
774 - def DeleteInstanceTags(self, instance, tags, dry_run=False):
775 """Deletes tags from an instance. 776 777 @type instance: str 778 @param instance: instance to delete tags from 779 @type tags: list of str 780 @param tags: tags to delete 781 @type dry_run: bool 782 @param dry_run: whether to perform a dry run 783 784 """ 785 query = [("tag", t) for t in tags] 786 if dry_run: 787 query.append(("dry-run", 1)) 788 789 return self._SendRequest(HTTP_DELETE, 790 ("/%s/instances/%s/tags" % 791 (GANETI_RAPI_VERSION, instance)), query, None)
792
793 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None, 794 dry_run=False):
795 """Reboots an instance. 796 797 @type instance: str 798 @param instance: instance to rebot 799 @type reboot_type: str 800 @param reboot_type: one of: hard, soft, full 801 @type ignore_secondaries: bool 802 @param ignore_secondaries: if True, ignores errors for the secondary node 803 while re-assembling disks (in hard-reboot mode only) 804 @type dry_run: bool 805 @param dry_run: whether to perform a dry run 806 807 """ 808 query = [] 809 if reboot_type: 810 query.append(("type", reboot_type)) 811 if ignore_secondaries is not None: 812 query.append(("ignore_secondaries", ignore_secondaries)) 813 if dry_run: 814 query.append(("dry-run", 1)) 815 816 return self._SendRequest(HTTP_POST, 817 ("/%s/instances/%s/reboot" % 818 (GANETI_RAPI_VERSION, instance)), query, None)
819
820 - def ShutdownInstance(self, instance, dry_run=False):
821 """Shuts down an instance. 822 823 @type instance: str 824 @param instance: the instance to shut down 825 @type dry_run: bool 826 @param dry_run: whether to perform a dry run 827 828 """ 829 query = [] 830 if dry_run: 831 query.append(("dry-run", 1)) 832 833 return self._SendRequest(HTTP_PUT, 834 ("/%s/instances/%s/shutdown" % 835 (GANETI_RAPI_VERSION, instance)), query, None)
836
837 - def StartupInstance(self, instance, dry_run=False):
838 """Starts up an instance. 839 840 @type instance: str 841 @param instance: the instance to start up 842 @type dry_run: bool 843 @param dry_run: whether to perform a dry run 844 845 """ 846 query = [] 847 if dry_run: 848 query.append(("dry-run", 1)) 849 850 return self._SendRequest(HTTP_PUT, 851 ("/%s/instances/%s/startup" % 852 (GANETI_RAPI_VERSION, instance)), query, None)
853
854 - def ReinstallInstance(self, instance, os=None, no_startup=False):
855 """Reinstalls an instance. 856 857 @type instance: str 858 @param instance: The instance to reinstall 859 @type os: str or None 860 @param os: The operating system to reinstall. If None, the instance's 861 current operating system will be installed again 862 @type no_startup: bool 863 @param no_startup: Whether to start the instance automatically 864 865 """ 866 query = [] 867 if os: 868 query.append(("os", os)) 869 if no_startup: 870 query.append(("nostartup", 1)) 871 return self._SendRequest(HTTP_POST, 872 ("/%s/instances/%s/reinstall" % 873 (GANETI_RAPI_VERSION, instance)), query, None)
874
875 - def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO, 876 remote_node=None, iallocator=None, dry_run=False):
877 """Replaces disks on an instance. 878 879 @type instance: str 880 @param instance: instance whose disks to replace 881 @type disks: list of ints 882 @param disks: Indexes of disks to replace 883 @type mode: str 884 @param mode: replacement mode to use (defaults to replace_auto) 885 @type remote_node: str or None 886 @param remote_node: new secondary node to use (for use with 887 replace_new_secondary mode) 888 @type iallocator: str or None 889 @param iallocator: instance allocator plugin to use (for use with 890 replace_auto mode) 891 @type dry_run: bool 892 @param dry_run: whether to perform a dry run 893 894 @rtype: int 895 @return: job id 896 897 """ 898 query = [ 899 ("mode", mode), 900 ] 901 902 if disks: 903 query.append(("disks", ",".join(str(idx) for idx in disks))) 904 905 if remote_node: 906 query.append(("remote_node", remote_node)) 907 908 if iallocator: 909 query.append(("iallocator", iallocator)) 910 911 if dry_run: 912 query.append(("dry-run", 1)) 913 914 return self._SendRequest(HTTP_POST, 915 ("/%s/instances/%s/replace-disks" % 916 (GANETI_RAPI_VERSION, instance)), query, None)
917
918 - def PrepareExport(self, instance, mode):
919 """Prepares an instance for an export. 920 921 @type instance: string 922 @param instance: Instance name 923 @type mode: string 924 @param mode: Export mode 925 @rtype: string 926 @return: Job ID 927 928 """ 929 query = [("mode", mode)] 930 return self._SendRequest(HTTP_PUT, 931 ("/%s/instances/%s/prepare-export" % 932 (GANETI_RAPI_VERSION, instance)), query, None)
933
934 - def ExportInstance(self, instance, mode, destination, shutdown=None, 935 remove_instance=None, 936 x509_key_name=None, destination_x509_ca=None):
937 """Exports an instance. 938 939 @type instance: string 940 @param instance: Instance name 941 @type mode: string 942 @param mode: Export mode 943 @rtype: string 944 @return: Job ID 945 946 """ 947 body = { 948 "destination": destination, 949 "mode": mode, 950 } 951 952 if shutdown is not None: 953 body["shutdown"] = shutdown 954 955 if remove_instance is not None: 956 body["remove_instance"] = remove_instance 957 958 if x509_key_name is not None: 959 body["x509_key_name"] = x509_key_name 960 961 if destination_x509_ca is not None: 962 body["destination_x509_ca"] = destination_x509_ca 963 964 return self._SendRequest(HTTP_PUT, 965 ("/%s/instances/%s/export" % 966 (GANETI_RAPI_VERSION, instance)), None, body)
967
968 - def MigrateInstance(self, instance, mode=None, cleanup=None):
969 """Migrates an instance. 970 971 @type instance: string 972 @param instance: Instance name 973 @type mode: string 974 @param mode: Migration mode 975 @type cleanup: bool 976 @param cleanup: Whether to clean up a previously failed migration 977 978 """ 979 body = {} 980 981 if mode is not None: 982 body["mode"] = mode 983 984 if cleanup is not None: 985 body["cleanup"] = cleanup 986 987 return self._SendRequest(HTTP_PUT, 988 ("/%s/instances/%s/migrate" % 989 (GANETI_RAPI_VERSION, instance)), None, body)
990
991 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
992 """Changes the name of an instance. 993 994 @type instance: string 995 @param instance: Instance name 996 @type new_name: string 997 @param new_name: New instance name 998 @type ip_check: bool 999 @param ip_check: Whether to ensure instance's IP address is inactive 1000 @type name_check: bool 1001 @param name_check: Whether to ensure instance's name is resolvable 1002 1003 """ 1004 body = { 1005 "new_name": new_name, 1006 } 1007 1008 if ip_check is not None: 1009 body["ip_check"] = ip_check 1010 1011 if name_check is not None: 1012 body["name_check"] = name_check 1013 1014 return self._SendRequest(HTTP_PUT, 1015 ("/%s/instances/%s/rename" % 1016 (GANETI_RAPI_VERSION, instance)), None, body)
1017
1018 - def GetJobs(self):
1019 """Gets all jobs for the cluster. 1020 1021 @rtype: list of int 1022 @return: job ids for the cluster 1023 1024 """ 1025 return [int(j["id"]) 1026 for j in self._SendRequest(HTTP_GET, 1027 "/%s/jobs" % GANETI_RAPI_VERSION, 1028 None, None)]
1029
1030 - def GetJobStatus(self, job_id):
1031 """Gets the status of a job. 1032 1033 @type job_id: int 1034 @param job_id: job id whose status to query 1035 1036 @rtype: dict 1037 @return: job status 1038 1039 """ 1040 return self._SendRequest(HTTP_GET, 1041 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), 1042 None, None)
1043
1044 - def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1045 """Waits for job changes. 1046 1047 @type job_id: int 1048 @param job_id: Job ID for which to wait 1049 1050 """ 1051 body = { 1052 "fields": fields, 1053 "previous_job_info": prev_job_info, 1054 "previous_log_serial": prev_log_serial, 1055 } 1056 1057 return self._SendRequest(HTTP_GET, 1058 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id), 1059 None, body)
1060
1061 - def CancelJob(self, job_id, dry_run=False):
1062 """Cancels a job. 1063 1064 @type job_id: int 1065 @param job_id: id of the job to delete 1066 @type dry_run: bool 1067 @param dry_run: whether to perform a dry run 1068 1069 """ 1070 query = [] 1071 if dry_run: 1072 query.append(("dry-run", 1)) 1073 1074 return self._SendRequest(HTTP_DELETE, 1075 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), 1076 query, None)
1077
1078 - def GetNodes(self, bulk=False):
1079 """Gets all nodes in the cluster. 1080 1081 @type bulk: bool 1082 @param bulk: whether to return all information about all instances 1083 1084 @rtype: list of dict or str 1085 @return: if bulk is true, info about nodes in the cluster, 1086 else list of nodes in the cluster 1087 1088 """ 1089 query = [] 1090 if bulk: 1091 query.append(("bulk", 1)) 1092 1093 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION, 1094 query, None) 1095 if bulk: 1096 return nodes 1097 else: 1098 return [n["id"] for n in nodes]
1099
1100 - def GetNode(self, node):
1101 """Gets information about a node. 1102 1103 @type node: str 1104 @param node: node whose info to return 1105 1106 @rtype: dict 1107 @return: info about the node 1108 1109 """ 1110 return self._SendRequest(HTTP_GET, 1111 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node), 1112 None, None)
1113
1114 - def EvacuateNode(self, node, iallocator=None, remote_node=None, 1115 dry_run=False, early_release=False):
1116 """Evacuates instances from a Ganeti node. 1117 1118 @type node: str 1119 @param node: node to evacuate 1120 @type iallocator: str or None 1121 @param iallocator: instance allocator to use 1122 @type remote_node: str 1123 @param remote_node: node to evaucate to 1124 @type dry_run: bool 1125 @param dry_run: whether to perform a dry run 1126 @type early_release: bool 1127 @param early_release: whether to enable parallelization 1128 1129 @rtype: list 1130 @return: list of (job ID, instance name, new secondary node); if 1131 dry_run was specified, then the actual move jobs were not 1132 submitted and the job IDs will be C{None} 1133 1134 @raises GanetiApiError: if an iallocator and remote_node are both 1135 specified 1136 1137 """ 1138 if iallocator and remote_node: 1139 raise GanetiApiError("Only one of iallocator or remote_node can be used") 1140 1141 query = [] 1142 if iallocator: 1143 query.append(("iallocator", iallocator)) 1144 if remote_node: 1145 query.append(("remote_node", remote_node)) 1146 if dry_run: 1147 query.append(("dry-run", 1)) 1148 if early_release: 1149 query.append(("early_release", 1)) 1150 1151 return self._SendRequest(HTTP_POST, 1152 ("/%s/nodes/%s/evacuate" % 1153 (GANETI_RAPI_VERSION, node)), query, None)
1154
1155 - def MigrateNode(self, node, mode=None, dry_run=False):
1156 """Migrates all primary instances from a node. 1157 1158 @type node: str 1159 @param node: node to migrate 1160 @type mode: string 1161 @param mode: if passed, it will overwrite the live migration type, 1162 otherwise the hypervisor default will be used 1163 @type dry_run: bool 1164 @param dry_run: whether to perform a dry run 1165 1166 @rtype: int 1167 @return: job id 1168 1169 """ 1170 query = [] 1171 if mode is not None: 1172 query.append(("mode", mode)) 1173 if dry_run: 1174 query.append(("dry-run", 1)) 1175 1176 return self._SendRequest(HTTP_POST, 1177 ("/%s/nodes/%s/migrate" % 1178 (GANETI_RAPI_VERSION, node)), query, None)
1179
1180 - def GetNodeRole(self, node):
1181 """Gets the current role for a node. 1182 1183 @type node: str 1184 @param node: node whose role to return 1185 1186 @rtype: str 1187 @return: the current role for a node 1188 1189 """ 1190 return self._SendRequest(HTTP_GET, 1191 ("/%s/nodes/%s/role" % 1192 (GANETI_RAPI_VERSION, node)), None, None)
1193
1194 - def SetNodeRole(self, node, role, force=False):
1195 """Sets the role for a node. 1196 1197 @type node: str 1198 @param node: the node whose role to set 1199 @type role: str 1200 @param role: the role to set for the node 1201 @type force: bool 1202 @param force: whether to force the role change 1203 1204 @rtype: int 1205 @return: job id 1206 1207 """ 1208 query = [ 1209 ("force", force), 1210 ] 1211 1212 return self._SendRequest(HTTP_PUT, 1213 ("/%s/nodes/%s/role" % 1214 (GANETI_RAPI_VERSION, node)), query, role)
1215
1216 - def GetNodeStorageUnits(self, node, storage_type, output_fields):
1217 """Gets the storage units for a node. 1218 1219 @type node: str 1220 @param node: the node whose storage units to return 1221 @type storage_type: str 1222 @param storage_type: storage type whose units to return 1223 @type output_fields: str 1224 @param output_fields: storage type fields to return 1225 1226 @rtype: int 1227 @return: job id where results can be retrieved 1228 1229 """ 1230 query = [ 1231 ("storage_type", storage_type), 1232 ("output_fields", output_fields), 1233 ] 1234 1235 return self._SendRequest(HTTP_GET, 1236 ("/%s/nodes/%s/storage" % 1237 (GANETI_RAPI_VERSION, node)), query, None)
1238
1239 - def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1240 """Modifies parameters of storage units on the node. 1241 1242 @type node: str 1243 @param node: node whose storage units to modify 1244 @type storage_type: str 1245 @param storage_type: storage type whose units to modify 1246 @type name: str 1247 @param name: name of the storage unit 1248 @type allocatable: bool or None 1249 @param allocatable: Whether to set the "allocatable" flag on the storage 1250 unit (None=no modification, True=set, False=unset) 1251 1252 @rtype: int 1253 @return: job id 1254 1255 """ 1256 query = [ 1257 ("storage_type", storage_type), 1258 ("name", name), 1259 ] 1260 1261 if allocatable is not None: 1262 query.append(("allocatable", allocatable)) 1263 1264 return self._SendRequest(HTTP_PUT, 1265 ("/%s/nodes/%s/storage/modify" % 1266 (GANETI_RAPI_VERSION, node)), query, None)
1267
1268 - def RepairNodeStorageUnits(self, node, storage_type, name):
1269 """Repairs a storage unit on the node. 1270 1271 @type node: str 1272 @param node: node whose storage units to repair 1273 @type storage_type: str 1274 @param storage_type: storage type to repair 1275 @type name: str 1276 @param name: name of the storage unit to repair 1277 1278 @rtype: int 1279 @return: job id 1280 1281 """ 1282 query = [ 1283 ("storage_type", storage_type), 1284 ("name", name), 1285 ] 1286 1287 return self._SendRequest(HTTP_PUT, 1288 ("/%s/nodes/%s/storage/repair" % 1289 (GANETI_RAPI_VERSION, node)), query, None)
1290
1291 - def GetNodeTags(self, node):
1292 """Gets the tags for a node. 1293 1294 @type node: str 1295 @param node: node whose tags to return 1296 1297 @rtype: list of str 1298 @return: tags for the node 1299 1300 """ 1301 return self._SendRequest(HTTP_GET, 1302 ("/%s/nodes/%s/tags" % 1303 (GANETI_RAPI_VERSION, node)), None, None)
1304
1305 - def AddNodeTags(self, node, tags, dry_run=False):
1306 """Adds tags to a node. 1307 1308 @type node: str 1309 @param node: node to add tags to 1310 @type tags: list of str 1311 @param tags: tags to add to the node 1312 @type dry_run: bool 1313 @param dry_run: whether to perform a dry run 1314 1315 @rtype: int 1316 @return: job id 1317 1318 """ 1319 query = [("tag", t) for t in tags] 1320 if dry_run: 1321 query.append(("dry-run", 1)) 1322 1323 return self._SendRequest(HTTP_PUT, 1324 ("/%s/nodes/%s/tags" % 1325 (GANETI_RAPI_VERSION, node)), query, tags)
1326
1327 - def DeleteNodeTags(self, node, tags, dry_run=False):
1328 """Delete tags from a node. 1329 1330 @type node: str 1331 @param node: node to remove tags from 1332 @type tags: list of str 1333 @param tags: tags to remove from the node 1334 @type dry_run: bool 1335 @param dry_run: whether to perform a dry run 1336 1337 @rtype: int 1338 @return: job id 1339 1340 """ 1341 query = [("tag", t) for t in tags] 1342 if dry_run: 1343 query.append(("dry-run", 1)) 1344 1345 return self._SendRequest(HTTP_DELETE, 1346 ("/%s/nodes/%s/tags" % 1347 (GANETI_RAPI_VERSION, node)), query, None)
1348