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