Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ aa922d64

History | View | Annotate | Download (62.7 kB)

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 reboot
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)