Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ 8140e24f

History | View | Annotate | Download (59.5 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
# Older pycURL versions don't have all error constants
118
try:
119
  _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
120
  _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
121
except AttributeError:
122
  _CURLE_SSL_CACERT = 60
123
  _CURLE_SSL_CACERT_BADFILE = 77
124

    
125
_CURL_SSL_CERT_ERRORS = frozenset([
126
  _CURLE_SSL_CACERT,
127
  _CURLE_SSL_CACERT_BADFILE,
128
  ])
129

    
130

    
131
class Error(Exception):
132
  """Base error class for this module.
133

134
  """
135
  pass
136

    
137

    
138
class GanetiApiError(Error):
139
  """Generic error raised from Ganeti API.
140

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

    
146

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

150
  """
151
  pass
152

    
153

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

157
  """
158
  if condition:
159
    container.append(value)
160

    
161
  return condition
162

    
163

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

167
  """
168
  return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
169

    
170

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

174
  """
175
  return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
176

    
177

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

181
  """
182
  if condition:
183
    container[item] = value
184

    
185
  return condition
186

    
187

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

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

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

    
205
  return wrapper
206

    
207

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

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

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

    
241
  def _ConfigCurl(curl, logger):
242
    """Configures a cURL object
243

244
    @type curl: pycurl.Curl
245
    @param curl: cURL object
246

247
    """
248
    logger.debug("Using cURL version %s", pycurl.version)
249

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

    
258
    lcsslver = sslver.lower()
259
    if lcsslver.startswith("openssl/"):
260
      pass
261
    elif lcsslver.startswith("nss/"):
262
      # TODO: investigate compatibility beyond a simple test
263
      pass
264
    elif lcsslver.startswith("gnutls/"):
265
      if capath:
266
        raise Error("cURL linked against GnuTLS has no support for a"
267
                    " CA path (%s)" % (pycurl.version, ))
268
    else:
269
      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
270
                                sslver)
271

    
272
    curl.setopt(pycurl.VERBOSE, verbose)
273
    curl.setopt(pycurl.NOSIGNAL, not use_signal)
274

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

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

    
298
    if proxy is not None:
299
      curl.setopt(pycurl.PROXY, str(proxy))
300

    
301
    # Timeouts
302
    if connect_timeout is not None:
303
      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
304
    if timeout is not None:
305
      curl.setopt(pycurl.TIMEOUT, timeout)
306

    
307
  return _ConfigCurl
308

    
309

    
310
class GanetiRapiClient(object): # pylint: disable=R0904
311
  """Ganeti RAPI client.
312

313
  """
314
  USER_AGENT = "Ganeti RAPI Client"
315
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
316

    
317
  def __init__(self, host, port=GANETI_RAPI_PORT,
318
               username=None, password=None, logger=logging,
319
               curl_config_fn=None, curl_factory=None):
320
    """Initializes this class.
321

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

334
    """
335
    self._username = username
336
    self._password = password
337
    self._logger = logger
338
    self._curl_config_fn = curl_config_fn
339
    self._curl_factory = curl_factory
340

    
341
    try:
342
      socket.inet_pton(socket.AF_INET6, host)
343
      address = "[%s]:%s" % (host, port)
344
    except socket.error:
345
      address = "%s:%s" % (host, port)
346

    
347
    self._base_url = "https://%s" % address
348

    
349
    if username is not None:
350
      if password is None:
351
        raise Error("Password not specified")
352
    elif password:
353
      raise Error("Specified password without username")
354

    
355
  def _CreateCurl(self):
356
    """Creates a cURL object.
357

358
    """
359
    # Create pycURL object if no factory is provided
360
    if self._curl_factory:
361
      curl = self._curl_factory()
362
    else:
363
      curl = pycurl.Curl()
364

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

    
378
    assert ((self._username is None and self._password is None) ^
379
            (self._username is not None and self._password is not None))
380

    
381
    if self._username:
382
      # Setup authentication
383
      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
384
      curl.setopt(pycurl.USERPWD,
385
                  str("%s:%s" % (self._username, self._password)))
386

    
387
    # Call external configuration function
388
    if self._curl_config_fn:
389
      self._curl_config_fn(curl, self._logger)
390

    
391
    return curl
392

    
393
  @staticmethod
394
  def _EncodeQuery(query):
395
    """Encode query values for RAPI URL.
396

397
    @type query: list of two-tuples
398
    @param query: Query arguments
399
    @rtype: list
400
    @return: Query list with encoded values
401

402
    """
403
    result = []
404

    
405
    for name, value in query:
406
      if value is None:
407
        result.append((name, ""))
408

    
409
      elif isinstance(value, bool):
410
        # Boolean values must be encoded as 0 or 1
411
        result.append((name, int(value)))
412

    
413
      elif isinstance(value, (list, tuple, dict)):
414
        raise ValueError("Invalid query data type %r" % type(value).__name__)
415

    
416
      else:
417
        result.append((name, value))
418

    
419
    return result
420

    
421
  def _SendRequest(self, method, path, query, content):
422
    """Sends an HTTP request.
423

424
    This constructs a full URL, encodes and decodes HTTP bodies, and
425
    handles invalid responses in a pythonic way.
426

427
    @type method: string
428
    @param method: HTTP method to use
429
    @type path: string
430
    @param path: HTTP URL path
431
    @type query: list of two-tuples
432
    @param query: query arguments to pass to urllib.urlencode
433
    @type content: str or None
434
    @param content: HTTP body content
435

436
    @rtype: str
437
    @return: JSON-Decoded response
438

439
    @raises CertificateError: If an invalid SSL certificate is found
440
    @raises GanetiApiError: If an invalid response is returned
441

442
    """
443
    assert path.startswith("/")
444

    
445
    curl = self._CreateCurl()
446

    
447
    if content is not None:
448
      encoded_content = self._json_encoder.encode(content)
449
    else:
450
      encoded_content = ""
451

    
452
    # Build URL
453
    urlparts = [self._base_url, path]
454
    if query:
455
      urlparts.append("?")
456
      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
457

    
458
    url = "".join(urlparts)
459

    
460
    self._logger.debug("Sending request %s %s (content=%r)",
461
                       method, url, encoded_content)
462

    
463
    # Buffer for response
464
    encoded_resp_body = StringIO()
465

    
466
    # Configure cURL
467
    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
468
    curl.setopt(pycurl.URL, str(url))
469
    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
470
    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
471

    
472
    try:
473
      # Send request and wait for response
474
      try:
475
        curl.perform()
476
      except pycurl.error, err:
477
        if err.args[0] in _CURL_SSL_CERT_ERRORS:
478
          raise CertificateError("SSL certificate error %s" % err,
479
                                 code=err.args[0])
480

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

    
488
    # Get HTTP response code
489
    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
490

    
491
    # Was anything written to the response buffer?
492
    if encoded_resp_body.tell():
493
      response_content = simplejson.loads(encoded_resp_body.getvalue())
494
    else:
495
      response_content = None
496

    
497
    if http_code != HTTP_OK:
498
      if isinstance(response_content, dict):
499
        msg = ("%s %s: %s" %
500
               (response_content["code"],
501
                response_content["message"],
502
                response_content["explain"]))
503
      else:
504
        msg = str(response_content)
505

    
506
      raise GanetiApiError(msg, code=http_code)
507

    
508
    return response_content
509

    
510
  def GetVersion(self):
511
    """Gets the Remote API version running on the cluster.
512

513
    @rtype: int
514
    @return: Ganeti Remote API version
515

516
    """
517
    return self._SendRequest(HTTP_GET, "/version", None, None)
518

    
519
  def GetFeatures(self):
520
    """Gets the list of optional features supported by RAPI server.
521

522
    @rtype: list
523
    @return: List of optional features
524

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

    
534
      raise
535

    
536
  def GetOperatingSystems(self):
537
    """Gets the Operating Systems running in the Ganeti cluster.
538

539
    @rtype: list of str
540
    @return: operating systems
541

542
    """
543
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
544
                             None, None)
545

    
546
  def GetInfo(self):
547
    """Gets info about the cluster.
548

549
    @rtype: dict
550
    @return: information about the cluster
551

552
    """
553
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
554
                             None, None)
555

    
556
  def RedistributeConfig(self):
557
    """Tells the cluster to redistribute its configuration files.
558

559
    @rtype: string
560
    @return: job id
561

562
    """
563
    return self._SendRequest(HTTP_PUT,
564
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
565
                             None, None)
566

    
567
  def ModifyCluster(self, **kwargs):
568
    """Modifies cluster parameters.
569

570
    More details for parameters can be found in the RAPI documentation.
571

572
    @rtype: string
573
    @return: job id
574

575
    """
576
    body = kwargs
577

    
578
    return self._SendRequest(HTTP_PUT,
579
                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
580

    
581
  def GetClusterTags(self):
582
    """Gets the cluster tags.
583

584
    @rtype: list of str
585
    @return: cluster tags
586

587
    """
588
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
589
                             None, None)
590

    
591
  def AddClusterTags(self, tags, dry_run=False):
592
    """Adds tags to the cluster.
593

594
    @type tags: list of str
595
    @param tags: tags to add to the cluster
596
    @type dry_run: bool
597
    @param dry_run: whether to perform a dry run
598

599
    @rtype: string
600
    @return: job id
601

602
    """
603
    query = [("tag", t) for t in tags]
604
    _AppendDryRunIf(query, dry_run)
605

    
606
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
607
                             query, None)
608

    
609
  def DeleteClusterTags(self, tags, dry_run=False):
610
    """Deletes tags from the cluster.
611

612
    @type tags: list of str
613
    @param tags: tags to delete
614
    @type dry_run: bool
615
    @param dry_run: whether to perform a dry run
616
    @rtype: string
617
    @return: job id
618

619
    """
620
    query = [("tag", t) for t in tags]
621
    _AppendDryRunIf(query, dry_run)
622

    
623
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
624
                             query, None)
625

    
626
  def GetInstances(self, bulk=False):
627
    """Gets information about instances on the cluster.
628

629
    @type bulk: bool
630
    @param bulk: whether to return all information about all instances
631

632
    @rtype: list of dict or list of str
633
    @return: if bulk is True, info about the instances, else a list of instances
634

635
    """
636
    query = []
637
    _AppendIf(query, bulk, ("bulk", 1))
638

    
639
    instances = self._SendRequest(HTTP_GET,
640
                                  "/%s/instances" % GANETI_RAPI_VERSION,
641
                                  query, None)
642
    if bulk:
643
      return instances
644
    else:
645
      return [i["id"] for i in instances]
646

    
647
  def GetInstance(self, instance):
648
    """Gets information about an instance.
649

650
    @type instance: str
651
    @param instance: instance whose info to return
652

653
    @rtype: dict
654
    @return: info about the instance
655

656
    """
657
    return self._SendRequest(HTTP_GET,
658
                             ("/%s/instances/%s" %
659
                              (GANETI_RAPI_VERSION, instance)), None, None)
660

    
661
  def GetInstanceInfo(self, instance, static=None):
662
    """Gets information about an instance.
663

664
    @type instance: string
665
    @param instance: Instance name
666
    @rtype: string
667
    @return: Job ID
668

669
    """
670
    if static is not None:
671
      query = [("static", static)]
672
    else:
673
      query = None
674

    
675
    return self._SendRequest(HTTP_GET,
676
                             ("/%s/instances/%s/info" %
677
                              (GANETI_RAPI_VERSION, instance)), query, None)
678

    
679
  @staticmethod
680
  def _UpdateWithKwargs(base, **kwargs):
681
    """Updates the base with params from kwargs.
682

683
    @param base: The base dict, filled with required fields
684

685
    @note: This is an inplace update of base
686

687
    """
688
    conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
689
    if conflicts:
690
      raise GanetiApiError("Required fields can not be specified as"
691
                           " keywords: %s" % ", ".join(conflicts))
692

    
693
    base.update((key, value) for key, value in kwargs.iteritems()
694
                if key != "dry_run")
695

    
696
  def InstanceAllocation(self, mode, name, disk_template, disks, nics,
697
                         **kwargs):
698
    """Generates an instance allocation as used by multiallocate.
699

700
    More details for parameters can be found in the RAPI documentation.
701
    It is the same as used by CreateInstance.
702

703
    @type mode: string
704
    @param mode: Instance creation mode
705
    @type name: string
706
    @param name: Hostname of the instance to create
707
    @type disk_template: string
708
    @param disk_template: Disk template for instance (e.g. plain, diskless,
709
                          file, or drbd)
710
    @type disks: list of dicts
711
    @param disks: List of disk definitions
712
    @type nics: list of dicts
713
    @param nics: List of NIC definitions
714

715
    @return: A dict with the generated entry
716

717
    """
718
    # All required fields for request data version 1
719
    alloc = {
720
      "mode": mode,
721
      "name": name,
722
      "disk_template": disk_template,
723
      "disks": disks,
724
      "nics": nics,
725
      }
726

    
727
    self._UpdateWithKwargs(alloc, **kwargs)
728

    
729
    return alloc
730

    
731
  def InstancesMultiAlloc(self, instances, **kwargs):
732
    """Tries to allocate multiple instances.
733

734
    More details for parameters can be found in the RAPI documentation.
735

736
    @param instances: A list of L{InstanceAllocation} results
737

738
    """
739
    query = []
740
    body = {
741
      "instances": instances,
742
      }
743
    self._UpdateWithKwargs(body, **kwargs)
744

    
745
    _AppendDryRunIf(query, kwargs.get("dry_run"))
746

    
747
    return self._SendRequest(HTTP_POST,
748
                             "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION,
749
                             query, body)
750

    
751
  def CreateInstance(self, mode, name, disk_template, disks, nics,
752
                     **kwargs):
753
    """Creates a new instance.
754

755
    More details for parameters can be found in the RAPI documentation.
756

757
    @type mode: string
758
    @param mode: Instance creation mode
759
    @type name: string
760
    @param name: Hostname of the instance to create
761
    @type disk_template: string
762
    @param disk_template: Disk template for instance (e.g. plain, diskless,
763
                          file, or drbd)
764
    @type disks: list of dicts
765
    @param disks: List of disk definitions
766
    @type nics: list of dicts
767
    @param nics: List of NIC definitions
768
    @type dry_run: bool
769
    @keyword dry_run: whether to perform a dry run
770

771
    @rtype: string
772
    @return: job id
773

774
    """
775
    query = []
776

    
777
    _AppendDryRunIf(query, kwargs.get("dry_run"))
778

    
779
    if _INST_CREATE_REQV1 in self.GetFeatures():
780
      body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
781
                                     **kwargs)
782
      body[_REQ_DATA_VERSION_FIELD] = 1
783
    else:
784
      raise GanetiApiError("Server does not support new-style (version 1)"
785
                           " instance creation requests")
786

    
787
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
788
                             query, body)
789

    
790
  def DeleteInstance(self, instance, dry_run=False):
791
    """Deletes an instance.
792

793
    @type instance: str
794
    @param instance: the instance to delete
795

796
    @rtype: string
797
    @return: job id
798

799
    """
800
    query = []
801
    _AppendDryRunIf(query, dry_run)
802

    
803
    return self._SendRequest(HTTP_DELETE,
804
                             ("/%s/instances/%s" %
805
                              (GANETI_RAPI_VERSION, instance)), query, None)
806

    
807
  def ModifyInstance(self, instance, **kwargs):
808
    """Modifies an instance.
809

810
    More details for parameters can be found in the RAPI documentation.
811

812
    @type instance: string
813
    @param instance: Instance name
814
    @rtype: string
815
    @return: job id
816

817
    """
818
    body = kwargs
819

    
820
    return self._SendRequest(HTTP_PUT,
821
                             ("/%s/instances/%s/modify" %
822
                              (GANETI_RAPI_VERSION, instance)), None, body)
823

    
824
  def ActivateInstanceDisks(self, instance, ignore_size=None):
825
    """Activates an instance's disks.
826

827
    @type instance: string
828
    @param instance: Instance name
829
    @type ignore_size: bool
830
    @param ignore_size: Whether to ignore recorded size
831
    @rtype: string
832
    @return: job id
833

834
    """
835
    query = []
836
    _AppendIf(query, ignore_size, ("ignore_size", 1))
837

    
838
    return self._SendRequest(HTTP_PUT,
839
                             ("/%s/instances/%s/activate-disks" %
840
                              (GANETI_RAPI_VERSION, instance)), query, None)
841

    
842
  def DeactivateInstanceDisks(self, instance):
843
    """Deactivates an instance's disks.
844

845
    @type instance: string
846
    @param instance: Instance name
847
    @rtype: string
848
    @return: job id
849

850
    """
851
    return self._SendRequest(HTTP_PUT,
852
                             ("/%s/instances/%s/deactivate-disks" %
853
                              (GANETI_RAPI_VERSION, instance)), None, None)
854

    
855
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
856
    """Recreate an instance's disks.
857

858
    @type instance: string
859
    @param instance: Instance name
860
    @type disks: list of int
861
    @param disks: List of disk indexes
862
    @type nodes: list of string
863
    @param nodes: New instance nodes, if relocation is desired
864
    @rtype: string
865
    @return: job id
866

867
    """
868
    body = {}
869
    _SetItemIf(body, disks is not None, "disks", disks)
870
    _SetItemIf(body, nodes is not None, "nodes", nodes)
871

    
872
    return self._SendRequest(HTTP_POST,
873
                             ("/%s/instances/%s/recreate-disks" %
874
                              (GANETI_RAPI_VERSION, instance)), None, body)
875

    
876
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
877
    """Grows a disk of an instance.
878

879
    More details for parameters can be found in the RAPI documentation.
880

881
    @type instance: string
882
    @param instance: Instance name
883
    @type disk: integer
884
    @param disk: Disk index
885
    @type amount: integer
886
    @param amount: Grow disk by this amount (MiB)
887
    @type wait_for_sync: bool
888
    @param wait_for_sync: Wait for disk to synchronize
889
    @rtype: string
890
    @return: job id
891

892
    """
893
    body = {
894
      "amount": amount,
895
      }
896

    
897
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
898

    
899
    return self._SendRequest(HTTP_POST,
900
                             ("/%s/instances/%s/disk/%s/grow" %
901
                              (GANETI_RAPI_VERSION, instance, disk)),
902
                             None, body)
903

    
904
  def GetInstanceTags(self, instance):
905
    """Gets tags for an instance.
906

907
    @type instance: str
908
    @param instance: instance whose tags to return
909

910
    @rtype: list of str
911
    @return: tags for the instance
912

913
    """
914
    return self._SendRequest(HTTP_GET,
915
                             ("/%s/instances/%s/tags" %
916
                              (GANETI_RAPI_VERSION, instance)), None, None)
917

    
918
  def AddInstanceTags(self, instance, tags, dry_run=False):
919
    """Adds tags to an instance.
920

921
    @type instance: str
922
    @param instance: instance to add tags to
923
    @type tags: list of str
924
    @param tags: tags to add to the instance
925
    @type dry_run: bool
926
    @param dry_run: whether to perform a dry run
927

928
    @rtype: string
929
    @return: job id
930

931
    """
932
    query = [("tag", t) for t in tags]
933
    _AppendDryRunIf(query, dry_run)
934

    
935
    return self._SendRequest(HTTP_PUT,
936
                             ("/%s/instances/%s/tags" %
937
                              (GANETI_RAPI_VERSION, instance)), query, None)
938

    
939
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
940
    """Deletes tags from an instance.
941

942
    @type instance: str
943
    @param instance: instance to delete tags from
944
    @type tags: list of str
945
    @param tags: tags to delete
946
    @type dry_run: bool
947
    @param dry_run: whether to perform a dry run
948
    @rtype: string
949
    @return: job id
950

951
    """
952
    query = [("tag", t) for t in tags]
953
    _AppendDryRunIf(query, dry_run)
954

    
955
    return self._SendRequest(HTTP_DELETE,
956
                             ("/%s/instances/%s/tags" %
957
                              (GANETI_RAPI_VERSION, instance)), query, None)
958

    
959
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
960
                     dry_run=False):
961
    """Reboots an instance.
962

963
    @type instance: str
964
    @param instance: instance to rebot
965
    @type reboot_type: str
966
    @param reboot_type: one of: hard, soft, full
967
    @type ignore_secondaries: bool
968
    @param ignore_secondaries: if True, ignores errors for the secondary node
969
        while re-assembling disks (in hard-reboot mode only)
970
    @type dry_run: bool
971
    @param dry_run: whether to perform a dry run
972
    @rtype: string
973
    @return: job id
974

975
    """
976
    query = []
977
    _AppendDryRunIf(query, dry_run)
978
    _AppendIf(query, reboot_type, ("type", reboot_type))
979
    _AppendIf(query, ignore_secondaries is not None,
980
              ("ignore_secondaries", ignore_secondaries))
981

    
982
    return self._SendRequest(HTTP_POST,
983
                             ("/%s/instances/%s/reboot" %
984
                              (GANETI_RAPI_VERSION, instance)), query, None)
985

    
986
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
987
                       **kwargs):
988
    """Shuts down an instance.
989

990
    @type instance: str
991
    @param instance: the instance to shut down
992
    @type dry_run: bool
993
    @param dry_run: whether to perform a dry run
994
    @type no_remember: bool
995
    @param no_remember: if true, will not record the state change
996
    @rtype: string
997
    @return: job id
998

999
    """
1000
    query = []
1001
    body = kwargs
1002

    
1003
    _AppendDryRunIf(query, dry_run)
1004
    _AppendIf(query, no_remember, ("no-remember", 1))
1005

    
1006
    return self._SendRequest(HTTP_PUT,
1007
                             ("/%s/instances/%s/shutdown" %
1008
                              (GANETI_RAPI_VERSION, instance)), query, body)
1009

    
1010
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
1011
    """Starts up an instance.
1012

1013
    @type instance: str
1014
    @param instance: the instance to start up
1015
    @type dry_run: bool
1016
    @param dry_run: whether to perform a dry run
1017
    @type no_remember: bool
1018
    @param no_remember: if true, will not record the state change
1019
    @rtype: string
1020
    @return: job id
1021

1022
    """
1023
    query = []
1024
    _AppendDryRunIf(query, dry_run)
1025
    _AppendIf(query, no_remember, ("no-remember", 1))
1026

    
1027
    return self._SendRequest(HTTP_PUT,
1028
                             ("/%s/instances/%s/startup" %
1029
                              (GANETI_RAPI_VERSION, instance)), query, None)
1030

    
1031
  def ReinstallInstance(self, instance, os=None, no_startup=False,
1032
                        osparams=None):
1033
    """Reinstalls an instance.
1034

1035
    @type instance: str
1036
    @param instance: The instance to reinstall
1037
    @type os: str or None
1038
    @param os: The operating system to reinstall. If None, the instance's
1039
        current operating system will be installed again
1040
    @type no_startup: bool
1041
    @param no_startup: Whether to start the instance automatically
1042
    @rtype: string
1043
    @return: job id
1044

1045
    """
1046
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
1047
      body = {
1048
        "start": not no_startup,
1049
        }
1050
      _SetItemIf(body, os is not None, "os", os)
1051
      _SetItemIf(body, osparams is not None, "osparams", osparams)
1052
      return self._SendRequest(HTTP_POST,
1053
                               ("/%s/instances/%s/reinstall" %
1054
                                (GANETI_RAPI_VERSION, instance)), None, body)
1055

    
1056
    # Use old request format
1057
    if osparams:
1058
      raise GanetiApiError("Server does not support specifying OS parameters"
1059
                           " for instance reinstallation")
1060

    
1061
    query = []
1062
    _AppendIf(query, os, ("os", os))
1063
    _AppendIf(query, no_startup, ("nostartup", 1))
1064

    
1065
    return self._SendRequest(HTTP_POST,
1066
                             ("/%s/instances/%s/reinstall" %
1067
                              (GANETI_RAPI_VERSION, instance)), query, None)
1068

    
1069
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1070
                           remote_node=None, iallocator=None):
1071
    """Replaces disks on an instance.
1072

1073
    @type instance: str
1074
    @param instance: instance whose disks to replace
1075
    @type disks: list of ints
1076
    @param disks: Indexes of disks to replace
1077
    @type mode: str
1078
    @param mode: replacement mode to use (defaults to replace_auto)
1079
    @type remote_node: str or None
1080
    @param remote_node: new secondary node to use (for use with
1081
        replace_new_secondary mode)
1082
    @type iallocator: str or None
1083
    @param iallocator: instance allocator plugin to use (for use with
1084
                       replace_auto mode)
1085

1086
    @rtype: string
1087
    @return: job id
1088

1089
    """
1090
    query = [
1091
      ("mode", mode),
1092
      ]
1093

    
1094
    # TODO: Convert to body parameters
1095

    
1096
    if disks is not None:
1097
      _AppendIf(query, True,
1098
                ("disks", ",".join(str(idx) for idx in disks)))
1099

    
1100
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1101
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1102

    
1103
    return self._SendRequest(HTTP_POST,
1104
                             ("/%s/instances/%s/replace-disks" %
1105
                              (GANETI_RAPI_VERSION, instance)), query, None)
1106

    
1107
  def PrepareExport(self, instance, mode):
1108
    """Prepares an instance for an export.
1109

1110
    @type instance: string
1111
    @param instance: Instance name
1112
    @type mode: string
1113
    @param mode: Export mode
1114
    @rtype: string
1115
    @return: Job ID
1116

1117
    """
1118
    query = [("mode", mode)]
1119
    return self._SendRequest(HTTP_PUT,
1120
                             ("/%s/instances/%s/prepare-export" %
1121
                              (GANETI_RAPI_VERSION, instance)), query, None)
1122

    
1123
  def ExportInstance(self, instance, mode, destination, shutdown=None,
1124
                     remove_instance=None,
1125
                     x509_key_name=None, destination_x509_ca=None):
1126
    """Exports an instance.
1127

1128
    @type instance: string
1129
    @param instance: Instance name
1130
    @type mode: string
1131
    @param mode: Export mode
1132
    @rtype: string
1133
    @return: Job ID
1134

1135
    """
1136
    body = {
1137
      "destination": destination,
1138
      "mode": mode,
1139
      }
1140

    
1141
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1142
    _SetItemIf(body, remove_instance is not None,
1143
               "remove_instance", remove_instance)
1144
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1145
    _SetItemIf(body, destination_x509_ca is not None,
1146
               "destination_x509_ca", destination_x509_ca)
1147

    
1148
    return self._SendRequest(HTTP_PUT,
1149
                             ("/%s/instances/%s/export" %
1150
                              (GANETI_RAPI_VERSION, instance)), None, body)
1151

    
1152
  def MigrateInstance(self, instance, mode=None, cleanup=None):
1153
    """Migrates an instance.
1154

1155
    @type instance: string
1156
    @param instance: Instance name
1157
    @type mode: string
1158
    @param mode: Migration mode
1159
    @type cleanup: bool
1160
    @param cleanup: Whether to clean up a previously failed migration
1161
    @rtype: string
1162
    @return: job id
1163

1164
    """
1165
    body = {}
1166
    _SetItemIf(body, mode is not None, "mode", mode)
1167
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1168

    
1169
    return self._SendRequest(HTTP_PUT,
1170
                             ("/%s/instances/%s/migrate" %
1171
                              (GANETI_RAPI_VERSION, instance)), None, body)
1172

    
1173
  def FailoverInstance(self, instance, iallocator=None,
1174
                       ignore_consistency=None, target_node=None):
1175
    """Does a failover of an instance.
1176

1177
    @type instance: string
1178
    @param instance: Instance name
1179
    @type iallocator: string
1180
    @param iallocator: Iallocator for deciding the target node for
1181
      shared-storage instances
1182
    @type ignore_consistency: bool
1183
    @param ignore_consistency: Whether to ignore disk consistency
1184
    @type target_node: string
1185
    @param target_node: Target node for shared-storage instances
1186
    @rtype: string
1187
    @return: job id
1188

1189
    """
1190
    body = {}
1191
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1192
    _SetItemIf(body, ignore_consistency is not None,
1193
               "ignore_consistency", ignore_consistency)
1194
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1195

    
1196
    return self._SendRequest(HTTP_PUT,
1197
                             ("/%s/instances/%s/failover" %
1198
                              (GANETI_RAPI_VERSION, instance)), None, body)
1199

    
1200
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1201
    """Changes the name of an instance.
1202

1203
    @type instance: string
1204
    @param instance: Instance name
1205
    @type new_name: string
1206
    @param new_name: New instance name
1207
    @type ip_check: bool
1208
    @param ip_check: Whether to ensure instance's IP address is inactive
1209
    @type name_check: bool
1210
    @param name_check: Whether to ensure instance's name is resolvable
1211
    @rtype: string
1212
    @return: job id
1213

1214
    """
1215
    body = {
1216
      "new_name": new_name,
1217
      }
1218

    
1219
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1220
    _SetItemIf(body, name_check is not None, "name_check", name_check)
1221

    
1222
    return self._SendRequest(HTTP_PUT,
1223
                             ("/%s/instances/%s/rename" %
1224
                              (GANETI_RAPI_VERSION, instance)), None, body)
1225

    
1226
  def GetInstanceConsole(self, instance):
1227
    """Request information for connecting to instance's console.
1228

1229
    @type instance: string
1230
    @param instance: Instance name
1231
    @rtype: dict
1232
    @return: dictionary containing information about instance's console
1233

1234
    """
1235
    return self._SendRequest(HTTP_GET,
1236
                             ("/%s/instances/%s/console" %
1237
                              (GANETI_RAPI_VERSION, instance)), None, None)
1238

    
1239
  def GetJobs(self):
1240
    """Gets all jobs for the cluster.
1241

1242
    @rtype: list of int
1243
    @return: job ids for the cluster
1244

1245
    """
1246
    return [int(j["id"])
1247
            for j in self._SendRequest(HTTP_GET,
1248
                                       "/%s/jobs" % GANETI_RAPI_VERSION,
1249
                                       None, None)]
1250

    
1251
  def GetJobStatus(self, job_id):
1252
    """Gets the status of a job.
1253

1254
    @type job_id: string
1255
    @param job_id: job id whose status to query
1256

1257
    @rtype: dict
1258
    @return: job status
1259

1260
    """
1261
    return self._SendRequest(HTTP_GET,
1262
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1263
                             None, None)
1264

    
1265
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1266
    """Polls cluster for job status until completion.
1267

1268
    Completion is defined as any of the following states listed in
1269
    L{JOB_STATUS_FINALIZED}.
1270

1271
    @type job_id: string
1272
    @param job_id: job id to watch
1273
    @type period: int
1274
    @param period: how often to poll for status (optional, default 5s)
1275
    @type retries: int
1276
    @param retries: how many time to poll before giving up
1277
                    (optional, default -1 means unlimited)
1278

1279
    @rtype: bool
1280
    @return: C{True} if job succeeded or C{False} if failed/status timeout
1281
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
1282
      possible; L{WaitForJobChange} returns immediately after a job changed and
1283
      does not use polling
1284

1285
    """
1286
    while retries != 0:
1287
      job_result = self.GetJobStatus(job_id)
1288

    
1289
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1290
        return True
1291
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1292
        return False
1293

    
1294
      if period:
1295
        time.sleep(period)
1296

    
1297
      if retries > 0:
1298
        retries -= 1
1299

    
1300
    return False
1301

    
1302
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1303
    """Waits for job changes.
1304

1305
    @type job_id: string
1306
    @param job_id: Job ID for which to wait
1307
    @return: C{None} if no changes have been detected and a dict with two keys,
1308
      C{job_info} and C{log_entries} otherwise.
1309
    @rtype: dict
1310

1311
    """
1312
    body = {
1313
      "fields": fields,
1314
      "previous_job_info": prev_job_info,
1315
      "previous_log_serial": prev_log_serial,
1316
      }
1317

    
1318
    return self._SendRequest(HTTP_GET,
1319
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1320
                             None, body)
1321

    
1322
  def CancelJob(self, job_id, dry_run=False):
1323
    """Cancels a job.
1324

1325
    @type job_id: string
1326
    @param job_id: id of the job to delete
1327
    @type dry_run: bool
1328
    @param dry_run: whether to perform a dry run
1329
    @rtype: tuple
1330
    @return: tuple containing the result, and a message (bool, string)
1331

1332
    """
1333
    query = []
1334
    _AppendDryRunIf(query, dry_run)
1335

    
1336
    return self._SendRequest(HTTP_DELETE,
1337
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1338
                             query, None)
1339

    
1340
  def GetNodes(self, bulk=False):
1341
    """Gets all nodes in the cluster.
1342

1343
    @type bulk: bool
1344
    @param bulk: whether to return all information about all instances
1345

1346
    @rtype: list of dict or str
1347
    @return: if bulk is true, info about nodes in the cluster,
1348
        else list of nodes in the cluster
1349

1350
    """
1351
    query = []
1352
    _AppendIf(query, bulk, ("bulk", 1))
1353

    
1354
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1355
                              query, None)
1356
    if bulk:
1357
      return nodes
1358
    else:
1359
      return [n["id"] for n in nodes]
1360

    
1361
  def GetNode(self, node):
1362
    """Gets information about a node.
1363

1364
    @type node: str
1365
    @param node: node whose info to return
1366

1367
    @rtype: dict
1368
    @return: info about the node
1369

1370
    """
1371
    return self._SendRequest(HTTP_GET,
1372
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1373
                             None, None)
1374

    
1375
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1376
                   dry_run=False, early_release=None,
1377
                   mode=None, accept_old=False):
1378
    """Evacuates instances from a Ganeti node.
1379

1380
    @type node: str
1381
    @param node: node to evacuate
1382
    @type iallocator: str or None
1383
    @param iallocator: instance allocator to use
1384
    @type remote_node: str
1385
    @param remote_node: node to evaucate to
1386
    @type dry_run: bool
1387
    @param dry_run: whether to perform a dry run
1388
    @type early_release: bool
1389
    @param early_release: whether to enable parallelization
1390
    @type mode: string
1391
    @param mode: Node evacuation mode
1392
    @type accept_old: bool
1393
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1394
        results
1395

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

1402
    @raises GanetiApiError: if an iallocator and remote_node are both
1403
        specified
1404

1405
    """
1406
    if iallocator and remote_node:
1407
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1408

    
1409
    query = []
1410
    _AppendDryRunIf(query, dry_run)
1411

    
1412
    if _NODE_EVAC_RES1 in self.GetFeatures():
1413
      # Server supports body parameters
1414
      body = {}
1415

    
1416
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1417
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1418
      _SetItemIf(body, early_release is not None,
1419
                 "early_release", early_release)
1420
      _SetItemIf(body, mode is not None, "mode", mode)
1421
    else:
1422
      # Pre-2.5 request format
1423
      body = None
1424

    
1425
      if not accept_old:
1426
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1427
                             " not accept old-style results (parameter"
1428
                             " accept_old)")
1429

    
1430
      # Pre-2.5 servers can only evacuate secondaries
1431
      if mode is not None and mode != NODE_EVAC_SEC:
1432
        raise GanetiApiError("Server can only evacuate secondary instances")
1433

    
1434
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1435
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1436
      _AppendIf(query, early_release, ("early_release", 1))
1437

    
1438
    return self._SendRequest(HTTP_POST,
1439
                             ("/%s/nodes/%s/evacuate" %
1440
                              (GANETI_RAPI_VERSION, node)), query, body)
1441

    
1442
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1443
                  target_node=None):
1444
    """Migrates all primary instances from a node.
1445

1446
    @type node: str
1447
    @param node: node to migrate
1448
    @type mode: string
1449
    @param mode: if passed, it will overwrite the live migration type,
1450
        otherwise the hypervisor default will be used
1451
    @type dry_run: bool
1452
    @param dry_run: whether to perform a dry run
1453
    @type iallocator: string
1454
    @param iallocator: instance allocator to use
1455
    @type target_node: string
1456
    @param target_node: Target node for shared-storage instances
1457

1458
    @rtype: string
1459
    @return: job id
1460

1461
    """
1462
    query = []
1463
    _AppendDryRunIf(query, dry_run)
1464

    
1465
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1466
      body = {}
1467

    
1468
      _SetItemIf(body, mode is not None, "mode", mode)
1469
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1470
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1471

    
1472
      assert len(query) <= 1
1473

    
1474
      return self._SendRequest(HTTP_POST,
1475
                               ("/%s/nodes/%s/migrate" %
1476
                                (GANETI_RAPI_VERSION, node)), query, body)
1477
    else:
1478
      # Use old request format
1479
      if target_node is not None:
1480
        raise GanetiApiError("Server does not support specifying target node"
1481
                             " for node migration")
1482

    
1483
      _AppendIf(query, mode is not None, ("mode", mode))
1484

    
1485
      return self._SendRequest(HTTP_POST,
1486
                               ("/%s/nodes/%s/migrate" %
1487
                                (GANETI_RAPI_VERSION, node)), query, None)
1488

    
1489
  def GetNodeRole(self, node):
1490
    """Gets the current role for a node.
1491

1492
    @type node: str
1493
    @param node: node whose role to return
1494

1495
    @rtype: str
1496
    @return: the current role for a node
1497

1498
    """
1499
    return self._SendRequest(HTTP_GET,
1500
                             ("/%s/nodes/%s/role" %
1501
                              (GANETI_RAPI_VERSION, node)), None, None)
1502

    
1503
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1504
    """Sets the role for a node.
1505

1506
    @type node: str
1507
    @param node: the node whose role to set
1508
    @type role: str
1509
    @param role: the role to set for the node
1510
    @type force: bool
1511
    @param force: whether to force the role change
1512
    @type auto_promote: bool
1513
    @param auto_promote: Whether node(s) should be promoted to master candidate
1514
                         if necessary
1515

1516
    @rtype: string
1517
    @return: job id
1518

1519
    """
1520
    query = []
1521
    _AppendForceIf(query, force)
1522
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1523

    
1524
    return self._SendRequest(HTTP_PUT,
1525
                             ("/%s/nodes/%s/role" %
1526
                              (GANETI_RAPI_VERSION, node)), query, role)
1527

    
1528
  def PowercycleNode(self, node, force=False):
1529
    """Powercycles a node.
1530

1531
    @type node: string
1532
    @param node: Node name
1533
    @type force: bool
1534
    @param force: Whether to force the operation
1535
    @rtype: string
1536
    @return: job id
1537

1538
    """
1539
    query = []
1540
    _AppendForceIf(query, force)
1541

    
1542
    return self._SendRequest(HTTP_POST,
1543
                             ("/%s/nodes/%s/powercycle" %
1544
                              (GANETI_RAPI_VERSION, node)), query, None)
1545

    
1546
  def ModifyNode(self, node, **kwargs):
1547
    """Modifies a node.
1548

1549
    More details for parameters can be found in the RAPI documentation.
1550

1551
    @type node: string
1552
    @param node: Node name
1553
    @rtype: string
1554
    @return: job id
1555

1556
    """
1557
    return self._SendRequest(HTTP_POST,
1558
                             ("/%s/nodes/%s/modify" %
1559
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1560

    
1561
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1562
    """Gets the storage units for a node.
1563

1564
    @type node: str
1565
    @param node: the node whose storage units to return
1566
    @type storage_type: str
1567
    @param storage_type: storage type whose units to return
1568
    @type output_fields: str
1569
    @param output_fields: storage type fields to return
1570

1571
    @rtype: string
1572
    @return: job id where results can be retrieved
1573

1574
    """
1575
    query = [
1576
      ("storage_type", storage_type),
1577
      ("output_fields", output_fields),
1578
      ]
1579

    
1580
    return self._SendRequest(HTTP_GET,
1581
                             ("/%s/nodes/%s/storage" %
1582
                              (GANETI_RAPI_VERSION, node)), query, None)
1583

    
1584
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1585
    """Modifies parameters of storage units on the node.
1586

1587
    @type node: str
1588
    @param node: node whose storage units to modify
1589
    @type storage_type: str
1590
    @param storage_type: storage type whose units to modify
1591
    @type name: str
1592
    @param name: name of the storage unit
1593
    @type allocatable: bool or None
1594
    @param allocatable: Whether to set the "allocatable" flag on the storage
1595
                        unit (None=no modification, True=set, False=unset)
1596

1597
    @rtype: string
1598
    @return: job id
1599

1600
    """
1601
    query = [
1602
      ("storage_type", storage_type),
1603
      ("name", name),
1604
      ]
1605

    
1606
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1607

    
1608
    return self._SendRequest(HTTP_PUT,
1609
                             ("/%s/nodes/%s/storage/modify" %
1610
                              (GANETI_RAPI_VERSION, node)), query, None)
1611

    
1612
  def RepairNodeStorageUnits(self, node, storage_type, name):
1613
    """Repairs a storage unit on the node.
1614

1615
    @type node: str
1616
    @param node: node whose storage units to repair
1617
    @type storage_type: str
1618
    @param storage_type: storage type to repair
1619
    @type name: str
1620
    @param name: name of the storage unit to repair
1621

1622
    @rtype: string
1623
    @return: job id
1624

1625
    """
1626
    query = [
1627
      ("storage_type", storage_type),
1628
      ("name", name),
1629
      ]
1630

    
1631
    return self._SendRequest(HTTP_PUT,
1632
                             ("/%s/nodes/%s/storage/repair" %
1633
                              (GANETI_RAPI_VERSION, node)), query, None)
1634

    
1635
  def GetNodeTags(self, node):
1636
    """Gets the tags for a node.
1637

1638
    @type node: str
1639
    @param node: node whose tags to return
1640

1641
    @rtype: list of str
1642
    @return: tags for the node
1643

1644
    """
1645
    return self._SendRequest(HTTP_GET,
1646
                             ("/%s/nodes/%s/tags" %
1647
                              (GANETI_RAPI_VERSION, node)), None, None)
1648

    
1649
  def AddNodeTags(self, node, tags, dry_run=False):
1650
    """Adds tags to a node.
1651

1652
    @type node: str
1653
    @param node: node to add tags to
1654
    @type tags: list of str
1655
    @param tags: tags to add to the node
1656
    @type dry_run: bool
1657
    @param dry_run: whether to perform a dry run
1658

1659
    @rtype: string
1660
    @return: job id
1661

1662
    """
1663
    query = [("tag", t) for t in tags]
1664
    _AppendDryRunIf(query, dry_run)
1665

    
1666
    return self._SendRequest(HTTP_PUT,
1667
                             ("/%s/nodes/%s/tags" %
1668
                              (GANETI_RAPI_VERSION, node)), query, tags)
1669

    
1670
  def DeleteNodeTags(self, node, tags, dry_run=False):
1671
    """Delete tags from a node.
1672

1673
    @type node: str
1674
    @param node: node to remove tags from
1675
    @type tags: list of str
1676
    @param tags: tags to remove from the node
1677
    @type dry_run: bool
1678
    @param dry_run: whether to perform a dry run
1679

1680
    @rtype: string
1681
    @return: job id
1682

1683
    """
1684
    query = [("tag", t) for t in tags]
1685
    _AppendDryRunIf(query, dry_run)
1686

    
1687
    return self._SendRequest(HTTP_DELETE,
1688
                             ("/%s/nodes/%s/tags" %
1689
                              (GANETI_RAPI_VERSION, node)), query, None)
1690

    
1691
  def GetNetworks(self, bulk=False):
1692
    """Gets all networks in the cluster.
1693

1694
    @type bulk: bool
1695
    @param bulk: whether to return all information about the networks
1696

1697
    @rtype: list of dict or str
1698
    @return: if bulk is true, a list of dictionaries with info about all
1699
        networks in the cluster, else a list of names of those networks
1700

1701
    """
1702
    query = []
1703
    _AppendIf(query, bulk, ("bulk", 1))
1704

    
1705
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1706
                               query, None)
1707
    if bulk:
1708
      return networks
1709
    else:
1710
      return [n["name"] for n in networks]
1711

    
1712
  def GetNetwork(self, network):
1713
    """Gets information about a network.
1714

1715
    @type group: str
1716
    @param group: name of the network whose info to return
1717

1718
    @rtype: dict
1719
    @return: info about the network
1720

1721
    """
1722
    return self._SendRequest(HTTP_GET,
1723
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1724
                             None, None)
1725

    
1726
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1727
                    gateway6=None, mac_prefix=None, network_type=None,
1728
                    add_reserved_ips=None, tags=None, dry_run=False):
1729
    """Creates a new network.
1730

1731
    @type name: str
1732
    @param name: the name of network to create
1733
    @type dry_run: bool
1734
    @param dry_run: whether to peform a dry run
1735

1736
    @rtype: string
1737
    @return: job id
1738

1739
    """
1740
    query = []
1741
    _AppendDryRunIf(query, dry_run)
1742

    
1743
    if add_reserved_ips:
1744
      add_reserved_ips = add_reserved_ips.split(',')
1745

    
1746
    if tags:
1747
      tags = tags.split(',')
1748

    
1749
    body = {
1750
      "network_name": network_name,
1751
      "gateway": gateway,
1752
      "network": network,
1753
      "gateway6": gateway6,
1754
      "network6": network6,
1755
      "mac_prefix": mac_prefix,
1756
      "network_type": network_type,
1757
      "add_reserved_ips": add_reserved_ips,
1758
      "tags": tags,
1759
      }
1760

    
1761
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1762
                             query, body)
1763

    
1764
  def ConnectNetwork(self, network_name, group_name, mode, link):
1765
    """Connects a Network to a NodeGroup with the given netparams
1766

1767
    """
1768
    body = {
1769
      "group_name": group_name,
1770
      "network_mode": mode,
1771
      "network_link": link
1772
      }
1773

    
1774
    return self._SendRequest(HTTP_PUT,
1775
                             ("/%s/networks/%s/connect" %
1776
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1777

    
1778
  def DisconnectNetwork(self, network_name, group_name):
1779
    """Connects a Network to a NodeGroup with the given netparams
1780

1781
    """
1782
    body = {
1783
      "group_name": group_name
1784
      }
1785
    return self._SendRequest(HTTP_PUT,
1786
                             ("/%s/networks/%s/disconnect" %
1787
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1788

    
1789

    
1790
  def DeleteNetwork(self, network, dry_run=False):
1791
    """Deletes a network.
1792

1793
    @type group: str
1794
    @param group: the network to delete
1795
    @type dry_run: bool
1796
    @param dry_run: whether to peform a dry run
1797

1798
    @rtype: string
1799
    @return: job id
1800

1801
    """
1802
    query = []
1803
    _AppendDryRunIf(query, dry_run)
1804

    
1805
    return self._SendRequest(HTTP_DELETE,
1806
                             ("/%s/networks/%s" %
1807
                              (GANETI_RAPI_VERSION, network)), query, None)
1808

    
1809
  def GetGroups(self, bulk=False):
1810
    """Gets all node groups in the cluster.
1811

1812
    @type bulk: bool
1813
    @param bulk: whether to return all information about the groups
1814

1815
    @rtype: list of dict or str
1816
    @return: if bulk is true, a list of dictionaries with info about all node
1817
        groups in the cluster, else a list of names of those node groups
1818

1819
    """
1820
    query = []
1821
    _AppendIf(query, bulk, ("bulk", 1))
1822

    
1823
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1824
                               query, None)
1825
    if bulk:
1826
      return groups
1827
    else:
1828
      return [g["name"] for g in groups]
1829

    
1830
  def GetGroup(self, group):
1831
    """Gets information about a node group.
1832

1833
    @type group: str
1834
    @param group: name of the node group whose info to return
1835

1836
    @rtype: dict
1837
    @return: info about the node group
1838

1839
    """
1840
    return self._SendRequest(HTTP_GET,
1841
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1842
                             None, None)
1843

    
1844
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1845
    """Creates a new node group.
1846

1847
    @type name: str
1848
    @param name: the name of node group to create
1849
    @type alloc_policy: str
1850
    @param alloc_policy: the desired allocation policy for the group, if any
1851
    @type dry_run: bool
1852
    @param dry_run: whether to peform a dry run
1853

1854
    @rtype: string
1855
    @return: job id
1856

1857
    """
1858
    query = []
1859
    _AppendDryRunIf(query, dry_run)
1860

    
1861
    body = {
1862
      "name": name,
1863
      "alloc_policy": alloc_policy
1864
      }
1865

    
1866
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1867
                             query, body)
1868

    
1869
  def ModifyGroup(self, group, **kwargs):
1870
    """Modifies a node group.
1871

1872
    More details for parameters can be found in the RAPI documentation.
1873

1874
    @type group: string
1875
    @param group: Node group name
1876
    @rtype: string
1877
    @return: job id
1878

1879
    """
1880
    return self._SendRequest(HTTP_PUT,
1881
                             ("/%s/groups/%s/modify" %
1882
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
1883

    
1884
  def DeleteGroup(self, group, dry_run=False):
1885
    """Deletes a node group.
1886

1887
    @type group: str
1888
    @param group: the node group to delete
1889
    @type dry_run: bool
1890
    @param dry_run: whether to peform a dry run
1891

1892
    @rtype: string
1893
    @return: job id
1894

1895
    """
1896
    query = []
1897
    _AppendDryRunIf(query, dry_run)
1898

    
1899
    return self._SendRequest(HTTP_DELETE,
1900
                             ("/%s/groups/%s" %
1901
                              (GANETI_RAPI_VERSION, group)), query, None)
1902

    
1903
  def RenameGroup(self, group, new_name):
1904
    """Changes the name of a node group.
1905

1906
    @type group: string
1907
    @param group: Node group name
1908
    @type new_name: string
1909
    @param new_name: New node group name
1910

1911
    @rtype: string
1912
    @return: job id
1913

1914
    """
1915
    body = {
1916
      "new_name": new_name,
1917
      }
1918

    
1919
    return self._SendRequest(HTTP_PUT,
1920
                             ("/%s/groups/%s/rename" %
1921
                              (GANETI_RAPI_VERSION, group)), None, body)
1922

    
1923
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1924
    """Assigns nodes to a group.
1925

1926
    @type group: string
1927
    @param group: Node group name
1928
    @type nodes: list of strings
1929
    @param nodes: List of nodes to assign to the group
1930

1931
    @rtype: string
1932
    @return: job id
1933

1934
    """
1935
    query = []
1936
    _AppendForceIf(query, force)
1937
    _AppendDryRunIf(query, dry_run)
1938

    
1939
    body = {
1940
      "nodes": nodes,
1941
      }
1942

    
1943
    return self._SendRequest(HTTP_PUT,
1944
                             ("/%s/groups/%s/assign-nodes" %
1945
                             (GANETI_RAPI_VERSION, group)), query, body)
1946

    
1947
  def GetGroupTags(self, group):
1948
    """Gets tags for a node group.
1949

1950
    @type group: string
1951
    @param group: Node group whose tags to return
1952

1953
    @rtype: list of strings
1954
    @return: tags for the group
1955

1956
    """
1957
    return self._SendRequest(HTTP_GET,
1958
                             ("/%s/groups/%s/tags" %
1959
                              (GANETI_RAPI_VERSION, group)), None, None)
1960

    
1961
  def AddGroupTags(self, group, tags, dry_run=False):
1962
    """Adds tags to a node group.
1963

1964
    @type group: str
1965
    @param group: group to add tags to
1966
    @type tags: list of string
1967
    @param tags: tags to add to the group
1968
    @type dry_run: bool
1969
    @param dry_run: whether to perform a dry run
1970

1971
    @rtype: string
1972
    @return: job id
1973

1974
    """
1975
    query = [("tag", t) for t in tags]
1976
    _AppendDryRunIf(query, dry_run)
1977

    
1978
    return self._SendRequest(HTTP_PUT,
1979
                             ("/%s/groups/%s/tags" %
1980
                              (GANETI_RAPI_VERSION, group)), query, None)
1981

    
1982
  def DeleteGroupTags(self, group, tags, dry_run=False):
1983
    """Deletes tags from a node group.
1984

1985
    @type group: str
1986
    @param group: group to delete tags from
1987
    @type tags: list of string
1988
    @param tags: tags to delete
1989
    @type dry_run: bool
1990
    @param dry_run: whether to perform a dry run
1991
    @rtype: string
1992
    @return: job id
1993

1994
    """
1995
    query = [("tag", t) for t in tags]
1996
    _AppendDryRunIf(query, dry_run)
1997

    
1998
    return self._SendRequest(HTTP_DELETE,
1999
                             ("/%s/groups/%s/tags" %
2000
                              (GANETI_RAPI_VERSION, group)), query, None)
2001

    
2002
  def Query(self, what, fields, qfilter=None):
2003
    """Retrieves information about resources.
2004

2005
    @type what: string
2006
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2007
    @type fields: list of string
2008
    @param fields: Requested fields
2009
    @type qfilter: None or list
2010
    @param qfilter: Query filter
2011

2012
    @rtype: string
2013
    @return: job id
2014

2015
    """
2016
    body = {
2017
      "fields": fields,
2018
      }
2019

    
2020
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2021
    # TODO: remove "filter" after 2.7
2022
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
2023

    
2024
    return self._SendRequest(HTTP_PUT,
2025
                             ("/%s/query/%s" %
2026
                              (GANETI_RAPI_VERSION, what)), None, body)
2027

    
2028
  def QueryFields(self, what, fields=None):
2029
    """Retrieves available fields for a resource.
2030

2031
    @type what: string
2032
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2033
    @type fields: list of string
2034
    @param fields: Requested fields
2035

2036
    @rtype: string
2037
    @return: job id
2038

2039
    """
2040
    query = []
2041

    
2042
    if fields is not None:
2043
      _AppendIf(query, True, ("fields", ",".join(fields)))
2044

    
2045
    return self._SendRequest(HTTP_GET,
2046
                             ("/%s/query/%s/fields" %
2047
                              (GANETI_RAPI_VERSION, what)), query, None)