Statistics
| Branch: | Tag: | Revision:

root / snf-common / synnefo / util / rapi.py @ 78548bed

History | View | Annotate | Download (58.9 kB)

1
#
2
#
3

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

    
21

    
22
"""Ganeti RAPI client.
23

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

31
"""
32

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

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

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

    
49

    
50
GANETI_RAPI_PORT = 5080
51
GANETI_RAPI_VERSION = 2
52

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

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

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

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

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

    
95
# Legacy name
96
JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
97

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

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

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

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

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

    
128

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

132
  """
133
  pass
134

    
135

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

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

    
144

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

148
  """
149
  pass
150

    
151

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

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

    
159
  return condition
160

    
161

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

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

    
168

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

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

    
175

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

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

    
183
  return condition
184

    
185

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

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

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

    
203
  return wrapper
204

    
205

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

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

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

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

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

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

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

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

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

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

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

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

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

    
302
  return _ConfigCurl
303

    
304

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
386
    return curl
387

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

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

397
    """
398
    result = []
399

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

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

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

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

    
414
    return result
415

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

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

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

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

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

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

    
440
    curl = self._CreateCurl()
441

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

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

    
453
    url = "".join(urlparts)
454

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

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

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

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

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

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

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

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

    
501
      raise GanetiApiError(msg, code=http_code)
502

    
503
    return response_content
504

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

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

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

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

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

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

    
529
      raise
530

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

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

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

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

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

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

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

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

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

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

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

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

570
    """
571
    body = kwargs
572

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

697
    """
698
    query = []
699

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

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

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

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

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

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

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

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

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

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

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

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

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

754
    """
755
    body = kwargs
756

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1028
    # TODO: Convert to body parameters
1029

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1234
    return False
1235

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1406
      assert len(query) <= 1
1407

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1485
    @type node: string
1486
    @param node: Node name
1487
    @rtype: string
1488
    @return: job id
1489

1490
    """
1491
    return self._SendRequest(HTTP_POST,
1492
                             ("/%s/nodes/%s/modify" %
1493
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1494

    
1495
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1496
    """Gets the storage units for a node.
1497

1498
    @type node: str
1499
    @param node: the node whose storage units to return
1500
    @type storage_type: str
1501
    @param storage_type: storage type whose units to return
1502
    @type output_fields: str
1503
    @param output_fields: storage type fields to return
1504

1505
    @rtype: string
1506
    @return: job id where results can be retrieved
1507

1508
    """
1509
    query = [
1510
      ("storage_type", storage_type),
1511
      ("output_fields", output_fields),
1512
      ]
1513

    
1514
    return self._SendRequest(HTTP_GET,
1515
                             ("/%s/nodes/%s/storage" %
1516
                              (GANETI_RAPI_VERSION, node)), query, None)
1517

    
1518
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1519
    """Modifies parameters of storage units on the node.
1520

1521
    @type node: str
1522
    @param node: node whose storage units to modify
1523
    @type storage_type: str
1524
    @param storage_type: storage type whose units to modify
1525
    @type name: str
1526
    @param name: name of the storage unit
1527
    @type allocatable: bool or None
1528
    @param allocatable: Whether to set the "allocatable" flag on the storage
1529
                        unit (None=no modification, True=set, False=unset)
1530

1531
    @rtype: string
1532
    @return: job id
1533

1534
    """
1535
    query = [
1536
      ("storage_type", storage_type),
1537
      ("name", name),
1538
      ]
1539

    
1540
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1541

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

    
1546
  def RepairNodeStorageUnits(self, node, storage_type, name):
1547
    """Repairs a storage unit on the node.
1548

1549
    @type node: str
1550
    @param node: node whose storage units to repair
1551
    @type storage_type: str
1552
    @param storage_type: storage type to repair
1553
    @type name: str
1554
    @param name: name of the storage unit to repair
1555

1556
    @rtype: string
1557
    @return: job id
1558

1559
    """
1560
    query = [
1561
      ("storage_type", storage_type),
1562
      ("name", name),
1563
      ]
1564

    
1565
    return self._SendRequest(HTTP_PUT,
1566
                             ("/%s/nodes/%s/storage/repair" %
1567
                              (GANETI_RAPI_VERSION, node)), query, None)
1568

    
1569
  def GetNodeTags(self, node):
1570
    """Gets the tags for a node.
1571

1572
    @type node: str
1573
    @param node: node whose tags to return
1574

1575
    @rtype: list of str
1576
    @return: tags for the node
1577

1578
    """
1579
    return self._SendRequest(HTTP_GET,
1580
                             ("/%s/nodes/%s/tags" %
1581
                              (GANETI_RAPI_VERSION, node)), None, None)
1582

    
1583
  def AddNodeTags(self, node, tags, dry_run=False):
1584
    """Adds tags to a node.
1585

1586
    @type node: str
1587
    @param node: node to add tags to
1588
    @type tags: list of str
1589
    @param tags: tags to add to the node
1590
    @type dry_run: bool
1591
    @param dry_run: whether to perform a dry run
1592

1593
    @rtype: string
1594
    @return: job id
1595

1596
    """
1597
    query = [("tag", t) for t in tags]
1598
    _AppendDryRunIf(query, dry_run)
1599

    
1600
    return self._SendRequest(HTTP_PUT,
1601
                             ("/%s/nodes/%s/tags" %
1602
                              (GANETI_RAPI_VERSION, node)), query, tags)
1603

    
1604
  def DeleteNodeTags(self, node, tags, dry_run=False):
1605
    """Delete tags from a node.
1606

1607
    @type node: str
1608
    @param node: node to remove tags from
1609
    @type tags: list of str
1610
    @param tags: tags to remove from the node
1611
    @type dry_run: bool
1612
    @param dry_run: whether to perform a dry run
1613

1614
    @rtype: string
1615
    @return: job id
1616

1617
    """
1618
    query = [("tag", t) for t in tags]
1619
    _AppendDryRunIf(query, dry_run)
1620

    
1621
    return self._SendRequest(HTTP_DELETE,
1622
                             ("/%s/nodes/%s/tags" %
1623
                              (GANETI_RAPI_VERSION, node)), query, None)
1624

    
1625
  def GetNetworks(self, bulk=False):
1626
    """Gets all networks in the cluster.
1627

1628
    @type bulk: bool
1629
    @param bulk: whether to return all information about the networks
1630

1631
    @rtype: list of dict or str
1632
    @return: if bulk is true, a list of dictionaries with info about all
1633
        networks in the cluster, else a list of names of those networks
1634

1635
    """
1636
    query = []
1637
    _AppendIf(query, bulk, ("bulk", 1))
1638

    
1639
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1640
                               query, None)
1641
    if bulk:
1642
      return networks
1643
    else:
1644
      return [n["name"] for n in networks]
1645

    
1646
  def GetNetwork(self, network):
1647
    """Gets information about a network.
1648

1649
    @type group: str
1650
    @param group: name of the network whose info to return
1651

1652
    @rtype: dict
1653
    @return: info about the network
1654

1655
    """
1656
    return self._SendRequest(HTTP_GET,
1657
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1658
                             None, None)
1659

    
1660
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1661
                    gateway6=None, mac_prefix=None, network_type="private",
1662
                    reserved_ips=None, tags=None, dry_run=False):
1663
    """Creates a new network.
1664

1665
    @type name: str
1666
    @param name: the name of network to create
1667
    @type dry_run: bool
1668
    @param dry_run: whether to peform a dry run
1669

1670
    @rtype: string
1671
    @return: job id
1672

1673
    """
1674
    query = []
1675
    _AppendDryRunIf(query, dry_run)
1676

    
1677
    if reserved_ips:
1678
        reserved_ips = reserved_ips.split(',')
1679

    
1680
    if tags:
1681
        tags = tags.split(',')
1682

    
1683
    body = {
1684
      "network_name": network_name,
1685
      "gateway": gateway,
1686
      "network": network,
1687
      "gateway6": gateway6,
1688
      "network6": network6,
1689
      "mac_prefix": mac_prefix,
1690
      "network_type": network_type,
1691
      "reserved_ips": reserved_ips,
1692
      "tags": tags
1693
      }
1694

    
1695
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1696
                             query, body)
1697

    
1698
  def ConnectNetwork(self, network_name, group_name, mode, link, depends=None):
1699
    """Connects a Network to a NodeGroup with the given netparams
1700

1701
    """
1702
    body = {
1703
      "group_name": group_name,
1704
      "network_mode": mode,
1705
      "network_link": link
1706
      }
1707

    
1708
    if depends:
1709
      body['depends'] = []
1710
      for d in depends:
1711
        body['depends'].append([d, ["success"]])
1712

    
1713

    
1714
    return self._SendRequest(HTTP_PUT,
1715
                             ("/%s/networks/%s/connect" %
1716
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1717

    
1718
  def DisconnectNetwork(self, network_name, group_name, depends=None):
1719
    """Connects a Network to a NodeGroup with the given netparams
1720

1721
    """
1722
    body = {
1723
      "group_name": group_name
1724
      }
1725

    
1726
    if depends:
1727
      body['depends'] = []
1728
      for d in depends:
1729
        body['depends'].append([d, ["success"]])
1730

    
1731
    return self._SendRequest(HTTP_PUT,
1732
                             ("/%s/networks/%s/disconnect" %
1733
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1734

    
1735
  def ConnectNetworkAll(self, network_name, mode, link, depends=None):
1736
    """Connects a Network to a NodeGroup with the given netparams
1737

1738
    """
1739
    body = {
1740
      "network_mode": mode,
1741
      "network_link": link
1742
      }
1743

    
1744
    if depends:
1745
      body['depends'] = []
1746
      for d in depends:
1747
        body['depends'].append([d, ["success"]])
1748

    
1749
    return self._SendRequest(HTTP_PUT,
1750
                             ("/%s/networks/%s/connectall" %
1751
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1752

    
1753
  def DisconnectNetworkAll(self, network_name, depends=None):
1754
    """Connects a Network to a NodeGroup with the given netparams
1755

1756
    """
1757
    body = {}
1758
    if depends:
1759
      body['depends'] = []
1760
      for d in depends:
1761
        body['depends'].append([d, ["success"]])
1762

    
1763
    return self._SendRequest(HTTP_PUT,
1764
                             ("/%s/networks/%s/disconnectall" %
1765
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1766

    
1767
  def DeleteNetwork(self, network, depends=None):
1768
    """Deletes a network.
1769

1770
    @type group: str
1771
    @param group: the network to delete
1772
    @type dry_run: bool
1773
    @param dry_run: whether to peform a dry run
1774

1775
    @rtype: string
1776
    @return: job id
1777

1778
    """
1779
    body = {}
1780
    if depends:
1781
      body['depends'] = []
1782
      for d in depends:
1783
        body['depends'].append([d, ["success"]])
1784

    
1785

    
1786
    return self._SendRequest(HTTP_DELETE,
1787
                             ("/%s/networks/%s" %
1788
                              (GANETI_RAPI_VERSION, network)), None, body)
1789

    
1790
  def GetGroups(self, bulk=False):
1791
    """Gets all node groups in the cluster.
1792

1793
    @type bulk: bool
1794
    @param bulk: whether to return all information about the groups
1795

1796
    @rtype: list of dict or str
1797
    @return: if bulk is true, a list of dictionaries with info about all node
1798
        groups in the cluster, else a list of names of those node groups
1799

1800
    """
1801
    query = []
1802
    _AppendIf(query, bulk, ("bulk", 1))
1803

    
1804
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1805
                               query, None)
1806
    if bulk:
1807
      return groups
1808
    else:
1809
      return [g["name"] for g in groups]
1810

    
1811
  def GetGroup(self, group):
1812
    """Gets information about a node group.
1813

1814
    @type group: str
1815
    @param group: name of the node group whose info to return
1816

1817
    @rtype: dict
1818
    @return: info about the node group
1819

1820
    """
1821
    return self._SendRequest(HTTP_GET,
1822
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1823
                             None, None)
1824

    
1825
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1826
    """Creates a new node group.
1827

1828
    @type name: str
1829
    @param name: the name of node group to create
1830
    @type alloc_policy: str
1831
    @param alloc_policy: the desired allocation policy for the group, if any
1832
    @type dry_run: bool
1833
    @param dry_run: whether to peform a dry run
1834

1835
    @rtype: string
1836
    @return: job id
1837

1838
    """
1839
    query = []
1840
    _AppendDryRunIf(query, dry_run)
1841

    
1842
    body = {
1843
      "name": name,
1844
      "alloc_policy": alloc_policy
1845
      }
1846

    
1847
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1848
                             query, body)
1849

    
1850
  def ModifyGroup(self, group, **kwargs):
1851
    """Modifies a node group.
1852

1853
    More details for parameters can be found in the RAPI documentation.
1854

1855
    @type group: string
1856
    @param group: Node group name
1857
    @rtype: string
1858
    @return: job id
1859

1860
    """
1861
    return self._SendRequest(HTTP_PUT,
1862
                             ("/%s/groups/%s/modify" %
1863
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
1864

    
1865
  def DeleteGroup(self, group, dry_run=False):
1866
    """Deletes a node group.
1867

1868
    @type group: str
1869
    @param group: the node group to delete
1870
    @type dry_run: bool
1871
    @param dry_run: whether to peform a dry run
1872

1873
    @rtype: string
1874
    @return: job id
1875

1876
    """
1877
    query = []
1878
    _AppendDryRunIf(query, dry_run)
1879

    
1880
    return self._SendRequest(HTTP_DELETE,
1881
                             ("/%s/groups/%s" %
1882
                              (GANETI_RAPI_VERSION, group)), query, None)
1883

    
1884
  def RenameGroup(self, group, new_name):
1885
    """Changes the name of a node group.
1886

1887
    @type group: string
1888
    @param group: Node group name
1889
    @type new_name: string
1890
    @param new_name: New node group name
1891

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

1895
    """
1896
    body = {
1897
      "new_name": new_name,
1898
      }
1899

    
1900
    return self._SendRequest(HTTP_PUT,
1901
                             ("/%s/groups/%s/rename" %
1902
                              (GANETI_RAPI_VERSION, group)), None, body)
1903

    
1904
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1905
    """Assigns nodes to a group.
1906

1907
    @type group: string
1908
    @param group: Node gropu name
1909
    @type nodes: list of strings
1910
    @param nodes: List of nodes to assign to the group
1911

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

1915
    """
1916
    query = []
1917
    _AppendForceIf(query, force)
1918
    _AppendDryRunIf(query, dry_run)
1919

    
1920
    body = {
1921
      "nodes": nodes,
1922
      }
1923

    
1924
    return self._SendRequest(HTTP_PUT,
1925
                             ("/%s/groups/%s/assign-nodes" %
1926
                             (GANETI_RAPI_VERSION, group)), query, body)
1927

    
1928
  def GetGroupTags(self, group):
1929
    """Gets tags for a node group.
1930

1931
    @type group: string
1932
    @param group: Node group whose tags to return
1933

1934
    @rtype: list of strings
1935
    @return: tags for the group
1936

1937
    """
1938
    return self._SendRequest(HTTP_GET,
1939
                             ("/%s/groups/%s/tags" %
1940
                              (GANETI_RAPI_VERSION, group)), None, None)
1941

    
1942
  def AddGroupTags(self, group, tags, dry_run=False):
1943
    """Adds tags to a node group.
1944

1945
    @type group: str
1946
    @param group: group to add tags to
1947
    @type tags: list of string
1948
    @param tags: tags to add to the group
1949
    @type dry_run: bool
1950
    @param dry_run: whether to perform a dry run
1951

1952
    @rtype: string
1953
    @return: job id
1954

1955
    """
1956
    query = [("tag", t) for t in tags]
1957
    _AppendDryRunIf(query, dry_run)
1958

    
1959
    return self._SendRequest(HTTP_PUT,
1960
                             ("/%s/groups/%s/tags" %
1961
                              (GANETI_RAPI_VERSION, group)), query, None)
1962

    
1963
  def DeleteGroupTags(self, group, tags, dry_run=False):
1964
    """Deletes tags from a node group.
1965

1966
    @type group: str
1967
    @param group: group to delete tags from
1968
    @type tags: list of string
1969
    @param tags: tags to delete
1970
    @type dry_run: bool
1971
    @param dry_run: whether to perform a dry run
1972
    @rtype: string
1973
    @return: job id
1974

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

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

    
1983
  def Query(self, what, fields, qfilter=None):
1984
    """Retrieves information about resources.
1985

1986
    @type what: string
1987
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1988
    @type fields: list of string
1989
    @param fields: Requested fields
1990
    @type qfilter: None or list
1991
    @param qfilter: Query filter
1992

1993
    @rtype: string
1994
    @return: job id
1995

1996
    """
1997
    body = {
1998
      "fields": fields,
1999
      }
2000

    
2001
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2002
    # TODO: remove "filter" after 2.7
2003
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
2004

    
2005
    return self._SendRequest(HTTP_PUT,
2006
                             ("/%s/query/%s" %
2007
                              (GANETI_RAPI_VERSION, what)), None, body)
2008

    
2009
  def QueryFields(self, what, fields=None):
2010
    """Retrieves available fields for a resource.
2011

2012
    @type what: string
2013
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2014
    @type fields: list of string
2015
    @param fields: Requested fields
2016

2017
    @rtype: string
2018
    @return: job id
2019

2020
    """
2021
    query = []
2022

    
2023
    if fields is not None:
2024
      _AppendIf(query, True, ("fields", ",".join(fields)))
2025

    
2026
    return self._SendRequest(HTTP_GET,
2027
                             ("/%s/query/%s/fields" %
2028
                              (GANETI_RAPI_VERSION, what)), query, None)