Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ 4c864b55

History | View | Annotate | Download (54.1 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
_INST_CREATE_REQV1 = "instance-create-reqv1"
101
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
102
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
103
_NODE_EVAC_RES1 = "node-evac-res1"
104
_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link"])
105
_INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
106
_INST_CREATE_V0_PARAMS = frozenset([
107
  "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
108
  "hypervisor", "file_storage_dir", "file_driver", "dry_run",
109
  ])
110
_INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
111
_QPARAM_DRY_RUN = "dry-run"
112
_QPARAM_FORCE = "force"
113

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

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

    
127

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

131
  """
132
  pass
133

    
134

    
135
class CertificateError(Error):
136
  """Raised when a problem is found with the SSL certificate.
137

138
  """
139
  pass
140

    
141

    
142
class GanetiApiError(Error):
143
  """Generic error raised from Ganeti API.
144

145
  """
146
  def __init__(self, msg, code=None):
147
    Error.__init__(self, msg)
148
    self.code = code
149

    
150

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

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

    
158
  return condition
159

    
160

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

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

    
167

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

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

    
174

    
175
def UsesRapiClient(fn):
176
  """Decorator for code using RAPI client to initialize pycURL.
177

178
  """
179
  def wrapper(*args, **kwargs):
180
    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
181
    # one thread running. This check is just a safety measure -- it doesn't
182
    # cover all cases.
183
    assert threading.activeCount() == 1, \
184
           "Found active threads when initializing pycURL"
185

    
186
    pycurl.global_init(pycurl.GLOBAL_ALL)
187
    try:
188
      return fn(*args, **kwargs)
189
    finally:
190
      pycurl.global_cleanup()
191

    
192
  return wrapper
193

    
194

    
195
def GenericCurlConfig(verbose=False, use_signal=False,
196
                      use_curl_cabundle=False, cafile=None, capath=None,
197
                      proxy=None, verify_hostname=False,
198
                      connect_timeout=None, timeout=None,
199
                      _pycurl_version_fn=pycurl.version_info):
200
  """Curl configuration function generator.
201

202
  @type verbose: bool
203
  @param verbose: Whether to set cURL to verbose mode
204
  @type use_signal: bool
205
  @param use_signal: Whether to allow cURL to use signals
206
  @type use_curl_cabundle: bool
207
  @param use_curl_cabundle: Whether to use cURL's default CA bundle
208
  @type cafile: string
209
  @param cafile: In which file we can find the certificates
210
  @type capath: string
211
  @param capath: In which directory we can find the certificates
212
  @type proxy: string
213
  @param proxy: Proxy to use, None for default behaviour and empty string for
214
                disabling proxies (see curl_easy_setopt(3))
215
  @type verify_hostname: bool
216
  @param verify_hostname: Whether to verify the remote peer certificate's
217
                          commonName
218
  @type connect_timeout: number
219
  @param connect_timeout: Timeout for establishing connection in seconds
220
  @type timeout: number
221
  @param timeout: Timeout for complete transfer in seconds (see
222
                  curl_easy_setopt(3)).
223

224
  """
225
  if use_curl_cabundle and (cafile or capath):
226
    raise Error("Can not use default CA bundle when CA file or path is set")
227

    
228
  def _ConfigCurl(curl, logger):
229
    """Configures a cURL object
230

231
    @type curl: pycurl.Curl
232
    @param curl: cURL object
233

234
    """
235
    logger.debug("Using cURL version %s", pycurl.version)
236

    
237
    # pycurl.version_info returns a tuple with information about the used
238
    # version of libcurl. Item 5 is the SSL library linked to it.
239
    # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
240
    # 0, '1.2.3.3', ...)
241
    sslver = _pycurl_version_fn()[5]
242
    if not sslver:
243
      raise Error("No SSL support in cURL")
244

    
245
    lcsslver = sslver.lower()
246
    if lcsslver.startswith("openssl/"):
247
      pass
248
    elif lcsslver.startswith("gnutls/"):
249
      if capath:
250
        raise Error("cURL linked against GnuTLS has no support for a"
251
                    " CA path (%s)" % (pycurl.version, ))
252
    else:
253
      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
254
                                sslver)
255

    
256
    curl.setopt(pycurl.VERBOSE, verbose)
257
    curl.setopt(pycurl.NOSIGNAL, not use_signal)
258

    
259
    # Whether to verify remote peer's CN
260
    if verify_hostname:
261
      # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
262
      # certificate must indicate that the server is the server to which you
263
      # meant to connect, or the connection fails. [...] When the value is 1,
264
      # the certificate must contain a Common Name field, but it doesn't matter
265
      # what name it says. [...]"
266
      curl.setopt(pycurl.SSL_VERIFYHOST, 2)
267
    else:
268
      curl.setopt(pycurl.SSL_VERIFYHOST, 0)
269

    
270
    if cafile or capath or use_curl_cabundle:
271
      # Require certificates to be checked
272
      curl.setopt(pycurl.SSL_VERIFYPEER, True)
273
      if cafile:
274
        curl.setopt(pycurl.CAINFO, str(cafile))
275
      if capath:
276
        curl.setopt(pycurl.CAPATH, str(capath))
277
      # Not changing anything for using default CA bundle
278
    else:
279
      # Disable SSL certificate verification
280
      curl.setopt(pycurl.SSL_VERIFYPEER, False)
281

    
282
    if proxy is not None:
283
      curl.setopt(pycurl.PROXY, str(proxy))
284

    
285
    # Timeouts
286
    if connect_timeout is not None:
287
      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
288
    if timeout is not None:
289
      curl.setopt(pycurl.TIMEOUT, timeout)
290

    
291
  return _ConfigCurl
292

    
293

    
294
class GanetiRapiClient(object): # pylint: disable=R0904
295
  """Ganeti RAPI client.
296

297
  """
298
  USER_AGENT = "Ganeti RAPI Client"
299
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
300

    
301
  def __init__(self, host, port=GANETI_RAPI_PORT,
302
               username=None, password=None, logger=logging,
303
               curl_config_fn=None, curl_factory=None):
304
    """Initializes this class.
305

306
    @type host: string
307
    @param host: the ganeti cluster master to interact with
308
    @type port: int
309
    @param port: the port on which the RAPI is running (default is 5080)
310
    @type username: string
311
    @param username: the username to connect with
312
    @type password: string
313
    @param password: the password to connect with
314
    @type curl_config_fn: callable
315
    @param curl_config_fn: Function to configure C{pycurl.Curl} object
316
    @param logger: Logging object
317

318
    """
319
    self._username = username
320
    self._password = password
321
    self._logger = logger
322
    self._curl_config_fn = curl_config_fn
323
    self._curl_factory = curl_factory
324

    
325
    try:
326
      socket.inet_pton(socket.AF_INET6, host)
327
      address = "[%s]:%s" % (host, port)
328
    except socket.error:
329
      address = "%s:%s" % (host, port)
330

    
331
    self._base_url = "https://%s" % address
332

    
333
    if username is not None:
334
      if password is None:
335
        raise Error("Password not specified")
336
    elif password:
337
      raise Error("Specified password without username")
338

    
339
  def _CreateCurl(self):
340
    """Creates a cURL object.
341

342
    """
343
    # Create pycURL object if no factory is provided
344
    if self._curl_factory:
345
      curl = self._curl_factory()
346
    else:
347
      curl = pycurl.Curl()
348

    
349
    # Default cURL settings
350
    curl.setopt(pycurl.VERBOSE, False)
351
    curl.setopt(pycurl.FOLLOWLOCATION, False)
352
    curl.setopt(pycurl.MAXREDIRS, 5)
353
    curl.setopt(pycurl.NOSIGNAL, True)
354
    curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
355
    curl.setopt(pycurl.SSL_VERIFYHOST, 0)
356
    curl.setopt(pycurl.SSL_VERIFYPEER, False)
357
    curl.setopt(pycurl.HTTPHEADER, [
358
      "Accept: %s" % HTTP_APP_JSON,
359
      "Content-type: %s" % HTTP_APP_JSON,
360
      ])
361

    
362
    assert ((self._username is None and self._password is None) ^
363
            (self._username is not None and self._password is not None))
364

    
365
    if self._username:
366
      # Setup authentication
367
      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
368
      curl.setopt(pycurl.USERPWD,
369
                  str("%s:%s" % (self._username, self._password)))
370

    
371
    # Call external configuration function
372
    if self._curl_config_fn:
373
      self._curl_config_fn(curl, self._logger)
374

    
375
    return curl
376

    
377
  @staticmethod
378
  def _EncodeQuery(query):
379
    """Encode query values for RAPI URL.
380

381
    @type query: list of two-tuples
382
    @param query: Query arguments
383
    @rtype: list
384
    @return: Query list with encoded values
385

386
    """
387
    result = []
388

    
389
    for name, value in query:
390
      if value is None:
391
        result.append((name, ""))
392

    
393
      elif isinstance(value, bool):
394
        # Boolean values must be encoded as 0 or 1
395
        result.append((name, int(value)))
396

    
397
      elif isinstance(value, (list, tuple, dict)):
398
        raise ValueError("Invalid query data type %r" % type(value).__name__)
399

    
400
      else:
401
        result.append((name, value))
402

    
403
    return result
404

    
405
  def _SendRequest(self, method, path, query, content):
406
    """Sends an HTTP request.
407

408
    This constructs a full URL, encodes and decodes HTTP bodies, and
409
    handles invalid responses in a pythonic way.
410

411
    @type method: string
412
    @param method: HTTP method to use
413
    @type path: string
414
    @param path: HTTP URL path
415
    @type query: list of two-tuples
416
    @param query: query arguments to pass to urllib.urlencode
417
    @type content: str or None
418
    @param content: HTTP body content
419

420
    @rtype: str
421
    @return: JSON-Decoded response
422

423
    @raises CertificateError: If an invalid SSL certificate is found
424
    @raises GanetiApiError: If an invalid response is returned
425

426
    """
427
    assert path.startswith("/")
428

    
429
    curl = self._CreateCurl()
430

    
431
    if content is not None:
432
      encoded_content = self._json_encoder.encode(content)
433
    else:
434
      encoded_content = ""
435

    
436
    # Build URL
437
    urlparts = [self._base_url, path]
438
    if query:
439
      urlparts.append("?")
440
      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
441

    
442
    url = "".join(urlparts)
443

    
444
    self._logger.debug("Sending request %s %s (content=%r)",
445
                       method, url, encoded_content)
446

    
447
    # Buffer for response
448
    encoded_resp_body = StringIO()
449

    
450
    # Configure cURL
451
    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
452
    curl.setopt(pycurl.URL, str(url))
453
    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
454
    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
455

    
456
    try:
457
      # Send request and wait for response
458
      try:
459
        curl.perform()
460
      except pycurl.error, err:
461
        if err.args[0] in _CURL_SSL_CERT_ERRORS:
462
          raise CertificateError("SSL certificate error %s" % err)
463

    
464
        raise GanetiApiError(str(err))
465
    finally:
466
      # Reset settings to not keep references to large objects in memory
467
      # between requests
468
      curl.setopt(pycurl.POSTFIELDS, "")
469
      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
470

    
471
    # Get HTTP response code
472
    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
473

    
474
    # Was anything written to the response buffer?
475
    if encoded_resp_body.tell():
476
      response_content = simplejson.loads(encoded_resp_body.getvalue())
477
    else:
478
      response_content = None
479

    
480
    if http_code != HTTP_OK:
481
      if isinstance(response_content, dict):
482
        msg = ("%s %s: %s" %
483
               (response_content["code"],
484
                response_content["message"],
485
                response_content["explain"]))
486
      else:
487
        msg = str(response_content)
488

    
489
      raise GanetiApiError(msg, code=http_code)
490

    
491
    return response_content
492

    
493
  def GetVersion(self):
494
    """Gets the Remote API version running on the cluster.
495

496
    @rtype: int
497
    @return: Ganeti Remote API version
498

499
    """
500
    return self._SendRequest(HTTP_GET, "/version", None, None)
501

    
502
  def GetFeatures(self):
503
    """Gets the list of optional features supported by RAPI server.
504

505
    @rtype: list
506
    @return: List of optional features
507

508
    """
509
    try:
510
      return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
511
                               None, None)
512
    except GanetiApiError, err:
513
      # Older RAPI servers don't support this resource
514
      if err.code == HTTP_NOT_FOUND:
515
        return []
516

    
517
      raise
518

    
519
  def GetOperatingSystems(self):
520
    """Gets the Operating Systems running in the Ganeti cluster.
521

522
    @rtype: list of str
523
    @return: operating systems
524

525
    """
526
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
527
                             None, None)
528

    
529
  def GetInfo(self):
530
    """Gets info about the cluster.
531

532
    @rtype: dict
533
    @return: information about the cluster
534

535
    """
536
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
537
                             None, None)
538

    
539
  def RedistributeConfig(self):
540
    """Tells the cluster to redistribute its configuration files.
541

542
    @rtype: string
543
    @return: job id
544

545
    """
546
    return self._SendRequest(HTTP_PUT,
547
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
548
                             None, None)
549

    
550
  def ModifyCluster(self, **kwargs):
551
    """Modifies cluster parameters.
552

553
    More details for parameters can be found in the RAPI documentation.
554

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

558
    """
559
    body = kwargs
560

    
561
    return self._SendRequest(HTTP_PUT,
562
                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
563

    
564
  def GetClusterTags(self):
565
    """Gets the cluster tags.
566

567
    @rtype: list of str
568
    @return: cluster tags
569

570
    """
571
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
572
                             None, None)
573

    
574
  def AddClusterTags(self, tags, dry_run=False):
575
    """Adds tags to the cluster.
576

577
    @type tags: list of str
578
    @param tags: tags to add to the cluster
579
    @type dry_run: bool
580
    @param dry_run: whether to perform a dry run
581

582
    @rtype: string
583
    @return: job id
584

585
    """
586
    query = [("tag", t) for t in tags]
587
    _AppendDryRunIf(query, dry_run)
588

    
589
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
590
                             query, None)
591

    
592
  def DeleteClusterTags(self, tags, dry_run=False):
593
    """Deletes tags from the cluster.
594

595
    @type tags: list of str
596
    @param tags: tags to delete
597
    @type dry_run: bool
598
    @param dry_run: whether to perform a dry run
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_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
607
                             query, None)
608

    
609
  def GetInstances(self, bulk=False):
610
    """Gets information about instances on the cluster.
611

612
    @type bulk: bool
613
    @param bulk: whether to return all information about all instances
614

615
    @rtype: list of dict or list of str
616
    @return: if bulk is True, info about the instances, else a list of instances
617

618
    """
619
    query = []
620
    _AppendIf(query, bulk, ("bulk", 1))
621

    
622
    instances = self._SendRequest(HTTP_GET,
623
                                  "/%s/instances" % GANETI_RAPI_VERSION,
624
                                  query, None)
625
    if bulk:
626
      return instances
627
    else:
628
      return [i["id"] for i in instances]
629

    
630
  def GetInstance(self, instance):
631
    """Gets information about an instance.
632

633
    @type instance: str
634
    @param instance: instance whose info to return
635

636
    @rtype: dict
637
    @return: info about the instance
638

639
    """
640
    return self._SendRequest(HTTP_GET,
641
                             ("/%s/instances/%s" %
642
                              (GANETI_RAPI_VERSION, instance)), None, None)
643

    
644
  def GetInstanceInfo(self, instance, static=None):
645
    """Gets information about an instance.
646

647
    @type instance: string
648
    @param instance: Instance name
649
    @rtype: string
650
    @return: Job ID
651

652
    """
653
    if static is not None:
654
      query = [("static", static)]
655
    else:
656
      query = None
657

    
658
    return self._SendRequest(HTTP_GET,
659
                             ("/%s/instances/%s/info" %
660
                              (GANETI_RAPI_VERSION, instance)), query, None)
661

    
662
  def CreateInstance(self, mode, name, disk_template, disks, nics,
663
                     **kwargs):
664
    """Creates a new instance.
665

666
    More details for parameters can be found in the RAPI documentation.
667

668
    @type mode: string
669
    @param mode: Instance creation mode
670
    @type name: string
671
    @param name: Hostname of the instance to create
672
    @type disk_template: string
673
    @param disk_template: Disk template for instance (e.g. plain, diskless,
674
                          file, or drbd)
675
    @type disks: list of dicts
676
    @param disks: List of disk definitions
677
    @type nics: list of dicts
678
    @param nics: List of NIC definitions
679
    @type dry_run: bool
680
    @keyword dry_run: whether to perform a dry run
681

682
    @rtype: string
683
    @return: job id
684

685
    """
686
    query = []
687

    
688
    _AppendDryRunIf(query, kwargs.get("dry_run"))
689

    
690
    if _INST_CREATE_REQV1 in self.GetFeatures():
691
      # All required fields for request data version 1
692
      body = {
693
        _REQ_DATA_VERSION_FIELD: 1,
694
        "mode": mode,
695
        "name": name,
696
        "disk_template": disk_template,
697
        "disks": disks,
698
        "nics": nics,
699
        }
700

    
701
      conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
702
      if conflicts:
703
        raise GanetiApiError("Required fields can not be specified as"
704
                             " keywords: %s" % ", ".join(conflicts))
705

    
706
      body.update((key, value) for key, value in kwargs.iteritems()
707
                  if key != "dry_run")
708
    else:
709
      raise GanetiApiError("Server does not support new-style (version 1)"
710
                           " instance creation requests")
711

    
712
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
713
                             query, body)
714

    
715
  def DeleteInstance(self, instance, dry_run=False):
716
    """Deletes an instance.
717

718
    @type instance: str
719
    @param instance: the instance to delete
720

721
    @rtype: string
722
    @return: job id
723

724
    """
725
    query = []
726
    _AppendDryRunIf(query, dry_run)
727

    
728
    return self._SendRequest(HTTP_DELETE,
729
                             ("/%s/instances/%s" %
730
                              (GANETI_RAPI_VERSION, instance)), query, None)
731

    
732
  def ModifyInstance(self, instance, **kwargs):
733
    """Modifies an instance.
734

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

737
    @type instance: string
738
    @param instance: Instance name
739
    @rtype: string
740
    @return: job id
741

742
    """
743
    body = kwargs
744

    
745
    return self._SendRequest(HTTP_PUT,
746
                             ("/%s/instances/%s/modify" %
747
                              (GANETI_RAPI_VERSION, instance)), None, body)
748

    
749
  def ActivateInstanceDisks(self, instance, ignore_size=None):
750
    """Activates an instance's disks.
751

752
    @type instance: string
753
    @param instance: Instance name
754
    @type ignore_size: bool
755
    @param ignore_size: Whether to ignore recorded size
756
    @rtype: string
757
    @return: job id
758

759
    """
760
    query = []
761
    _AppendIf(query, ignore_size, ("ignore_size", 1))
762

    
763
    return self._SendRequest(HTTP_PUT,
764
                             ("/%s/instances/%s/activate-disks" %
765
                              (GANETI_RAPI_VERSION, instance)), query, None)
766

    
767
  def DeactivateInstanceDisks(self, instance):
768
    """Deactivates an instance's disks.
769

770
    @type instance: string
771
    @param instance: Instance name
772
    @rtype: string
773
    @return: job id
774

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

    
780
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
781
    """Recreate an instance's disks.
782

783
    @type instance: string
784
    @param instance: Instance name
785
    @type disks: list of int
786
    @param disks: List of disk indexes
787
    @type nodes: list of string
788
    @param nodes: New instance nodes, if relocation is desired
789
    @rtype: string
790
    @return: job id
791

792
    """
793
    body = {}
794

    
795
    if disks is not None:
796
      body["disks"] = disks
797

    
798
    if nodes is not None:
799
      body["nodes"] = nodes
800

    
801
    return self._SendRequest(HTTP_POST,
802
                             ("/%s/instances/%s/recreate-disks" %
803
                              (GANETI_RAPI_VERSION, instance)), None, body)
804

    
805
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
806
    """Grows a disk of an instance.
807

808
    More details for parameters can be found in the RAPI documentation.
809

810
    @type instance: string
811
    @param instance: Instance name
812
    @type disk: integer
813
    @param disk: Disk index
814
    @type amount: integer
815
    @param amount: Grow disk by this amount (MiB)
816
    @type wait_for_sync: bool
817
    @param wait_for_sync: Wait for disk to synchronize
818
    @rtype: string
819
    @return: job id
820

821
    """
822
    body = {
823
      "amount": amount,
824
      }
825

    
826
    if wait_for_sync is not None:
827
      body["wait_for_sync"] = wait_for_sync
828

    
829
    return self._SendRequest(HTTP_POST,
830
                             ("/%s/instances/%s/disk/%s/grow" %
831
                              (GANETI_RAPI_VERSION, instance, disk)),
832
                             None, body)
833

    
834
  def GetInstanceTags(self, instance):
835
    """Gets tags for an instance.
836

837
    @type instance: str
838
    @param instance: instance whose tags to return
839

840
    @rtype: list of str
841
    @return: tags for the instance
842

843
    """
844
    return self._SendRequest(HTTP_GET,
845
                             ("/%s/instances/%s/tags" %
846
                              (GANETI_RAPI_VERSION, instance)), None, None)
847

    
848
  def AddInstanceTags(self, instance, tags, dry_run=False):
849
    """Adds tags to an instance.
850

851
    @type instance: str
852
    @param instance: instance to add tags to
853
    @type tags: list of str
854
    @param tags: tags to add to the instance
855
    @type dry_run: bool
856
    @param dry_run: whether to perform a dry run
857

858
    @rtype: string
859
    @return: job id
860

861
    """
862
    query = [("tag", t) for t in tags]
863
    _AppendDryRunIf(query, dry_run)
864

    
865
    return self._SendRequest(HTTP_PUT,
866
                             ("/%s/instances/%s/tags" %
867
                              (GANETI_RAPI_VERSION, instance)), query, None)
868

    
869
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
870
    """Deletes tags from an instance.
871

872
    @type instance: str
873
    @param instance: instance to delete tags from
874
    @type tags: list of str
875
    @param tags: tags to delete
876
    @type dry_run: bool
877
    @param dry_run: whether to perform a dry run
878
    @rtype: string
879
    @return: job id
880

881
    """
882
    query = [("tag", t) for t in tags]
883
    _AppendDryRunIf(query, dry_run)
884

    
885
    return self._SendRequest(HTTP_DELETE,
886
                             ("/%s/instances/%s/tags" %
887
                              (GANETI_RAPI_VERSION, instance)), query, None)
888

    
889
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
890
                     dry_run=False):
891
    """Reboots an instance.
892

893
    @type instance: str
894
    @param instance: instance to rebot
895
    @type reboot_type: str
896
    @param reboot_type: one of: hard, soft, full
897
    @type ignore_secondaries: bool
898
    @param ignore_secondaries: if True, ignores errors for the secondary node
899
        while re-assembling disks (in hard-reboot mode only)
900
    @type dry_run: bool
901
    @param dry_run: whether to perform a dry run
902
    @rtype: string
903
    @return: job id
904

905
    """
906
    query = []
907
    _AppendDryRunIf(query, dry_run)
908
    _AppendIf(query, reboot_type, ("type", reboot_type))
909
    _AppendIf(query, ignore_secondaries is not None,
910
              ("ignore_secondaries", ignore_secondaries))
911

    
912
    return self._SendRequest(HTTP_POST,
913
                             ("/%s/instances/%s/reboot" %
914
                              (GANETI_RAPI_VERSION, instance)), query, None)
915

    
916
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
917
    """Shuts down an instance.
918

919
    @type instance: str
920
    @param instance: the instance to shut down
921
    @type dry_run: bool
922
    @param dry_run: whether to perform a dry run
923
    @type no_remember: bool
924
    @param no_remember: if true, will not record the state change
925
    @rtype: string
926
    @return: job id
927

928
    """
929
    query = []
930
    _AppendDryRunIf(query, dry_run)
931
    _AppendIf(query, no_remember, ("no-remember", 1))
932

    
933
    return self._SendRequest(HTTP_PUT,
934
                             ("/%s/instances/%s/shutdown" %
935
                              (GANETI_RAPI_VERSION, instance)), query, None)
936

    
937
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
938
    """Starts up an instance.
939

940
    @type instance: str
941
    @param instance: the instance to start up
942
    @type dry_run: bool
943
    @param dry_run: whether to perform a dry run
944
    @type no_remember: bool
945
    @param no_remember: if true, will not record the state change
946
    @rtype: string
947
    @return: job id
948

949
    """
950
    query = []
951
    _AppendDryRunIf(query, dry_run)
952
    _AppendIf(query, no_remember, ("no-remember", 1))
953

    
954
    return self._SendRequest(HTTP_PUT,
955
                             ("/%s/instances/%s/startup" %
956
                              (GANETI_RAPI_VERSION, instance)), query, None)
957

    
958
  def ReinstallInstance(self, instance, os=None, no_startup=False,
959
                        osparams=None):
960
    """Reinstalls an instance.
961

962
    @type instance: str
963
    @param instance: The instance to reinstall
964
    @type os: str or None
965
    @param os: The operating system to reinstall. If None, the instance's
966
        current operating system will be installed again
967
    @type no_startup: bool
968
    @param no_startup: Whether to start the instance automatically
969
    @rtype: string
970
    @return: job id
971

972
    """
973
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
974
      body = {
975
        "start": not no_startup,
976
        }
977
      if os is not None:
978
        body["os"] = os
979
      if osparams is not None:
980
        body["osparams"] = osparams
981
      return self._SendRequest(HTTP_POST,
982
                               ("/%s/instances/%s/reinstall" %
983
                                (GANETI_RAPI_VERSION, instance)), None, body)
984

    
985
    # Use old request format
986
    if osparams:
987
      raise GanetiApiError("Server does not support specifying OS parameters"
988
                           " for instance reinstallation")
989

    
990
    query = []
991
    _AppendIf(query, os, ("os", os))
992
    _AppendIf(query, no_startup, ("nostartup", 1))
993

    
994
    return self._SendRequest(HTTP_POST,
995
                             ("/%s/instances/%s/reinstall" %
996
                              (GANETI_RAPI_VERSION, instance)), query, None)
997

    
998
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
999
                           remote_node=None, iallocator=None):
1000
    """Replaces disks on an instance.
1001

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

1015
    @rtype: string
1016
    @return: job id
1017

1018
    """
1019
    query = [
1020
      ("mode", mode),
1021
      ]
1022

    
1023
    # TODO: Convert to body parameters
1024

    
1025
    if disks is not None:
1026
      _AppendIf(query, True,
1027
                ("disks", ",".join(str(idx) for idx in disks)))
1028

    
1029
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1030
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1031

    
1032
    return self._SendRequest(HTTP_POST,
1033
                             ("/%s/instances/%s/replace-disks" %
1034
                              (GANETI_RAPI_VERSION, instance)), query, None)
1035

    
1036
  def PrepareExport(self, instance, mode):
1037
    """Prepares an instance for an export.
1038

1039
    @type instance: string
1040
    @param instance: Instance name
1041
    @type mode: string
1042
    @param mode: Export mode
1043
    @rtype: string
1044
    @return: Job ID
1045

1046
    """
1047
    query = [("mode", mode)]
1048
    return self._SendRequest(HTTP_PUT,
1049
                             ("/%s/instances/%s/prepare-export" %
1050
                              (GANETI_RAPI_VERSION, instance)), query, None)
1051

    
1052
  def ExportInstance(self, instance, mode, destination, shutdown=None,
1053
                     remove_instance=None,
1054
                     x509_key_name=None, destination_x509_ca=None):
1055
    """Exports an instance.
1056

1057
    @type instance: string
1058
    @param instance: Instance name
1059
    @type mode: string
1060
    @param mode: Export mode
1061
    @rtype: string
1062
    @return: Job ID
1063

1064
    """
1065
    body = {
1066
      "destination": destination,
1067
      "mode": mode,
1068
      }
1069

    
1070
    if shutdown is not None:
1071
      body["shutdown"] = shutdown
1072

    
1073
    if remove_instance is not None:
1074
      body["remove_instance"] = remove_instance
1075

    
1076
    if x509_key_name is not None:
1077
      body["x509_key_name"] = x509_key_name
1078

    
1079
    if destination_x509_ca is not None:
1080
      body["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

    
1101
    if mode is not None:
1102
      body["mode"] = mode
1103

    
1104
    if cleanup is not None:
1105
      body["cleanup"] = cleanup
1106

    
1107
    return self._SendRequest(HTTP_PUT,
1108
                             ("/%s/instances/%s/migrate" %
1109
                              (GANETI_RAPI_VERSION, instance)), None, body)
1110

    
1111
  def FailoverInstance(self, instance, iallocator=None,
1112
                       ignore_consistency=None, target_node=None):
1113
    """Does a failover of an instance.
1114

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

1127
    """
1128
    body = {}
1129

    
1130
    if iallocator is not None:
1131
      body["iallocator"] = iallocator
1132

    
1133
    if ignore_consistency is not None:
1134
      body["ignore_consistency"] = ignore_consistency
1135

    
1136
    if target_node is not None:
1137
      body["target_node"] = target_node
1138

    
1139
    return self._SendRequest(HTTP_PUT,
1140
                             ("/%s/instances/%s/failover" %
1141
                              (GANETI_RAPI_VERSION, instance)), None, body)
1142

    
1143
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1144
    """Changes the name of an instance.
1145

1146
    @type instance: string
1147
    @param instance: Instance name
1148
    @type new_name: string
1149
    @param new_name: New instance name
1150
    @type ip_check: bool
1151
    @param ip_check: Whether to ensure instance's IP address is inactive
1152
    @type name_check: bool
1153
    @param name_check: Whether to ensure instance's name is resolvable
1154
    @rtype: string
1155
    @return: job id
1156

1157
    """
1158
    body = {
1159
      "new_name": new_name,
1160
      }
1161

    
1162
    if ip_check is not None:
1163
      body["ip_check"] = ip_check
1164

    
1165
    if name_check is not None:
1166
      body["name_check"] = name_check
1167

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

    
1172
  def GetInstanceConsole(self, instance):
1173
    """Request information for connecting to instance's console.
1174

1175
    @type instance: string
1176
    @param instance: Instance name
1177
    @rtype: dict
1178
    @return: dictionary containing information about instance's console
1179

1180
    """
1181
    return self._SendRequest(HTTP_GET,
1182
                             ("/%s/instances/%s/console" %
1183
                              (GANETI_RAPI_VERSION, instance)), None, None)
1184

    
1185
  def GetJobs(self):
1186
    """Gets all jobs for the cluster.
1187

1188
    @rtype: list of int
1189
    @return: job ids for the cluster
1190

1191
    """
1192
    return [int(j["id"])
1193
            for j in self._SendRequest(HTTP_GET,
1194
                                       "/%s/jobs" % GANETI_RAPI_VERSION,
1195
                                       None, None)]
1196

    
1197
  def GetJobStatus(self, job_id):
1198
    """Gets the status of a job.
1199

1200
    @type job_id: string
1201
    @param job_id: job id whose status to query
1202

1203
    @rtype: dict
1204
    @return: job status
1205

1206
    """
1207
    return self._SendRequest(HTTP_GET,
1208
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1209
                             None, None)
1210

    
1211
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1212
    """Polls cluster for job status until completion.
1213

1214
    Completion is defined as any of the following states listed in
1215
    L{JOB_STATUS_FINALIZED}.
1216

1217
    @type job_id: string
1218
    @param job_id: job id to watch
1219
    @type period: int
1220
    @param period: how often to poll for status (optional, default 5s)
1221
    @type retries: int
1222
    @param retries: how many time to poll before giving up
1223
                    (optional, default -1 means unlimited)
1224

1225
    @rtype: bool
1226
    @return: C{True} if job succeeded or C{False} if failed/status timeout
1227
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
1228
      possible; L{WaitForJobChange} returns immediately after a job changed and
1229
      does not use polling
1230

1231
    """
1232
    while retries != 0:
1233
      job_result = self.GetJobStatus(job_id)
1234

    
1235
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1236
        return True
1237
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1238
        return False
1239

    
1240
      if period:
1241
        time.sleep(period)
1242

    
1243
      if retries > 0:
1244
        retries -= 1
1245

    
1246
    return False
1247

    
1248
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1249
    """Waits for job changes.
1250

1251
    @type job_id: string
1252
    @param job_id: Job ID for which to wait
1253
    @return: C{None} if no changes have been detected and a dict with two keys,
1254
      C{job_info} and C{log_entries} otherwise.
1255
    @rtype: dict
1256

1257
    """
1258
    body = {
1259
      "fields": fields,
1260
      "previous_job_info": prev_job_info,
1261
      "previous_log_serial": prev_log_serial,
1262
      }
1263

    
1264
    return self._SendRequest(HTTP_GET,
1265
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1266
                             None, body)
1267

    
1268
  def CancelJob(self, job_id, dry_run=False):
1269
    """Cancels a job.
1270

1271
    @type job_id: string
1272
    @param job_id: id of the job to delete
1273
    @type dry_run: bool
1274
    @param dry_run: whether to perform a dry run
1275
    @rtype: tuple
1276
    @return: tuple containing the result, and a message (bool, string)
1277

1278
    """
1279
    query = []
1280
    _AppendDryRunIf(query, dry_run)
1281

    
1282
    return self._SendRequest(HTTP_DELETE,
1283
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1284
                             query, None)
1285

    
1286
  def GetNodes(self, bulk=False):
1287
    """Gets all nodes in the cluster.
1288

1289
    @type bulk: bool
1290
    @param bulk: whether to return all information about all instances
1291

1292
    @rtype: list of dict or str
1293
    @return: if bulk is true, info about nodes in the cluster,
1294
        else list of nodes in the cluster
1295

1296
    """
1297
    query = []
1298
    _AppendIf(query, bulk, ("bulk", 1))
1299

    
1300
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1301
                              query, None)
1302
    if bulk:
1303
      return nodes
1304
    else:
1305
      return [n["id"] for n in nodes]
1306

    
1307
  def GetNode(self, node):
1308
    """Gets information about a node.
1309

1310
    @type node: str
1311
    @param node: node whose info to return
1312

1313
    @rtype: dict
1314
    @return: info about the node
1315

1316
    """
1317
    return self._SendRequest(HTTP_GET,
1318
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1319
                             None, None)
1320

    
1321
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1322
                   dry_run=False, early_release=None,
1323
                   mode=None, accept_old=False):
1324
    """Evacuates instances from a Ganeti node.
1325

1326
    @type node: str
1327
    @param node: node to evacuate
1328
    @type iallocator: str or None
1329
    @param iallocator: instance allocator to use
1330
    @type remote_node: str
1331
    @param remote_node: node to evaucate to
1332
    @type dry_run: bool
1333
    @param dry_run: whether to perform a dry run
1334
    @type early_release: bool
1335
    @param early_release: whether to enable parallelization
1336
    @type mode: string
1337
    @param mode: Node evacuation mode
1338
    @type accept_old: bool
1339
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1340
        results
1341

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

1348
    @raises GanetiApiError: if an iallocator and remote_node are both
1349
        specified
1350

1351
    """
1352
    if iallocator and remote_node:
1353
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1354

    
1355
    query = []
1356
    _AppendDryRunIf(query, dry_run)
1357

    
1358
    if _NODE_EVAC_RES1 in self.GetFeatures():
1359
      # Server supports body parameters
1360
      body = {}
1361

    
1362
      if iallocator is not None:
1363
        body["iallocator"] = iallocator
1364
      if remote_node is not None:
1365
        body["remote_node"] = remote_node
1366
      if early_release is not None:
1367
        body["early_release"] = early_release
1368
      if mode is not None:
1369
        body["mode"] = mode
1370
    else:
1371
      # Pre-2.5 request format
1372
      body = None
1373

    
1374
      if not accept_old:
1375
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1376
                             " not accept old-style results (parameter"
1377
                             " accept_old)")
1378

    
1379
      # Pre-2.5 servers can only evacuate secondaries
1380
      if mode is not None and mode != NODE_EVAC_SEC:
1381
        raise GanetiApiError("Server can only evacuate secondary instances")
1382

    
1383
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1384
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1385
      _AppendIf(query, early_release, ("early_release", 1))
1386

    
1387
    return self._SendRequest(HTTP_POST,
1388
                             ("/%s/nodes/%s/evacuate" %
1389
                              (GANETI_RAPI_VERSION, node)), query, body)
1390

    
1391
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1392
                  target_node=None):
1393
    """Migrates all primary instances from a node.
1394

1395
    @type node: str
1396
    @param node: node to migrate
1397
    @type mode: string
1398
    @param mode: if passed, it will overwrite the live migration type,
1399
        otherwise the hypervisor default will be used
1400
    @type dry_run: bool
1401
    @param dry_run: whether to perform a dry run
1402
    @type iallocator: string
1403
    @param iallocator: instance allocator to use
1404
    @type target_node: string
1405
    @param target_node: Target node for shared-storage instances
1406

1407
    @rtype: string
1408
    @return: job id
1409

1410
    """
1411
    query = []
1412
    _AppendDryRunIf(query, dry_run)
1413

    
1414
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1415
      body = {}
1416

    
1417
      if mode is not None:
1418
        body["mode"] = mode
1419
      if iallocator is not None:
1420
        body["iallocator"] = iallocator
1421
      if target_node is not None:
1422
        body["target_node"] = target_node
1423

    
1424
      assert len(query) <= 1
1425

    
1426
      return self._SendRequest(HTTP_POST,
1427
                               ("/%s/nodes/%s/migrate" %
1428
                                (GANETI_RAPI_VERSION, node)), query, body)
1429
    else:
1430
      # Use old request format
1431
      if target_node is not None:
1432
        raise GanetiApiError("Server does not support specifying target node"
1433
                             " for node migration")
1434

    
1435
      _AppendIf(query, mode is not None, ("mode", mode))
1436

    
1437
      return self._SendRequest(HTTP_POST,
1438
                               ("/%s/nodes/%s/migrate" %
1439
                                (GANETI_RAPI_VERSION, node)), query, None)
1440

    
1441
  def GetNodeRole(self, node):
1442
    """Gets the current role for a node.
1443

1444
    @type node: str
1445
    @param node: node whose role to return
1446

1447
    @rtype: str
1448
    @return: the current role for a node
1449

1450
    """
1451
    return self._SendRequest(HTTP_GET,
1452
                             ("/%s/nodes/%s/role" %
1453
                              (GANETI_RAPI_VERSION, node)), None, None)
1454

    
1455
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1456
    """Sets the role for a node.
1457

1458
    @type node: str
1459
    @param node: the node whose role to set
1460
    @type role: str
1461
    @param role: the role to set for the node
1462
    @type force: bool
1463
    @param force: whether to force the role change
1464
    @type auto_promote: bool
1465
    @param auto_promote: Whether node(s) should be promoted to master candidate
1466
                         if necessary
1467

1468
    @rtype: string
1469
    @return: job id
1470

1471
    """
1472
    query = []
1473
    _AppendForceIf(query, force)
1474
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1475

    
1476
    return self._SendRequest(HTTP_PUT,
1477
                             ("/%s/nodes/%s/role" %
1478
                              (GANETI_RAPI_VERSION, node)), query, role)
1479

    
1480
  def PowercycleNode(self, node, force=False):
1481
    """Powercycles a node.
1482

1483
    @type node: string
1484
    @param node: Node name
1485
    @type force: bool
1486
    @param force: Whether to force the operation
1487
    @rtype: string
1488
    @return: job id
1489

1490
    """
1491
    query = []
1492
    _AppendForceIf(query, force)
1493

    
1494
    return self._SendRequest(HTTP_POST,
1495
                             ("/%s/nodes/%s/powercycle" %
1496
                              (GANETI_RAPI_VERSION, node)), query, None)
1497

    
1498
  def ModifyNode(self, node, **kwargs):
1499
    """Modifies a node.
1500

1501
    More details for parameters can be found in the RAPI documentation.
1502

1503
    @type node: string
1504
    @param node: Node name
1505
    @rtype: string
1506
    @return: job id
1507

1508
    """
1509
    return self._SendRequest(HTTP_POST,
1510
                             ("/%s/nodes/%s/modify" %
1511
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1512

    
1513
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1514
    """Gets the storage units for a node.
1515

1516
    @type node: str
1517
    @param node: the node whose storage units to return
1518
    @type storage_type: str
1519
    @param storage_type: storage type whose units to return
1520
    @type output_fields: str
1521
    @param output_fields: storage type fields to return
1522

1523
    @rtype: string
1524
    @return: job id where results can be retrieved
1525

1526
    """
1527
    query = [
1528
      ("storage_type", storage_type),
1529
      ("output_fields", output_fields),
1530
      ]
1531

    
1532
    return self._SendRequest(HTTP_GET,
1533
                             ("/%s/nodes/%s/storage" %
1534
                              (GANETI_RAPI_VERSION, node)), query, None)
1535

    
1536
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1537
    """Modifies parameters of storage units on the node.
1538

1539
    @type node: str
1540
    @param node: node whose storage units to modify
1541
    @type storage_type: str
1542
    @param storage_type: storage type whose units to modify
1543
    @type name: str
1544
    @param name: name of the storage unit
1545
    @type allocatable: bool or None
1546
    @param allocatable: Whether to set the "allocatable" flag on the storage
1547
                        unit (None=no modification, True=set, False=unset)
1548

1549
    @rtype: string
1550
    @return: job id
1551

1552
    """
1553
    query = [
1554
      ("storage_type", storage_type),
1555
      ("name", name),
1556
      ]
1557

    
1558
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1559

    
1560
    return self._SendRequest(HTTP_PUT,
1561
                             ("/%s/nodes/%s/storage/modify" %
1562
                              (GANETI_RAPI_VERSION, node)), query, None)
1563

    
1564
  def RepairNodeStorageUnits(self, node, storage_type, name):
1565
    """Repairs a storage unit on the node.
1566

1567
    @type node: str
1568
    @param node: node whose storage units to repair
1569
    @type storage_type: str
1570
    @param storage_type: storage type to repair
1571
    @type name: str
1572
    @param name: name of the storage unit to repair
1573

1574
    @rtype: string
1575
    @return: job id
1576

1577
    """
1578
    query = [
1579
      ("storage_type", storage_type),
1580
      ("name", name),
1581
      ]
1582

    
1583
    return self._SendRequest(HTTP_PUT,
1584
                             ("/%s/nodes/%s/storage/repair" %
1585
                              (GANETI_RAPI_VERSION, node)), query, None)
1586

    
1587
  def GetNodeTags(self, node):
1588
    """Gets the tags for a node.
1589

1590
    @type node: str
1591
    @param node: node whose tags to return
1592

1593
    @rtype: list of str
1594
    @return: tags for the node
1595

1596
    """
1597
    return self._SendRequest(HTTP_GET,
1598
                             ("/%s/nodes/%s/tags" %
1599
                              (GANETI_RAPI_VERSION, node)), None, None)
1600

    
1601
  def AddNodeTags(self, node, tags, dry_run=False):
1602
    """Adds tags to a node.
1603

1604
    @type node: str
1605
    @param node: node to add tags to
1606
    @type tags: list of str
1607
    @param tags: tags to add to the node
1608
    @type dry_run: bool
1609
    @param dry_run: whether to perform a dry run
1610

1611
    @rtype: string
1612
    @return: job id
1613

1614
    """
1615
    query = [("tag", t) for t in tags]
1616
    _AppendDryRunIf(query, dry_run)
1617

    
1618
    return self._SendRequest(HTTP_PUT,
1619
                             ("/%s/nodes/%s/tags" %
1620
                              (GANETI_RAPI_VERSION, node)), query, tags)
1621

    
1622
  def DeleteNodeTags(self, node, tags, dry_run=False):
1623
    """Delete tags from a node.
1624

1625
    @type node: str
1626
    @param node: node to remove tags from
1627
    @type tags: list of str
1628
    @param tags: tags to remove from the node
1629
    @type dry_run: bool
1630
    @param dry_run: whether to perform a dry run
1631

1632
    @rtype: string
1633
    @return: job id
1634

1635
    """
1636
    query = [("tag", t) for t in tags]
1637
    _AppendDryRunIf(query, dry_run)
1638

    
1639
    return self._SendRequest(HTTP_DELETE,
1640
                             ("/%s/nodes/%s/tags" %
1641
                              (GANETI_RAPI_VERSION, node)), query, None)
1642

    
1643
  def GetGroups(self, bulk=False):
1644
    """Gets all node groups in the cluster.
1645

1646
    @type bulk: bool
1647
    @param bulk: whether to return all information about the groups
1648

1649
    @rtype: list of dict or str
1650
    @return: if bulk is true, a list of dictionaries with info about all node
1651
        groups in the cluster, else a list of names of those node groups
1652

1653
    """
1654
    query = []
1655
    _AppendIf(query, bulk, ("bulk", 1))
1656

    
1657
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1658
                               query, None)
1659
    if bulk:
1660
      return groups
1661
    else:
1662
      return [g["name"] for g in groups]
1663

    
1664
  def GetGroup(self, group):
1665
    """Gets information about a node group.
1666

1667
    @type group: str
1668
    @param group: name of the node group whose info to return
1669

1670
    @rtype: dict
1671
    @return: info about the node group
1672

1673
    """
1674
    return self._SendRequest(HTTP_GET,
1675
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1676
                             None, None)
1677

    
1678
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1679
    """Creates a new node group.
1680

1681
    @type name: str
1682
    @param name: the name of node group to create
1683
    @type alloc_policy: str
1684
    @param alloc_policy: the desired allocation policy for the group, if any
1685
    @type dry_run: bool
1686
    @param dry_run: whether to peform a dry run
1687

1688
    @rtype: string
1689
    @return: job id
1690

1691
    """
1692
    query = []
1693
    _AppendDryRunIf(query, dry_run)
1694

    
1695
    body = {
1696
      "name": name,
1697
      "alloc_policy": alloc_policy
1698
      }
1699

    
1700
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1701
                             query, body)
1702

    
1703
  def ModifyGroup(self, group, **kwargs):
1704
    """Modifies a node group.
1705

1706
    More details for parameters can be found in the RAPI documentation.
1707

1708
    @type group: string
1709
    @param group: Node group name
1710
    @rtype: string
1711
    @return: job id
1712

1713
    """
1714
    return self._SendRequest(HTTP_PUT,
1715
                             ("/%s/groups/%s/modify" %
1716
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
1717

    
1718
  def DeleteGroup(self, group, dry_run=False):
1719
    """Deletes a node group.
1720

1721
    @type group: str
1722
    @param group: the node group to delete
1723
    @type dry_run: bool
1724
    @param dry_run: whether to peform a dry run
1725

1726
    @rtype: string
1727
    @return: job id
1728

1729
    """
1730
    query = []
1731
    _AppendDryRunIf(query, dry_run)
1732

    
1733
    return self._SendRequest(HTTP_DELETE,
1734
                             ("/%s/groups/%s" %
1735
                              (GANETI_RAPI_VERSION, group)), query, None)
1736

    
1737
  def RenameGroup(self, group, new_name):
1738
    """Changes the name of a node group.
1739

1740
    @type group: string
1741
    @param group: Node group name
1742
    @type new_name: string
1743
    @param new_name: New node group name
1744

1745
    @rtype: string
1746
    @return: job id
1747

1748
    """
1749
    body = {
1750
      "new_name": new_name,
1751
      }
1752

    
1753
    return self._SendRequest(HTTP_PUT,
1754
                             ("/%s/groups/%s/rename" %
1755
                              (GANETI_RAPI_VERSION, group)), None, body)
1756

    
1757
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1758
    """Assigns nodes to a group.
1759

1760
    @type group: string
1761
    @param group: Node gropu name
1762
    @type nodes: list of strings
1763
    @param nodes: List of nodes to assign to the group
1764

1765
    @rtype: string
1766
    @return: job id
1767

1768
    """
1769
    query = []
1770
    _AppendForceIf(query, force)
1771
    _AppendDryRunIf(query, dry_run)
1772

    
1773
    body = {
1774
      "nodes": nodes,
1775
      }
1776

    
1777
    return self._SendRequest(HTTP_PUT,
1778
                             ("/%s/groups/%s/assign-nodes" %
1779
                             (GANETI_RAPI_VERSION, group)), query, body)
1780

    
1781
  def GetGroupTags(self, group):
1782
    """Gets tags for a node group.
1783

1784
    @type group: string
1785
    @param group: Node group whose tags to return
1786

1787
    @rtype: list of strings
1788
    @return: tags for the group
1789

1790
    """
1791
    return self._SendRequest(HTTP_GET,
1792
                             ("/%s/groups/%s/tags" %
1793
                              (GANETI_RAPI_VERSION, group)), None, None)
1794

    
1795
  def AddGroupTags(self, group, tags, dry_run=False):
1796
    """Adds tags to a node group.
1797

1798
    @type group: str
1799
    @param group: group to add tags to
1800
    @type tags: list of string
1801
    @param tags: tags to add to the group
1802
    @type dry_run: bool
1803
    @param dry_run: whether to perform a dry run
1804

1805
    @rtype: string
1806
    @return: job id
1807

1808
    """
1809
    query = [("tag", t) for t in tags]
1810
    _AppendDryRunIf(query, dry_run)
1811

    
1812
    return self._SendRequest(HTTP_PUT,
1813
                             ("/%s/groups/%s/tags" %
1814
                              (GANETI_RAPI_VERSION, group)), query, None)
1815

    
1816
  def DeleteGroupTags(self, group, tags, dry_run=False):
1817
    """Deletes tags from a node group.
1818

1819
    @type group: str
1820
    @param group: group to delete tags from
1821
    @type tags: list of string
1822
    @param tags: tags to delete
1823
    @type dry_run: bool
1824
    @param dry_run: whether to perform a dry run
1825
    @rtype: string
1826
    @return: job id
1827

1828
    """
1829
    query = [("tag", t) for t in tags]
1830
    _AppendDryRunIf(query, dry_run)
1831

    
1832
    return self._SendRequest(HTTP_DELETE,
1833
                             ("/%s/groups/%s/tags" %
1834
                              (GANETI_RAPI_VERSION, group)), query, None)
1835

    
1836
  def Query(self, what, fields, qfilter=None):
1837
    """Retrieves information about resources.
1838

1839
    @type what: string
1840
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1841
    @type fields: list of string
1842
    @param fields: Requested fields
1843
    @type qfilter: None or list
1844
    @param qfilter: Query filter
1845

1846
    @rtype: string
1847
    @return: job id
1848

1849
    """
1850
    body = {
1851
      "fields": fields,
1852
      }
1853

    
1854
    if qfilter is not None:
1855
      body["qfilter"] = qfilter
1856
      # TODO: remove this after 2.7
1857
      body["filter"] = qfilter
1858

    
1859
    return self._SendRequest(HTTP_PUT,
1860
                             ("/%s/query/%s" %
1861
                              (GANETI_RAPI_VERSION, what)), None, body)
1862

    
1863
  def QueryFields(self, what, fields=None):
1864
    """Retrieves available fields for a resource.
1865

1866
    @type what: string
1867
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1868
    @type fields: list of string
1869
    @param fields: Requested fields
1870

1871
    @rtype: string
1872
    @return: job id
1873

1874
    """
1875
    query = []
1876

    
1877
    if fields is not None:
1878
      _AppendIf(query, True, ("fields", ",".join(fields)))
1879

    
1880
    return self._SendRequest(HTTP_GET,
1881
                             ("/%s/query/%s/fields" %
1882
                              (GANETI_RAPI_VERSION, what)), query, None)