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