Revision 3524241a

/dev/null
1
#
2
#
3

  
4
# Copyright (C) 2010, 2011 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

  
21

  
22
"""Ganeti RAPI client.
23

  
24
@attention: To use the RAPI client, the application B{must} call
25
            C{pycurl.global_init} during initialization and
26
            C{pycurl.global_cleanup} before exiting the process. This is very
27
            important in multi-threaded programs. See curl_global_init(3) and
28
            curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
29
            can be used.
30

  
31
"""
32

  
33
# No Ganeti-specific modules should be imported. The RAPI client is supposed to
34
# be standalone.
35

  
36
import logging
37
import simplejson
38
import socket
39
import urllib
40
import threading
41
import pycurl
42
import time
43

  
44
try:
45
  from cStringIO import StringIO
46
except ImportError:
47
  from StringIO import StringIO
48

  
49

  
50
GANETI_RAPI_PORT = 5080
51
GANETI_RAPI_VERSION = 2
52

  
53
HTTP_DELETE = "DELETE"
54
HTTP_GET = "GET"
55
HTTP_PUT = "PUT"
56
HTTP_POST = "POST"
57
HTTP_OK = 200
58
HTTP_NOT_FOUND = 404
59
HTTP_APP_JSON = "application/json"
60

  
61
REPLACE_DISK_PRI = "replace_on_primary"
62
REPLACE_DISK_SECONDARY = "replace_on_secondary"
63
REPLACE_DISK_CHG = "replace_new_secondary"
64
REPLACE_DISK_AUTO = "replace_auto"
65

  
66
NODE_EVAC_PRI = "primary-only"
67
NODE_EVAC_SEC = "secondary-only"
68
NODE_EVAC_ALL = "all"
69

  
70
NODE_ROLE_DRAINED = "drained"
71
NODE_ROLE_MASTER_CANDIATE = "master-candidate"
72
NODE_ROLE_MASTER = "master"
73
NODE_ROLE_OFFLINE = "offline"
74
NODE_ROLE_REGULAR = "regular"
75

  
76
JOB_STATUS_QUEUED = "queued"
77
JOB_STATUS_WAITING = "waiting"
78
JOB_STATUS_CANCELING = "canceling"
79
JOB_STATUS_RUNNING = "running"
80
JOB_STATUS_CANCELED = "canceled"
81
JOB_STATUS_SUCCESS = "success"
82
JOB_STATUS_ERROR = "error"
83
JOB_STATUS_FINALIZED = frozenset([
84
  JOB_STATUS_CANCELED,
85
  JOB_STATUS_SUCCESS,
86
  JOB_STATUS_ERROR,
87
  ])
88
JOB_STATUS_ALL = frozenset([
89
  JOB_STATUS_QUEUED,
90
  JOB_STATUS_WAITING,
91
  JOB_STATUS_CANCELING,
92
  JOB_STATUS_RUNNING,
93
  ]) | JOB_STATUS_FINALIZED
94

  
95
# Legacy name
96
JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
97

  
98
# Internal constants
99
_REQ_DATA_VERSION_FIELD = "__version__"
100
_QPARAM_DRY_RUN = "dry-run"
101
_QPARAM_FORCE = "force"
102

  
103
# Feature strings
104
INST_CREATE_REQV1 = "instance-create-reqv1"
105
INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
106
NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
107
NODE_EVAC_RES1 = "node-evac-res1"
108

  
109
# Old feature constant names in case they're references by users of this module
110
_INST_CREATE_REQV1 = INST_CREATE_REQV1
111
_INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
112
_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
113
_NODE_EVAC_RES1 = NODE_EVAC_RES1
114

  
115
# Older pycURL versions don't have all error constants
116
try:
117
  _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
118
  _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
119
except AttributeError:
120
  _CURLE_SSL_CACERT = 60
121
  _CURLE_SSL_CACERT_BADFILE = 77
122

  
123
_CURL_SSL_CERT_ERRORS = frozenset([
124
  _CURLE_SSL_CACERT,
125
  _CURLE_SSL_CACERT_BADFILE,
126
  ])
127

  
128

  
129
class Error(Exception):
130
  """Base error class for this module.
131

  
132
  """
133
  pass
134

  
135

  
136
class GanetiApiError(Error):
137
  """Generic error raised from Ganeti API.
138

  
139
  """
140
  def __init__(self, msg, code=None):
141
    Error.__init__(self, msg)
142
    self.code = code
143

  
144

  
145
class CertificateError(GanetiApiError):
146
  """Raised when a problem is found with the SSL certificate.
147

  
148
  """
149
  pass
150

  
151

  
152
def _AppendIf(container, condition, value):
153
  """Appends to a list if a condition evaluates to truth.
154

  
155
  """
156
  if condition:
157
    container.append(value)
158

  
159
  return condition
160

  
161

  
162
def _AppendDryRunIf(container, condition):
163
  """Appends a "dry-run" parameter if a condition evaluates to truth.
164

  
165
  """
166
  return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
167

  
168

  
169
def _AppendForceIf(container, condition):
170
  """Appends a "force" parameter if a condition evaluates to truth.
171

  
172
  """
173
  return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
174

  
175

  
176
def _SetItemIf(container, condition, item, value):
177
  """Sets an item if a condition evaluates to truth.
178

  
179
  """
180
  if condition:
181
    container[item] = value
182

  
183
  return condition
184

  
185

  
186
def UsesRapiClient(fn):
187
  """Decorator for code using RAPI client to initialize pycURL.
188

  
189
  """
190
  def wrapper(*args, **kwargs):
191
    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
192
    # one thread running. This check is just a safety measure -- it doesn't
193
    # cover all cases.
194
    assert threading.activeCount() == 1, \
195
           "Found active threads when initializing pycURL"
196

  
197
    pycurl.global_init(pycurl.GLOBAL_ALL)
198
    try:
199
      return fn(*args, **kwargs)
200
    finally:
201
      pycurl.global_cleanup()
202

  
203
  return wrapper
204

  
205

  
206
def GenericCurlConfig(verbose=False, use_signal=False,
207
                      use_curl_cabundle=False, cafile=None, capath=None,
208
                      proxy=None, verify_hostname=False,
209
                      connect_timeout=None, timeout=None,
210
                      _pycurl_version_fn=pycurl.version_info):
211
  """Curl configuration function generator.
212

  
213
  @type verbose: bool
214
  @param verbose: Whether to set cURL to verbose mode
215
  @type use_signal: bool
216
  @param use_signal: Whether to allow cURL to use signals
217
  @type use_curl_cabundle: bool
218
  @param use_curl_cabundle: Whether to use cURL's default CA bundle
219
  @type cafile: string
220
  @param cafile: In which file we can find the certificates
221
  @type capath: string
222
  @param capath: In which directory we can find the certificates
223
  @type proxy: string
224
  @param proxy: Proxy to use, None for default behaviour and empty string for
225
                disabling proxies (see curl_easy_setopt(3))
226
  @type verify_hostname: bool
227
  @param verify_hostname: Whether to verify the remote peer certificate's
228
                          commonName
229
  @type connect_timeout: number
230
  @param connect_timeout: Timeout for establishing connection in seconds
231
  @type timeout: number
232
  @param timeout: Timeout for complete transfer in seconds (see
233
                  curl_easy_setopt(3)).
234

  
235
  """
236
  if use_curl_cabundle and (cafile or capath):
237
    raise Error("Can not use default CA bundle when CA file or path is set")
238

  
239
  def _ConfigCurl(curl, logger):
240
    """Configures a cURL object
241

  
242
    @type curl: pycurl.Curl
243
    @param curl: cURL object
244

  
245
    """
246
    logger.debug("Using cURL version %s", pycurl.version)
247

  
248
    # pycurl.version_info returns a tuple with information about the used
249
    # version of libcurl. Item 5 is the SSL library linked to it.
250
    # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
251
    # 0, '1.2.3.3', ...)
252
    sslver = _pycurl_version_fn()[5]
253
    if not sslver:
254
      raise Error("No SSL support in cURL")
255

  
256
    lcsslver = sslver.lower()
257
    if lcsslver.startswith("openssl/"):
258
      pass
259
    elif lcsslver.startswith("gnutls/"):
260
      if capath:
261
        raise Error("cURL linked against GnuTLS has no support for a"
262
                    " CA path (%s)" % (pycurl.version, ))
263
    else:
264
      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
265
                                sslver)
266

  
267
    curl.setopt(pycurl.VERBOSE, verbose)
268
    curl.setopt(pycurl.NOSIGNAL, not use_signal)
269

  
270
    # Whether to verify remote peer's CN
271
    if verify_hostname:
272
      # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
273
      # certificate must indicate that the server is the server to which you
274
      # meant to connect, or the connection fails. [...] When the value is 1,
275
      # the certificate must contain a Common Name field, but it doesn't matter
276
      # what name it says. [...]"
277
      curl.setopt(pycurl.SSL_VERIFYHOST, 2)
278
    else:
279
      curl.setopt(pycurl.SSL_VERIFYHOST, 0)
280

  
281
    if cafile or capath or use_curl_cabundle:
282
      # Require certificates to be checked
283
      curl.setopt(pycurl.SSL_VERIFYPEER, True)
284
      if cafile:
285
        curl.setopt(pycurl.CAINFO, str(cafile))
286
      if capath:
287
        curl.setopt(pycurl.CAPATH, str(capath))
288
      # Not changing anything for using default CA bundle
289
    else:
290
      # Disable SSL certificate verification
291
      curl.setopt(pycurl.SSL_VERIFYPEER, False)
292

  
293
    if proxy is not None:
294
      curl.setopt(pycurl.PROXY, str(proxy))
295

  
296
    # Timeouts
297
    if connect_timeout is not None:
298
      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
299
    if timeout is not None:
300
      curl.setopt(pycurl.TIMEOUT, timeout)
301

  
302
  return _ConfigCurl
303

  
304

  
305
class GanetiRapiClient(object): # pylint: disable=R0904
306
  """Ganeti RAPI client.
307

  
308
  """
309
  USER_AGENT = "Ganeti RAPI Client"
310
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
311

  
312
  def __init__(self, host, port=GANETI_RAPI_PORT,
313
               username=None, password=None, logger=logging,
314
               curl_config_fn=None, curl_factory=None):
315
    """Initializes this class.
316

  
317
    @type host: string
318
    @param host: the ganeti cluster master to interact with
319
    @type port: int
320
    @param port: the port on which the RAPI is running (default is 5080)
321
    @type username: string
322
    @param username: the username to connect with
323
    @type password: string
324
    @param password: the password to connect with
325
    @type curl_config_fn: callable
326
    @param curl_config_fn: Function to configure C{pycurl.Curl} object
327
    @param logger: Logging object
328

  
329
    """
330
    self._username = username
331
    self._password = password
332
    self._logger = logger
333
    self._curl_config_fn = curl_config_fn
334
    self._curl_factory = curl_factory
335

  
336
    try:
337
      socket.inet_pton(socket.AF_INET6, host)
338
      address = "[%s]:%s" % (host, port)
339
    except socket.error:
340
      address = "%s:%s" % (host, port)
341

  
342
    self._base_url = "https://%s" % address
343

  
344
    if username is not None:
345
      if password is None:
346
        raise Error("Password not specified")
347
    elif password:
348
      raise Error("Specified password without username")
349

  
350
  def _CreateCurl(self):
351
    """Creates a cURL object.
352

  
353
    """
354
    # Create pycURL object if no factory is provided
355
    if self._curl_factory:
356
      curl = self._curl_factory()
357
    else:
358
      curl = pycurl.Curl()
359

  
360
    # Default cURL settings
361
    curl.setopt(pycurl.VERBOSE, False)
362
    curl.setopt(pycurl.FOLLOWLOCATION, False)
363
    curl.setopt(pycurl.MAXREDIRS, 5)
364
    curl.setopt(pycurl.NOSIGNAL, True)
365
    curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
366
    curl.setopt(pycurl.SSL_VERIFYHOST, 0)
367
    curl.setopt(pycurl.SSL_VERIFYPEER, False)
368
    curl.setopt(pycurl.HTTPHEADER, [
369
      "Accept: %s" % HTTP_APP_JSON,
370
      "Content-type: %s" % HTTP_APP_JSON,
371
      ])
372

  
373
    assert ((self._username is None and self._password is None) ^
374
            (self._username is not None and self._password is not None))
375

  
376
    if self._username:
377
      # Setup authentication
378
      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
379
      curl.setopt(pycurl.USERPWD,
380
                  str("%s:%s" % (self._username, self._password)))
381

  
382
    # Call external configuration function
383
    if self._curl_config_fn:
384
      self._curl_config_fn(curl, self._logger)
385

  
386
    return curl
387

  
388
  @staticmethod
389
  def _EncodeQuery(query):
390
    """Encode query values for RAPI URL.
391

  
392
    @type query: list of two-tuples
393
    @param query: Query arguments
394
    @rtype: list
395
    @return: Query list with encoded values
396

  
397
    """
398
    result = []
399

  
400
    for name, value in query:
401
      if value is None:
402
        result.append((name, ""))
403

  
404
      elif isinstance(value, bool):
405
        # Boolean values must be encoded as 0 or 1
406
        result.append((name, int(value)))
407

  
408
      elif isinstance(value, (list, tuple, dict)):
409
        raise ValueError("Invalid query data type %r" % type(value).__name__)
410

  
411
      else:
412
        result.append((name, value))
413

  
414
    return result
415

  
416
  def _SendRequest(self, method, path, query, content):
417
    """Sends an HTTP request.
418

  
419
    This constructs a full URL, encodes and decodes HTTP bodies, and
420
    handles invalid responses in a pythonic way.
421

  
422
    @type method: string
423
    @param method: HTTP method to use
424
    @type path: string
425
    @param path: HTTP URL path
426
    @type query: list of two-tuples
427
    @param query: query arguments to pass to urllib.urlencode
428
    @type content: str or None
429
    @param content: HTTP body content
430

  
431
    @rtype: str
432
    @return: JSON-Decoded response
433

  
434
    @raises CertificateError: If an invalid SSL certificate is found
435
    @raises GanetiApiError: If an invalid response is returned
436

  
437
    """
438
    assert path.startswith("/")
439

  
440
    curl = self._CreateCurl()
441

  
442
    if content is not None:
443
      encoded_content = self._json_encoder.encode(content)
444
    else:
445
      encoded_content = ""
446

  
447
    # Build URL
448
    urlparts = [self._base_url, path]
449
    if query:
450
      urlparts.append("?")
451
      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
452

  
453
    url = "".join(urlparts)
454

  
455
    self._logger.debug("Sending request %s %s (content=%r)",
456
                       method, url, encoded_content)
457

  
458
    # Buffer for response
459
    encoded_resp_body = StringIO()
460

  
461
    # Configure cURL
462
    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
463
    curl.setopt(pycurl.URL, str(url))
464
    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
465
    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
466

  
467
    try:
468
      # Send request and wait for response
469
      try:
470
        curl.perform()
471
      except pycurl.error, err:
472
        if err.args[0] in _CURL_SSL_CERT_ERRORS:
473
          raise CertificateError("SSL certificate error %s" % err,
474
                                 code=err.args[0])
475

  
476
        raise GanetiApiError(str(err), code=err.args[0])
477
    finally:
478
      # Reset settings to not keep references to large objects in memory
479
      # between requests
480
      curl.setopt(pycurl.POSTFIELDS, "")
481
      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
482

  
483
    # Get HTTP response code
484
    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
485

  
486
    # Was anything written to the response buffer?
487
    if encoded_resp_body.tell():
488
      response_content = simplejson.loads(encoded_resp_body.getvalue())
489
    else:
490
      response_content = None
491

  
492
    if http_code != HTTP_OK:
493
      if isinstance(response_content, dict):
494
        msg = ("%s %s: %s" %
495
               (response_content["code"],
496
                response_content["message"],
497
                response_content["explain"]))
498
      else:
499
        msg = str(response_content)
500

  
501
      raise GanetiApiError(msg, code=http_code)
502

  
503
    return response_content
504

  
505
  def GetVersion(self):
506
    """Gets the Remote API version running on the cluster.
507

  
508
    @rtype: int
509
    @return: Ganeti Remote API version
510

  
511
    """
512
    return self._SendRequest(HTTP_GET, "/version", None, None)
513

  
514
  def GetFeatures(self):
515
    """Gets the list of optional features supported by RAPI server.
516

  
517
    @rtype: list
518
    @return: List of optional features
519

  
520
    """
521
    try:
522
      return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
523
                               None, None)
524
    except GanetiApiError, err:
525
      # Older RAPI servers don't support this resource
526
      if err.code == HTTP_NOT_FOUND:
527
        return []
528

  
529
      raise
530

  
531
  def GetOperatingSystems(self):
532
    """Gets the Operating Systems running in the Ganeti cluster.
533

  
534
    @rtype: list of str
535
    @return: operating systems
536

  
537
    """
538
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
539
                             None, None)
540

  
541
  def GetInfo(self):
542
    """Gets info about the cluster.
543

  
544
    @rtype: dict
545
    @return: information about the cluster
546

  
547
    """
548
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
549
                             None, None)
550

  
551
  def RedistributeConfig(self):
552
    """Tells the cluster to redistribute its configuration files.
553

  
554
    @rtype: string
555
    @return: job id
556

  
557
    """
558
    return self._SendRequest(HTTP_PUT,
559
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
560
                             None, None)
561

  
562
  def ModifyCluster(self, **kwargs):
563
    """Modifies cluster parameters.
564

  
565
    More details for parameters can be found in the RAPI documentation.
566

  
567
    @rtype: string
568
    @return: job id
569

  
570
    """
571
    body = kwargs
572

  
573
    return self._SendRequest(HTTP_PUT,
574
                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
575

  
576
  def GetClusterTags(self):
577
    """Gets the cluster tags.
578

  
579
    @rtype: list of str
580
    @return: cluster tags
581

  
582
    """
583
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
584
                             None, None)
585

  
586
  def AddClusterTags(self, tags, dry_run=False):
587
    """Adds tags to the cluster.
588

  
589
    @type tags: list of str
590
    @param tags: tags to add to the cluster
591
    @type dry_run: bool
592
    @param dry_run: whether to perform a dry run
593

  
594
    @rtype: string
595
    @return: job id
596

  
597
    """
598
    query = [("tag", t) for t in tags]
599
    _AppendDryRunIf(query, dry_run)
600

  
601
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
602
                             query, None)
603

  
604
  def DeleteClusterTags(self, tags, dry_run=False):
605
    """Deletes tags from the cluster.
606

  
607
    @type tags: list of str
608
    @param tags: tags to delete
609
    @type dry_run: bool
610
    @param dry_run: whether to perform a dry run
611
    @rtype: string
612
    @return: job id
613

  
614
    """
615
    query = [("tag", t) for t in tags]
616
    _AppendDryRunIf(query, dry_run)
617

  
618
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
619
                             query, None)
620

  
621
  def GetInstances(self, bulk=False):
622
    """Gets information about instances on the cluster.
623

  
624
    @type bulk: bool
625
    @param bulk: whether to return all information about all instances
626

  
627
    @rtype: list of dict or list of str
628
    @return: if bulk is True, info about the instances, else a list of instances
629

  
630
    """
631
    query = []
632
    _AppendIf(query, bulk, ("bulk", 1))
633

  
634
    instances = self._SendRequest(HTTP_GET,
635
                                  "/%s/instances" % GANETI_RAPI_VERSION,
636
                                  query, None)
637
    if bulk:
638
      return instances
639
    else:
640
      return [i["id"] for i in instances]
641

  
642
  def GetInstance(self, instance):
643
    """Gets information about an instance.
644

  
645
    @type instance: str
646
    @param instance: instance whose info to return
647

  
648
    @rtype: dict
649
    @return: info about the instance
650

  
651
    """
652
    return self._SendRequest(HTTP_GET,
653
                             ("/%s/instances/%s" %
654
                              (GANETI_RAPI_VERSION, instance)), None, None)
655

  
656
  def GetInstanceInfo(self, instance, static=None):
657
    """Gets information about an instance.
658

  
659
    @type instance: string
660
    @param instance: Instance name
661
    @rtype: string
662
    @return: Job ID
663

  
664
    """
665
    if static is not None:
666
      query = [("static", static)]
667
    else:
668
      query = None
669

  
670
    return self._SendRequest(HTTP_GET,
671
                             ("/%s/instances/%s/info" %
672
                              (GANETI_RAPI_VERSION, instance)), query, None)
673

  
674
  def CreateInstance(self, mode, name, disk_template, disks, nics,
675
                     **kwargs):
676
    """Creates a new instance.
677

  
678
    More details for parameters can be found in the RAPI documentation.
679

  
680
    @type mode: string
681
    @param mode: Instance creation mode
682
    @type name: string
683
    @param name: Hostname of the instance to create
684
    @type disk_template: string
685
    @param disk_template: Disk template for instance (e.g. plain, diskless,
686
                          file, or drbd)
687
    @type disks: list of dicts
688
    @param disks: List of disk definitions
689
    @type nics: list of dicts
690
    @param nics: List of NIC definitions
691
    @type dry_run: bool
692
    @keyword dry_run: whether to perform a dry run
693

  
694
    @rtype: string
695
    @return: job id
696

  
697
    """
698
    query = []
699

  
700
    _AppendDryRunIf(query, kwargs.get("dry_run"))
701

  
702
    if _INST_CREATE_REQV1 in self.GetFeatures():
703
      # All required fields for request data version 1
704
      body = {
705
        _REQ_DATA_VERSION_FIELD: 1,
706
        "mode": mode,
707
        "name": name,
708
        "disk_template": disk_template,
709
        "disks": disks,
710
        "nics": nics,
711
        }
712

  
713
      conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
714
      if conflicts:
715
        raise GanetiApiError("Required fields can not be specified as"
716
                             " keywords: %s" % ", ".join(conflicts))
717

  
718
      body.update((key, value) for key, value in kwargs.iteritems()
719
                  if key != "dry_run")
720
    else:
721
      raise GanetiApiError("Server does not support new-style (version 1)"
722
                           " instance creation requests")
723

  
724
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
725
                             query, body)
726

  
727
  def DeleteInstance(self, instance, dry_run=False):
728
    """Deletes an instance.
729

  
730
    @type instance: str
731
    @param instance: the instance to delete
732

  
733
    @rtype: string
734
    @return: job id
735

  
736
    """
737
    query = []
738
    _AppendDryRunIf(query, dry_run)
739

  
740
    return self._SendRequest(HTTP_DELETE,
741
                             ("/%s/instances/%s" %
742
                              (GANETI_RAPI_VERSION, instance)), query, None)
743

  
744
  def ModifyInstance(self, instance, **kwargs):
745
    """Modifies an instance.
746

  
747
    More details for parameters can be found in the RAPI documentation.
748

  
749
    @type instance: string
750
    @param instance: Instance name
751
    @rtype: string
752
    @return: job id
753

  
754
    """
755
    body = kwargs
756

  
757
    return self._SendRequest(HTTP_PUT,
758
                             ("/%s/instances/%s/modify" %
759
                              (GANETI_RAPI_VERSION, instance)), None, body)
760

  
761
  def ActivateInstanceDisks(self, instance, ignore_size=None):
762
    """Activates an instance's disks.
763

  
764
    @type instance: string
765
    @param instance: Instance name
766
    @type ignore_size: bool
767
    @param ignore_size: Whether to ignore recorded size
768
    @rtype: string
769
    @return: job id
770

  
771
    """
772
    query = []
773
    _AppendIf(query, ignore_size, ("ignore_size", 1))
774

  
775
    return self._SendRequest(HTTP_PUT,
776
                             ("/%s/instances/%s/activate-disks" %
777
                              (GANETI_RAPI_VERSION, instance)), query, None)
778

  
779
  def DeactivateInstanceDisks(self, instance):
780
    """Deactivates an instance's disks.
781

  
782
    @type instance: string
783
    @param instance: Instance name
784
    @rtype: string
785
    @return: job id
786

  
787
    """
788
    return self._SendRequest(HTTP_PUT,
789
                             ("/%s/instances/%s/deactivate-disks" %
790
                              (GANETI_RAPI_VERSION, instance)), None, None)
791

  
792
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
793
    """Recreate an instance's disks.
794

  
795
    @type instance: string
796
    @param instance: Instance name
797
    @type disks: list of int
798
    @param disks: List of disk indexes
799
    @type nodes: list of string
800
    @param nodes: New instance nodes, if relocation is desired
801
    @rtype: string
802
    @return: job id
803

  
804
    """
805
    body = {}
806
    _SetItemIf(body, disks is not None, "disks", disks)
807
    _SetItemIf(body, nodes is not None, "nodes", nodes)
808

  
809
    return self._SendRequest(HTTP_POST,
810
                             ("/%s/instances/%s/recreate-disks" %
811
                              (GANETI_RAPI_VERSION, instance)), None, body)
812

  
813
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
814
    """Grows a disk of an instance.
815

  
816
    More details for parameters can be found in the RAPI documentation.
817

  
818
    @type instance: string
819
    @param instance: Instance name
820
    @type disk: integer
821
    @param disk: Disk index
822
    @type amount: integer
823
    @param amount: Grow disk by this amount (MiB)
824
    @type wait_for_sync: bool
825
    @param wait_for_sync: Wait for disk to synchronize
826
    @rtype: string
827
    @return: job id
828

  
829
    """
830
    body = {
831
      "amount": amount,
832
      }
833

  
834
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
835

  
836
    return self._SendRequest(HTTP_POST,
837
                             ("/%s/instances/%s/disk/%s/grow" %
838
                              (GANETI_RAPI_VERSION, instance, disk)),
839
                             None, body)
840

  
841
  def GetInstanceTags(self, instance):
842
    """Gets tags for an instance.
843

  
844
    @type instance: str
845
    @param instance: instance whose tags to return
846

  
847
    @rtype: list of str
848
    @return: tags for the instance
849

  
850
    """
851
    return self._SendRequest(HTTP_GET,
852
                             ("/%s/instances/%s/tags" %
853
                              (GANETI_RAPI_VERSION, instance)), None, None)
854

  
855
  def AddInstanceTags(self, instance, tags, dry_run=False):
856
    """Adds tags to an instance.
857

  
858
    @type instance: str
859
    @param instance: instance to add tags to
860
    @type tags: list of str
861
    @param tags: tags to add to the instance
862
    @type dry_run: bool
863
    @param dry_run: whether to perform a dry run
864

  
865
    @rtype: string
866
    @return: job id
867

  
868
    """
869
    query = [("tag", t) for t in tags]
870
    _AppendDryRunIf(query, dry_run)
871

  
872
    return self._SendRequest(HTTP_PUT,
873
                             ("/%s/instances/%s/tags" %
874
                              (GANETI_RAPI_VERSION, instance)), query, None)
875

  
876
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
877
    """Deletes tags from an instance.
878

  
879
    @type instance: str
880
    @param instance: instance to delete tags from
881
    @type tags: list of str
882
    @param tags: tags to delete
883
    @type dry_run: bool
884
    @param dry_run: whether to perform a dry run
885
    @rtype: string
886
    @return: job id
887

  
888
    """
889
    query = [("tag", t) for t in tags]
890
    _AppendDryRunIf(query, dry_run)
891

  
892
    return self._SendRequest(HTTP_DELETE,
893
                             ("/%s/instances/%s/tags" %
894
                              (GANETI_RAPI_VERSION, instance)), query, None)
895

  
896
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
897
                     dry_run=False):
898
    """Reboots an instance.
899

  
900
    @type instance: str
901
    @param instance: instance to rebot
902
    @type reboot_type: str
903
    @param reboot_type: one of: hard, soft, full
904
    @type ignore_secondaries: bool
905
    @param ignore_secondaries: if True, ignores errors for the secondary node
906
        while re-assembling disks (in hard-reboot mode only)
907
    @type dry_run: bool
908
    @param dry_run: whether to perform a dry run
909
    @rtype: string
910
    @return: job id
911

  
912
    """
913
    query = []
914
    _AppendDryRunIf(query, dry_run)
915
    _AppendIf(query, reboot_type, ("type", reboot_type))
916
    _AppendIf(query, ignore_secondaries is not None,
917
              ("ignore_secondaries", ignore_secondaries))
918

  
919
    return self._SendRequest(HTTP_POST,
920
                             ("/%s/instances/%s/reboot" %
921
                              (GANETI_RAPI_VERSION, instance)), query, None)
922

  
923
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
924
    """Shuts down an instance.
925

  
926
    @type instance: str
927
    @param instance: the instance to shut down
928
    @type dry_run: bool
929
    @param dry_run: whether to perform a dry run
930
    @type no_remember: bool
931
    @param no_remember: if true, will not record the state change
932
    @rtype: string
933
    @return: job id
934

  
935
    """
936
    query = []
937
    _AppendDryRunIf(query, dry_run)
938
    _AppendIf(query, no_remember, ("no-remember", 1))
939

  
940
    return self._SendRequest(HTTP_PUT,
941
                             ("/%s/instances/%s/shutdown" %
942
                              (GANETI_RAPI_VERSION, instance)), query, None)
943

  
944
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
945
    """Starts up an instance.
946

  
947
    @type instance: str
948
    @param instance: the instance to start up
949
    @type dry_run: bool
950
    @param dry_run: whether to perform a dry run
951
    @type no_remember: bool
952
    @param no_remember: if true, will not record the state change
953
    @rtype: string
954
    @return: job id
955

  
956
    """
957
    query = []
958
    _AppendDryRunIf(query, dry_run)
959
    _AppendIf(query, no_remember, ("no-remember", 1))
960

  
961
    return self._SendRequest(HTTP_PUT,
962
                             ("/%s/instances/%s/startup" %
963
                              (GANETI_RAPI_VERSION, instance)), query, None)
964

  
965
  def ReinstallInstance(self, instance, os=None, no_startup=False,
966
                        osparams=None):
967
    """Reinstalls an instance.
968

  
969
    @type instance: str
970
    @param instance: The instance to reinstall
971
    @type os: str or None
972
    @param os: The operating system to reinstall. If None, the instance's
973
        current operating system will be installed again
974
    @type no_startup: bool
975
    @param no_startup: Whether to start the instance automatically
976
    @rtype: string
977
    @return: job id
978

  
979
    """
980
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
981
      body = {
982
        "start": not no_startup,
983
        }
984
      _SetItemIf(body, os is not None, "os", os)
985
      _SetItemIf(body, osparams is not None, "osparams", osparams)
986
      return self._SendRequest(HTTP_POST,
987
                               ("/%s/instances/%s/reinstall" %
988
                                (GANETI_RAPI_VERSION, instance)), None, body)
989

  
990
    # Use old request format
991
    if osparams:
992
      raise GanetiApiError("Server does not support specifying OS parameters"
993
                           " for instance reinstallation")
994

  
995
    query = []
996
    _AppendIf(query, os, ("os", os))
997
    _AppendIf(query, no_startup, ("nostartup", 1))
998

  
999
    return self._SendRequest(HTTP_POST,
1000
                             ("/%s/instances/%s/reinstall" %
1001
                              (GANETI_RAPI_VERSION, instance)), query, None)
1002

  
1003
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1004
                           remote_node=None, iallocator=None):
1005
    """Replaces disks on an instance.
1006

  
1007
    @type instance: str
1008
    @param instance: instance whose disks to replace
1009
    @type disks: list of ints
1010
    @param disks: Indexes of disks to replace
1011
    @type mode: str
1012
    @param mode: replacement mode to use (defaults to replace_auto)
1013
    @type remote_node: str or None
1014
    @param remote_node: new secondary node to use (for use with
1015
        replace_new_secondary mode)
1016
    @type iallocator: str or None
1017
    @param iallocator: instance allocator plugin to use (for use with
1018
                       replace_auto mode)
1019

  
1020
    @rtype: string
1021
    @return: job id
1022

  
1023
    """
1024
    query = [
1025
      ("mode", mode),
1026
      ]
1027

  
1028
    # TODO: Convert to body parameters
1029

  
1030
    if disks is not None:
1031
      _AppendIf(query, True,
1032
                ("disks", ",".join(str(idx) for idx in disks)))
1033

  
1034
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1035
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1036

  
1037
    return self._SendRequest(HTTP_POST,
1038
                             ("/%s/instances/%s/replace-disks" %
1039
                              (GANETI_RAPI_VERSION, instance)), query, None)
1040

  
1041
  def PrepareExport(self, instance, mode):
1042
    """Prepares an instance for an export.
1043

  
1044
    @type instance: string
1045
    @param instance: Instance name
1046
    @type mode: string
1047
    @param mode: Export mode
1048
    @rtype: string
1049
    @return: Job ID
1050

  
1051
    """
1052
    query = [("mode", mode)]
1053
    return self._SendRequest(HTTP_PUT,
1054
                             ("/%s/instances/%s/prepare-export" %
1055
                              (GANETI_RAPI_VERSION, instance)), query, None)
1056

  
1057
  def ExportInstance(self, instance, mode, destination, shutdown=None,
1058
                     remove_instance=None,
1059
                     x509_key_name=None, destination_x509_ca=None):
1060
    """Exports an instance.
1061

  
1062
    @type instance: string
1063
    @param instance: Instance name
1064
    @type mode: string
1065
    @param mode: Export mode
1066
    @rtype: string
1067
    @return: Job ID
1068

  
1069
    """
1070
    body = {
1071
      "destination": destination,
1072
      "mode": mode,
1073
      }
1074

  
1075
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1076
    _SetItemIf(body, remove_instance is not None,
1077
               "remove_instance", remove_instance)
1078
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1079
    _SetItemIf(body, destination_x509_ca is not None,
1080
               "destination_x509_ca", destination_x509_ca)
1081

  
1082
    return self._SendRequest(HTTP_PUT,
1083
                             ("/%s/instances/%s/export" %
1084
                              (GANETI_RAPI_VERSION, instance)), None, body)
1085

  
1086
  def MigrateInstance(self, instance, mode=None, cleanup=None):
1087
    """Migrates an instance.
1088

  
1089
    @type instance: string
1090
    @param instance: Instance name
1091
    @type mode: string
1092
    @param mode: Migration mode
1093
    @type cleanup: bool
1094
    @param cleanup: Whether to clean up a previously failed migration
1095
    @rtype: string
1096
    @return: job id
1097

  
1098
    """
1099
    body = {}
1100
    _SetItemIf(body, mode is not None, "mode", mode)
1101
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1102

  
1103
    return self._SendRequest(HTTP_PUT,
1104
                             ("/%s/instances/%s/migrate" %
1105
                              (GANETI_RAPI_VERSION, instance)), None, body)
1106

  
1107
  def FailoverInstance(self, instance, iallocator=None,
1108
                       ignore_consistency=None, target_node=None):
1109
    """Does a failover of an instance.
1110

  
1111
    @type instance: string
1112
    @param instance: Instance name
1113
    @type iallocator: string
1114
    @param iallocator: Iallocator for deciding the target node for
1115
      shared-storage instances
1116
    @type ignore_consistency: bool
1117
    @param ignore_consistency: Whether to ignore disk consistency
1118
    @type target_node: string
1119
    @param target_node: Target node for shared-storage instances
1120
    @rtype: string
1121
    @return: job id
1122

  
1123
    """
1124
    body = {}
1125
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1126
    _SetItemIf(body, ignore_consistency is not None,
1127
               "ignore_consistency", ignore_consistency)
1128
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1129

  
1130
    return self._SendRequest(HTTP_PUT,
1131
                             ("/%s/instances/%s/failover" %
1132
                              (GANETI_RAPI_VERSION, instance)), None, body)
1133

  
1134
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1135
    """Changes the name of an instance.
1136

  
1137
    @type instance: string
1138
    @param instance: Instance name
1139
    @type new_name: string
1140
    @param new_name: New instance name
1141
    @type ip_check: bool
1142
    @param ip_check: Whether to ensure instance's IP address is inactive
1143
    @type name_check: bool
1144
    @param name_check: Whether to ensure instance's name is resolvable
1145
    @rtype: string
1146
    @return: job id
1147

  
1148
    """
1149
    body = {
1150
      "new_name": new_name,
1151
      }
1152

  
1153
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1154
    _SetItemIf(body, name_check is not None, "name_check", name_check)
1155

  
1156
    return self._SendRequest(HTTP_PUT,
1157
                             ("/%s/instances/%s/rename" %
1158
                              (GANETI_RAPI_VERSION, instance)), None, body)
1159

  
1160
  def GetInstanceConsole(self, instance):
1161
    """Request information for connecting to instance's console.
1162

  
1163
    @type instance: string
1164
    @param instance: Instance name
1165
    @rtype: dict
1166
    @return: dictionary containing information about instance's console
1167

  
1168
    """
1169
    return self._SendRequest(HTTP_GET,
1170
                             ("/%s/instances/%s/console" %
1171
                              (GANETI_RAPI_VERSION, instance)), None, None)
1172

  
1173
  def GetJobs(self):
1174
    """Gets all jobs for the cluster.
1175

  
1176
    @rtype: list of int
1177
    @return: job ids for the cluster
1178

  
1179
    """
1180
    return [int(j["id"])
1181
            for j in self._SendRequest(HTTP_GET,
1182
                                       "/%s/jobs" % GANETI_RAPI_VERSION,
1183
                                       None, None)]
1184

  
1185
  def GetJobStatus(self, job_id):
1186
    """Gets the status of a job.
1187

  
1188
    @type job_id: string
1189
    @param job_id: job id whose status to query
1190

  
1191
    @rtype: dict
1192
    @return: job status
1193

  
1194
    """
1195
    return self._SendRequest(HTTP_GET,
1196
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1197
                             None, None)
1198

  
1199
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1200
    """Polls cluster for job status until completion.
1201

  
1202
    Completion is defined as any of the following states listed in
1203
    L{JOB_STATUS_FINALIZED}.
1204

  
1205
    @type job_id: string
1206
    @param job_id: job id to watch
1207
    @type period: int
1208
    @param period: how often to poll for status (optional, default 5s)
1209
    @type retries: int
1210
    @param retries: how many time to poll before giving up
1211
                    (optional, default -1 means unlimited)
1212

  
1213
    @rtype: bool
1214
    @return: C{True} if job succeeded or C{False} if failed/status timeout
1215
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
1216
      possible; L{WaitForJobChange} returns immediately after a job changed and
1217
      does not use polling
1218

  
1219
    """
1220
    while retries != 0:
1221
      job_result = self.GetJobStatus(job_id)
1222

  
1223
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1224
        return True
1225
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1226
        return False
1227

  
1228
      if period:
1229
        time.sleep(period)
1230

  
1231
      if retries > 0:
1232
        retries -= 1
1233

  
1234
    return False
1235

  
1236
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1237
    """Waits for job changes.
1238

  
1239
    @type job_id: string
1240
    @param job_id: Job ID for which to wait
1241
    @return: C{None} if no changes have been detected and a dict with two keys,
1242
      C{job_info} and C{log_entries} otherwise.
1243
    @rtype: dict
1244

  
1245
    """
1246
    body = {
1247
      "fields": fields,
1248
      "previous_job_info": prev_job_info,
1249
      "previous_log_serial": prev_log_serial,
1250
      }
1251

  
1252
    return self._SendRequest(HTTP_GET,
1253
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1254
                             None, body)
1255

  
1256
  def CancelJob(self, job_id, dry_run=False):
1257
    """Cancels a job.
1258

  
1259
    @type job_id: string
1260
    @param job_id: id of the job to delete
1261
    @type dry_run: bool
1262
    @param dry_run: whether to perform a dry run
1263
    @rtype: tuple
1264
    @return: tuple containing the result, and a message (bool, string)
1265

  
1266
    """
1267
    query = []
1268
    _AppendDryRunIf(query, dry_run)
1269

  
1270
    return self._SendRequest(HTTP_DELETE,
1271
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1272
                             query, None)
1273

  
1274
  def GetNodes(self, bulk=False):
1275
    """Gets all nodes in the cluster.
1276

  
1277
    @type bulk: bool
1278
    @param bulk: whether to return all information about all instances
1279

  
1280
    @rtype: list of dict or str
1281
    @return: if bulk is true, info about nodes in the cluster,
1282
        else list of nodes in the cluster
1283

  
1284
    """
1285
    query = []
1286
    _AppendIf(query, bulk, ("bulk", 1))
1287

  
1288
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1289
                              query, None)
1290
    if bulk:
1291
      return nodes
1292
    else:
1293
      return [n["id"] for n in nodes]
1294

  
1295
  def GetNode(self, node):
1296
    """Gets information about a node.
1297

  
1298
    @type node: str
1299
    @param node: node whose info to return
1300

  
1301
    @rtype: dict
1302
    @return: info about the node
1303

  
1304
    """
1305
    return self._SendRequest(HTTP_GET,
1306
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1307
                             None, None)
1308

  
1309
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1310
                   dry_run=False, early_release=None,
1311
                   mode=None, accept_old=False):
1312
    """Evacuates instances from a Ganeti node.
1313

  
1314
    @type node: str
1315
    @param node: node to evacuate
1316
    @type iallocator: str or None
1317
    @param iallocator: instance allocator to use
1318
    @type remote_node: str
1319
    @param remote_node: node to evaucate to
1320
    @type dry_run: bool
1321
    @param dry_run: whether to perform a dry run
1322
    @type early_release: bool
1323
    @param early_release: whether to enable parallelization
1324
    @type mode: string
1325
    @param mode: Node evacuation mode
1326
    @type accept_old: bool
1327
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1328
        results
1329

  
1330
    @rtype: string, or a list for pre-2.5 results
1331
    @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1332
      list of (job ID, instance name, new secondary node); if dry_run was
1333
      specified, then the actual move jobs were not submitted and the job IDs
1334
      will be C{None}
1335

  
1336
    @raises GanetiApiError: if an iallocator and remote_node are both
1337
        specified
1338

  
1339
    """
1340
    if iallocator and remote_node:
1341
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1342

  
1343
    query = []
1344
    _AppendDryRunIf(query, dry_run)
1345

  
1346
    if _NODE_EVAC_RES1 in self.GetFeatures():
1347
      # Server supports body parameters
1348
      body = {}
1349

  
1350
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1351
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1352
      _SetItemIf(body, early_release is not None,
1353
                 "early_release", early_release)
1354
      _SetItemIf(body, mode is not None, "mode", mode)
1355
    else:
1356
      # Pre-2.5 request format
1357
      body = None
1358

  
1359
      if not accept_old:
1360
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1361
                             " not accept old-style results (parameter"
1362
                             " accept_old)")
1363

  
1364
      # Pre-2.5 servers can only evacuate secondaries
1365
      if mode is not None and mode != NODE_EVAC_SEC:
1366
        raise GanetiApiError("Server can only evacuate secondary instances")
1367

  
1368
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1369
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1370
      _AppendIf(query, early_release, ("early_release", 1))
1371

  
1372
    return self._SendRequest(HTTP_POST,
1373
                             ("/%s/nodes/%s/evacuate" %
1374
                              (GANETI_RAPI_VERSION, node)), query, body)
1375

  
1376
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1377
                  target_node=None):
1378
    """Migrates all primary instances from a node.
1379

  
1380
    @type node: str
1381
    @param node: node to migrate
1382
    @type mode: string
1383
    @param mode: if passed, it will overwrite the live migration type,
1384
        otherwise the hypervisor default will be used
1385
    @type dry_run: bool
1386
    @param dry_run: whether to perform a dry run
1387
    @type iallocator: string
1388
    @param iallocator: instance allocator to use
1389
    @type target_node: string
1390
    @param target_node: Target node for shared-storage instances
1391

  
1392
    @rtype: string
1393
    @return: job id
1394

  
1395
    """
1396
    query = []
1397
    _AppendDryRunIf(query, dry_run)
1398

  
1399
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1400
      body = {}
1401

  
1402
      _SetItemIf(body, mode is not None, "mode", mode)
1403
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1404
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1405

  
1406
      assert len(query) <= 1
1407

  
1408
      return self._SendRequest(HTTP_POST,
1409
                               ("/%s/nodes/%s/migrate" %
1410
                                (GANETI_RAPI_VERSION, node)), query, body)
1411
    else:
1412
      # Use old request format
1413
      if target_node is not None:
1414
        raise GanetiApiError("Server does not support specifying target node"
1415
                             " for node migration")
1416

  
1417
      _AppendIf(query, mode is not None, ("mode", mode))
1418

  
1419
      return self._SendRequest(HTTP_POST,
1420
                               ("/%s/nodes/%s/migrate" %
1421
                                (GANETI_RAPI_VERSION, node)), query, None)
1422

  
1423
  def GetNodeRole(self, node):
1424
    """Gets the current role for a node.
1425

  
1426
    @type node: str
1427
    @param node: node whose role to return
1428

  
1429
    @rtype: str
1430
    @return: the current role for a node
1431

  
1432
    """
1433
    return self._SendRequest(HTTP_GET,
1434
                             ("/%s/nodes/%s/role" %
1435
                              (GANETI_RAPI_VERSION, node)), None, None)
1436

  
1437
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1438
    """Sets the role for a node.
1439

  
1440
    @type node: str
1441
    @param node: the node whose role to set
1442
    @type role: str
1443
    @param role: the role to set for the node
1444
    @type force: bool
1445
    @param force: whether to force the role change
1446
    @type auto_promote: bool
1447
    @param auto_promote: Whether node(s) should be promoted to master candidate
1448
                         if necessary
1449

  
1450
    @rtype: string
1451
    @return: job id
1452

  
1453
    """
1454
    query = []
1455
    _AppendForceIf(query, force)
1456
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1457

  
1458
    return self._SendRequest(HTTP_PUT,
1459
                             ("/%s/nodes/%s/role" %
1460
                              (GANETI_RAPI_VERSION, node)), query, role)
1461

  
1462
  def PowercycleNode(self, node, force=False):
1463
    """Powercycles a node.
1464

  
1465
    @type node: string
1466
    @param node: Node name
1467
    @type force: bool
1468
    @param force: Whether to force the operation
1469
    @rtype: string
1470
    @return: job id
1471

  
1472
    """
1473
    query = []
1474
    _AppendForceIf(query, force)
1475

  
1476
    return self._SendRequest(HTTP_POST,
1477
                             ("/%s/nodes/%s/powercycle" %
1478
                              (GANETI_RAPI_VERSION, node)), query, None)
1479

  
1480
  def ModifyNode(self, node, **kwargs):
1481
    """Modifies a node.
1482

  
1483
    More details for parameters can be found in the RAPI documentation.
1484

  
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff