Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ 75f53ffe

History | View | Annotate | Download (29.5 kB)

1
#
2
#
3

    
4
# Copyright (C) 2010 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
import httplib
25
import urllib2
26
import logging
27
import simplejson
28
import socket
29
import urllib
30
import OpenSSL
31
import distutils.version
32

    
33

    
34
GANETI_RAPI_PORT = 5080
35
GANETI_RAPI_VERSION = 2
36

    
37
HTTP_DELETE = "DELETE"
38
HTTP_GET = "GET"
39
HTTP_PUT = "PUT"
40
HTTP_POST = "POST"
41
HTTP_OK = 200
42
HTTP_APP_JSON = "application/json"
43

    
44
REPLACE_DISK_PRI = "replace_on_primary"
45
REPLACE_DISK_SECONDARY = "replace_on_secondary"
46
REPLACE_DISK_CHG = "replace_new_secondary"
47
REPLACE_DISK_AUTO = "replace_auto"
48
VALID_REPLACEMENT_MODES = frozenset([
49
  REPLACE_DISK_PRI,
50
  REPLACE_DISK_SECONDARY,
51
  REPLACE_DISK_CHG,
52
  REPLACE_DISK_AUTO,
53
  ])
54
VALID_NODE_ROLES = frozenset([
55
  "drained", "master", "master-candidate", "offline", "regular",
56
  ])
57

    
58

    
59
class Error(Exception):
60
  """Base error class for this module.
61

62
  """
63
  pass
64

    
65

    
66
class CertificateError(Error):
67
  """Raised when a problem is found with the SSL certificate.
68

69
  """
70
  pass
71

    
72

    
73
class GanetiApiError(Error):
74
  """Generic error raised from Ganeti API.
75

76
  """
77
  def __init__(self, msg, code=None):
78
    Error.__init__(self, msg)
79
    self.code = code
80

    
81

    
82
class InvalidReplacementMode(Error):
83
  """Raised when an invalid disk replacement mode is attempted.
84

85
  """
86
  pass
87

    
88

    
89
class InvalidNodeRole(Error):
90
  """Raised when an invalid node role is used.
91

92
  """
93
  pass
94

    
95

    
96
def FormatX509Name(x509_name):
97
  """Formats an X509 name.
98

99
  @type x509_name: OpenSSL.crypto.X509Name
100

101
  """
102
  try:
103
    # Only supported in pyOpenSSL 0.7 and above
104
    get_components_fn = x509_name.get_components
105
  except AttributeError:
106
    return repr(x509_name)
107
  else:
108
    return "".join("/%s=%s" % (name, value)
109
                   for name, value in get_components_fn())
110

    
111

    
112
class CertAuthorityVerify:
113
  """Certificate verificator for SSL context.
114

115
  Configures SSL context to verify server's certificate.
116

117
  """
118
  _CAPATH_MINVERSION = "0.9"
119
  _DEFVFYPATHS_MINVERSION = "0.9"
120

    
121
  _PYOPENSSL_VERSION = OpenSSL.__version__
122
  _PARSED_PYOPENSSL_VERSION = distutils.version.LooseVersion(_PYOPENSSL_VERSION)
123

    
124
  _SUPPORT_CAPATH = (_PARSED_PYOPENSSL_VERSION >= _CAPATH_MINVERSION)
125
  _SUPPORT_DEFVFYPATHS = (_PARSED_PYOPENSSL_VERSION >= _DEFVFYPATHS_MINVERSION)
126

    
127
  def __init__(self, cafile=None, capath=None, use_default_verify_paths=False):
128
    """Initializes this class.
129

130
    @type cafile: string
131
    @param cafile: In which file we can find the certificates
132
    @type capath: string
133
    @param capath: In which directory we can find the certificates
134
    @type use_default_verify_paths: bool
135
    @param use_default_verify_paths: Whether the platform provided CA
136
                                     certificates are to be used for
137
                                     verification purposes
138

139
    """
140
    self._cafile = cafile
141
    self._capath = capath
142
    self._use_default_verify_paths = use_default_verify_paths
143

    
144
    if self._capath is not None and not self._SUPPORT_CAPATH:
145
      raise Error(("PyOpenSSL %s has no support for a CA directory,"
146
                   " version %s or above is required") %
147
                  (self._PYOPENSSL_VERSION, self._CAPATH_MINVERSION))
148

    
149
    if self._use_default_verify_paths and not self._SUPPORT_DEFVFYPATHS:
150
      raise Error(("PyOpenSSL %s has no support for using default verification"
151
                   " paths, version %s or above is required") %
152
                  (self._PYOPENSSL_VERSION, self._DEFVFYPATHS_MINVERSION))
153

    
154
  @staticmethod
155
  def _VerifySslCertCb(logger, _, cert, errnum, errdepth, ok):
156
    """Callback for SSL certificate verification.
157

158
    @param logger: Logging object
159

160
    """
161
    if ok:
162
      log_fn = logger.debug
163
    else:
164
      log_fn = logger.error
165

    
166
    log_fn("Verifying SSL certificate at depth %s, subject '%s', issuer '%s'",
167
           errdepth, FormatX509Name(cert.get_subject()),
168
           FormatX509Name(cert.get_issuer()))
169

    
170
    if not ok:
171
      try:
172
        # Only supported in pyOpenSSL 0.7 and above
173
        # pylint: disable-msg=E1101
174
        fn = OpenSSL.crypto.X509_verify_cert_error_string
175
      except AttributeError:
176
        errmsg = ""
177
      else:
178
        errmsg = ":%s" % fn(errnum)
179

    
180
      logger.error("verify error:num=%s%s", errnum, errmsg)
181

    
182
    return ok
183

    
184
  def __call__(self, ctx, logger):
185
    """Configures an SSL context to verify certificates.
186

187
    @type ctx: OpenSSL.SSL.Context
188
    @param ctx: SSL context
189

190
    """
191
    if self._use_default_verify_paths:
192
      ctx.set_default_verify_paths()
193

    
194
    if self._cafile or self._capath:
195
      if self._SUPPORT_CAPATH:
196
        ctx.load_verify_locations(self._cafile, self._capath)
197
      else:
198
        ctx.load_verify_locations(self._cafile)
199

    
200
    ctx.set_verify(OpenSSL.SSL.VERIFY_PEER,
201
                   lambda conn, cert, errnum, errdepth, ok: \
202
                     self._VerifySslCertCb(logger, conn, cert,
203
                                           errnum, errdepth, ok))
204

    
205

    
206
class _HTTPSConnectionOpenSSL(httplib.HTTPSConnection):
207
  """HTTPS Connection handler that verifies the SSL certificate.
208

209
  """
210
  def __init__(self, *args, **kwargs):
211
    """Initializes this class.
212

213
    """
214
    httplib.HTTPSConnection.__init__(self, *args, **kwargs)
215
    self._logger = None
216
    self._config_ssl_verification = None
217

    
218
  def Setup(self, logger, config_ssl_verification):
219
    """Sets the SSL verification config function.
220

221
    @param logger: Logging object
222
    @type config_ssl_verification: callable
223

224
    """
225
    assert self._logger is None
226
    assert self._config_ssl_verification is None
227

    
228
    self._logger = logger
229
    self._config_ssl_verification = config_ssl_verification
230

    
231
  def connect(self):
232
    """Connect to the server specified when the object was created.
233

234
    This ensures that SSL certificates are verified.
235

236
    """
237
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
238

    
239
    ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
240
    ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
241

    
242
    if self._config_ssl_verification:
243
      self._config_ssl_verification(ctx, self._logger)
244

    
245
    ssl = OpenSSL.SSL.Connection(ctx, sock)
246
    ssl.connect((self.host, self.port))
247

    
248
    self.sock = httplib.FakeSocket(sock, ssl)
249

    
250

    
251
class _HTTPSHandler(urllib2.HTTPSHandler):
252
  def __init__(self, logger, config_ssl_verification):
253
    """Initializes this class.
254

255
    @param logger: Logging object
256
    @type config_ssl_verification: callable
257
    @param config_ssl_verification: Function to configure SSL context for
258
                                    certificate verification
259

260
    """
261
    urllib2.HTTPSHandler.__init__(self)
262
    self._logger = logger
263
    self._config_ssl_verification = config_ssl_verification
264

    
265
  def _CreateHttpsConnection(self, *args, **kwargs):
266
    """Wrapper around L{_HTTPSConnectionOpenSSL} to add SSL verification.
267

268
    This wrapper is necessary provide a compatible API to urllib2.
269

270
    """
271
    conn = _HTTPSConnectionOpenSSL(*args, **kwargs)
272
    conn.Setup(self._logger, self._config_ssl_verification)
273
    return conn
274

    
275
  def https_open(self, req):
276
    """Creates HTTPS connection.
277

278
    Called by urllib2.
279

280
    """
281
    return self.do_open(self._CreateHttpsConnection, req)
282

    
283

    
284
class _RapiRequest(urllib2.Request):
285
  def __init__(self, method, url, headers, data):
286
    """Initializes this class.
287

288
    """
289
    urllib2.Request.__init__(self, url, data=data, headers=headers)
290
    self._method = method
291

    
292
  def get_method(self):
293
    """Returns the HTTP request method.
294

295
    """
296
    return self._method
297

    
298

    
299
class GanetiRapiClient(object):
300
  """Ganeti RAPI client.
301

302
  """
303
  USER_AGENT = "Ganeti RAPI Client"
304
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
305

    
306
  def __init__(self, host, port=GANETI_RAPI_PORT,
307
               username=None, password=None,
308
               config_ssl_verification=None, ignore_proxy=False,
309
               logger=logging):
310
    """Constructor.
311

312
    @type host: string
313
    @param host: the ganeti cluster master to interact with
314
    @type port: int
315
    @param port: the port on which the RAPI is running (default is 5080)
316
    @type username: string
317
    @param username: the username to connect with
318
    @type password: string
319
    @param password: the password to connect with
320
    @type config_ssl_verification: callable
321
    @param config_ssl_verification: Function to configure SSL context for
322
                                    certificate verification
323
    @type ignore_proxy: bool
324
    @param ignore_proxy: Whether to ignore proxy settings
325
    @param logger: Logging object
326

327
    """
328
    self._host = host
329
    self._port = port
330
    self._logger = logger
331

    
332
    self._base_url = "https://%s:%s" % (host, port)
333

    
334
    handlers = [_HTTPSHandler(self._logger, config_ssl_verification)]
335

    
336
    if username is not None:
337
      pwmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
338
      pwmgr.add_password(None, self._base_url, username, password)
339
      handlers.append(urllib2.HTTPBasicAuthHandler(pwmgr))
340
    elif password:
341
      raise Error("Specified password without username")
342

    
343
    if ignore_proxy:
344
      handlers.append(urllib2.ProxyHandler({}))
345

    
346
    self._http = urllib2.build_opener(*handlers) # pylint: disable-msg=W0142
347

    
348
    self._headers = {
349
      "Accept": HTTP_APP_JSON,
350
      "Content-type": HTTP_APP_JSON,
351
      "User-Agent": self.USER_AGENT,
352
      }
353

    
354
  def _SendRequest(self, method, path, query, content):
355
    """Sends an HTTP request.
356

357
    This constructs a full URL, encodes and decodes HTTP bodies, and
358
    handles invalid responses in a pythonic way.
359

360
    @type method: string
361
    @param method: HTTP method to use
362
    @type path: string
363
    @param path: HTTP URL path
364
    @type query: list of two-tuples
365
    @param query: query arguments to pass to urllib.urlencode
366
    @type content: str or None
367
    @param content: HTTP body content
368

369
    @rtype: str
370
    @return: JSON-Decoded response
371

372
    @raises CertificateError: If an invalid SSL certificate is found
373
    @raises GanetiApiError: If an invalid response is returned
374

375
    """
376
    assert path.startswith("/")
377

    
378
    if content:
379
      encoded_content = self._json_encoder.encode(content)
380
    else:
381
      encoded_content = None
382

    
383
    # Build URL
384
    url = [self._base_url, path]
385
    if query:
386
      url.append("?")
387
      url.append(urllib.urlencode(query))
388

    
389
    req = _RapiRequest(method, "".join(url), self._headers, encoded_content)
390

    
391
    try:
392
      resp = self._http.open(req)
393
      encoded_response_content = resp.read()
394
    except (OpenSSL.SSL.Error, OpenSSL.crypto.Error), err:
395
      raise CertificateError("SSL issue: %s" % err)
396

    
397
    if encoded_response_content:
398
      response_content = simplejson.loads(encoded_response_content)
399
    else:
400
      response_content = None
401

    
402
    # TODO: Are there other status codes that are valid? (redirect?)
403
    if resp.code != HTTP_OK:
404
      if isinstance(response_content, dict):
405
        msg = ("%s %s: %s" %
406
               (response_content["code"],
407
                response_content["message"],
408
                response_content["explain"]))
409
      else:
410
        msg = str(response_content)
411

    
412
      raise GanetiApiError(msg, code=resp.code)
413

    
414
    return response_content
415

    
416
  def GetVersion(self):
417
    """Gets the Remote API version running on the cluster.
418

419
    @rtype: int
420
    @return: Ganeti Remote API version
421

422
    """
423
    return self._SendRequest(HTTP_GET, "/version", None, None)
424

    
425
  def GetOperatingSystems(self):
426
    """Gets the Operating Systems running in the Ganeti cluster.
427

428
    @rtype: list of str
429
    @return: operating systems
430

431
    """
432
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
433
                             None, None)
434

    
435
  def GetInfo(self):
436
    """Gets info about the cluster.
437

438
    @rtype: dict
439
    @return: information about the cluster
440

441
    """
442
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
443
                             None, None)
444

    
445
  def GetClusterTags(self):
446
    """Gets the cluster tags.
447

448
    @rtype: list of str
449
    @return: cluster tags
450

451
    """
452
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
453
                             None, None)
454

    
455
  def AddClusterTags(self, tags, dry_run=False):
456
    """Adds tags to the cluster.
457

458
    @type tags: list of str
459
    @param tags: tags to add to the cluster
460
    @type dry_run: bool
461
    @param dry_run: whether to perform a dry run
462

463
    @rtype: int
464
    @return: job id
465

466
    """
467
    query = [("tag", t) for t in tags]
468
    if dry_run:
469
      query.append(("dry-run", 1))
470

    
471
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
472
                             query, None)
473

    
474
  def DeleteClusterTags(self, tags, dry_run=False):
475
    """Deletes tags from the cluster.
476

477
    @type tags: list of str
478
    @param tags: tags to delete
479
    @type dry_run: bool
480
    @param dry_run: whether to perform a dry run
481

482
    """
483
    query = [("tag", t) for t in tags]
484
    if dry_run:
485
      query.append(("dry-run", 1))
486

    
487
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
488
                             query, None)
489

    
490
  def GetInstances(self, bulk=False):
491
    """Gets information about instances on the cluster.
492

493
    @type bulk: bool
494
    @param bulk: whether to return all information about all instances
495

496
    @rtype: list of dict or list of str
497
    @return: if bulk is True, info about the instances, else a list of instances
498

499
    """
500
    query = []
501
    if bulk:
502
      query.append(("bulk", 1))
503

    
504
    instances = self._SendRequest(HTTP_GET,
505
                                  "/%s/instances" % GANETI_RAPI_VERSION,
506
                                  query, None)
507
    if bulk:
508
      return instances
509
    else:
510
      return [i["id"] for i in instances]
511

    
512
  def GetInstanceInfo(self, instance):
513
    """Gets information about an instance.
514

515
    @type instance: str
516
    @param instance: instance whose info to return
517

518
    @rtype: dict
519
    @return: info about the instance
520

521
    """
522
    return self._SendRequest(HTTP_GET,
523
                             ("/%s/instances/%s" %
524
                              (GANETI_RAPI_VERSION, instance)), None, None)
525

    
526
  def CreateInstance(self, dry_run=False):
527
    """Creates a new instance.
528

529
    @type dry_run: bool
530
    @param dry_run: whether to perform a dry run
531

532
    @rtype: int
533
    @return: job id
534

535
    """
536
    # TODO: Pass arguments needed to actually create an instance.
537
    query = []
538
    if dry_run:
539
      query.append(("dry-run", 1))
540

    
541
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
542
                             query, None)
543

    
544
  def DeleteInstance(self, instance, dry_run=False):
545
    """Deletes an instance.
546

547
    @type instance: str
548
    @param instance: the instance to delete
549

550
    @rtype: int
551
    @return: job id
552

553
    """
554
    query = []
555
    if dry_run:
556
      query.append(("dry-run", 1))
557

    
558
    return self._SendRequest(HTTP_DELETE,
559
                             ("/%s/instances/%s" %
560
                              (GANETI_RAPI_VERSION, instance)), query, None)
561

    
562
  def GetInstanceTags(self, instance):
563
    """Gets tags for an instance.
564

565
    @type instance: str
566
    @param instance: instance whose tags to return
567

568
    @rtype: list of str
569
    @return: tags for the instance
570

571
    """
572
    return self._SendRequest(HTTP_GET,
573
                             ("/%s/instances/%s/tags" %
574
                              (GANETI_RAPI_VERSION, instance)), None, None)
575

    
576
  def AddInstanceTags(self, instance, tags, dry_run=False):
577
    """Adds tags to an instance.
578

579
    @type instance: str
580
    @param instance: instance to add tags to
581
    @type tags: list of str
582
    @param tags: tags to add to the instance
583
    @type dry_run: bool
584
    @param dry_run: whether to perform a dry run
585

586
    @rtype: int
587
    @return: job id
588

589
    """
590
    query = [("tag", t) for t in tags]
591
    if dry_run:
592
      query.append(("dry-run", 1))
593

    
594
    return self._SendRequest(HTTP_PUT,
595
                             ("/%s/instances/%s/tags" %
596
                              (GANETI_RAPI_VERSION, instance)), query, None)
597

    
598
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
599
    """Deletes tags from an instance.
600

601
    @type instance: str
602
    @param instance: instance to delete tags from
603
    @type tags: list of str
604
    @param tags: tags to delete
605
    @type dry_run: bool
606
    @param dry_run: whether to perform a dry run
607

608
    """
609
    query = [("tag", t) for t in tags]
610
    if dry_run:
611
      query.append(("dry-run", 1))
612

    
613
    return self._SendRequest(HTTP_DELETE,
614
                             ("/%s/instances/%s/tags" %
615
                              (GANETI_RAPI_VERSION, instance)), query, None)
616

    
617
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
618
                     dry_run=False):
619
    """Reboots an instance.
620

621
    @type instance: str
622
    @param instance: instance to rebot
623
    @type reboot_type: str
624
    @param reboot_type: one of: hard, soft, full
625
    @type ignore_secondaries: bool
626
    @param ignore_secondaries: if True, ignores errors for the secondary node
627
        while re-assembling disks (in hard-reboot mode only)
628
    @type dry_run: bool
629
    @param dry_run: whether to perform a dry run
630

631
    """
632
    query = []
633
    if reboot_type:
634
      query.append(("type", reboot_type))
635
    if ignore_secondaries is not None:
636
      query.append(("ignore_secondaries", ignore_secondaries))
637
    if dry_run:
638
      query.append(("dry-run", 1))
639

    
640
    return self._SendRequest(HTTP_POST,
641
                             ("/%s/instances/%s/reboot" %
642
                              (GANETI_RAPI_VERSION, instance)), query, None)
643

    
644
  def ShutdownInstance(self, instance, dry_run=False):
645
    """Shuts down an instance.
646

647
    @type instance: str
648
    @param instance: the instance to shut down
649
    @type dry_run: bool
650
    @param dry_run: whether to perform a dry run
651

652
    """
653
    query = []
654
    if dry_run:
655
      query.append(("dry-run", 1))
656

    
657
    return self._SendRequest(HTTP_PUT,
658
                             ("/%s/instances/%s/shutdown" %
659
                              (GANETI_RAPI_VERSION, instance)), query, None)
660

    
661
  def StartupInstance(self, instance, dry_run=False):
662
    """Starts up an instance.
663

664
    @type instance: str
665
    @param instance: the instance to start up
666
    @type dry_run: bool
667
    @param dry_run: whether to perform a dry run
668

669
    """
670
    query = []
671
    if dry_run:
672
      query.append(("dry-run", 1))
673

    
674
    return self._SendRequest(HTTP_PUT,
675
                             ("/%s/instances/%s/startup" %
676
                              (GANETI_RAPI_VERSION, instance)), query, None)
677

    
678
  def ReinstallInstance(self, instance, os, no_startup=False):
679
    """Reinstalls an instance.
680

681
    @type instance: str
682
    @param instance: the instance to reinstall
683
    @type os: str
684
    @param os: the os to reinstall
685
    @type no_startup: bool
686
    @param no_startup: whether to start the instance automatically
687

688
    """
689
    query = [("os", os)]
690
    if no_startup:
691
      query.append(("nostartup", 1))
692
    return self._SendRequest(HTTP_POST,
693
                             ("/%s/instances/%s/reinstall" %
694
                              (GANETI_RAPI_VERSION, instance)), query, None)
695

    
696
  def ReplaceInstanceDisks(self, instance, disks, mode=REPLACE_DISK_AUTO,
697
                           remote_node=None, iallocator=None, dry_run=False):
698
    """Replaces disks on an instance.
699

700
    @type instance: str
701
    @param instance: instance whose disks to replace
702
    @type disks: list of str
703
    @param disks: disks to replace
704
    @type mode: str
705
    @param mode: replacement mode to use (defaults to replace_auto)
706
    @type remote_node: str or None
707
    @param remote_node: new secondary node to use (for use with
708
        replace_new_secondary mode)
709
    @type iallocator: str or None
710
    @param iallocator: instance allocator plugin to use (for use with
711
                       replace_auto mode)
712
    @type dry_run: bool
713
    @param dry_run: whether to perform a dry run
714

715
    @rtype: int
716
    @return: job id
717

718
    @raises InvalidReplacementMode: If an invalid disk replacement mode is given
719
    @raises GanetiApiError: If no secondary node is given with a non-auto
720
        replacement mode is requested.
721

722
    """
723
    if mode not in VALID_REPLACEMENT_MODES:
724
      raise InvalidReplacementMode("%s is not a valid disk replacement mode" %
725
                                   mode)
726

    
727
    query = [
728
      ("mode", mode),
729
      ("disks", ",".join(disks)),
730
      ]
731

    
732
    if mode == REPLACE_DISK_AUTO:
733
      query.append(("iallocator", iallocator))
734
    elif mode == REPLACE_DISK_SECONDARY:
735
      if remote_node is None:
736
        raise GanetiApiError("Missing secondary node")
737
      query.append(("remote_node", remote_node))
738

    
739
    if dry_run:
740
      query.append(("dry-run", 1))
741

    
742
    return self._SendRequest(HTTP_POST,
743
                             ("/%s/instances/%s/replace-disks" %
744
                              (GANETI_RAPI_VERSION, instance)), query, None)
745

    
746
  def GetJobs(self):
747
    """Gets all jobs for the cluster.
748

749
    @rtype: list of int
750
    @return: job ids for the cluster
751

752
    """
753
    return [int(j["id"])
754
            for j in self._SendRequest(HTTP_GET,
755
                                       "/%s/jobs" % GANETI_RAPI_VERSION,
756
                                       None, None)]
757

    
758
  def GetJobStatus(self, job_id):
759
    """Gets the status of a job.
760

761
    @type job_id: int
762
    @param job_id: job id whose status to query
763

764
    @rtype: dict
765
    @return: job status
766

767
    """
768
    return self._SendRequest(HTTP_GET,
769
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
770
                             None, None)
771

    
772
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
773
    """Waits for job changes.
774

775
    @type job_id: int
776
    @param job_id: Job ID for which to wait
777

778
    """
779
    body = {
780
      "fields": fields,
781
      "previous_job_info": prev_job_info,
782
      "previous_log_serial": prev_log_serial,
783
      }
784

    
785
    return self._SendRequest(HTTP_GET,
786
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
787
                             None, body)
788

    
789
  def CancelJob(self, job_id, dry_run=False):
790
    """Cancels a job.
791

792
    @type job_id: int
793
    @param job_id: id of the job to delete
794
    @type dry_run: bool
795
    @param dry_run: whether to perform a dry run
796

797
    """
798
    query = []
799
    if dry_run:
800
      query.append(("dry-run", 1))
801

    
802
    return self._SendRequest(HTTP_DELETE,
803
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
804
                             query, None)
805

    
806
  def GetNodes(self, bulk=False):
807
    """Gets all nodes in the cluster.
808

809
    @type bulk: bool
810
    @param bulk: whether to return all information about all instances
811

812
    @rtype: list of dict or str
813
    @return: if bulk is true, info about nodes in the cluster,
814
        else list of nodes in the cluster
815

816
    """
817
    query = []
818
    if bulk:
819
      query.append(("bulk", 1))
820

    
821
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
822
                              query, None)
823
    if bulk:
824
      return nodes
825
    else:
826
      return [n["id"] for n in nodes]
827

    
828
  def GetNodeInfo(self, node):
829
    """Gets information about a node.
830

831
    @type node: str
832
    @param node: node whose info to return
833

834
    @rtype: dict
835
    @return: info about the node
836

837
    """
838
    return self._SendRequest(HTTP_GET,
839
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
840
                             None, None)
841

    
842
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
843
                   dry_run=False):
844
    """Evacuates instances from a Ganeti node.
845

846
    @type node: str
847
    @param node: node to evacuate
848
    @type iallocator: str or None
849
    @param iallocator: instance allocator to use
850
    @type remote_node: str
851
    @param remote_node: node to evaucate to
852
    @type dry_run: bool
853
    @param dry_run: whether to perform a dry run
854

855
    @rtype: int
856
    @return: job id
857

858
    @raises GanetiApiError: if an iallocator and remote_node are both specified
859

860
    """
861
    if iallocator and remote_node:
862
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
863

    
864
    query = []
865
    if iallocator:
866
      query.append(("iallocator", iallocator))
867
    if remote_node:
868
      query.append(("remote_node", remote_node))
869
    if dry_run:
870
      query.append(("dry-run", 1))
871

    
872
    return self._SendRequest(HTTP_POST,
873
                             ("/%s/nodes/%s/evacuate" %
874
                              (GANETI_RAPI_VERSION, node)), query, None)
875

    
876
  def MigrateNode(self, node, live=True, dry_run=False):
877
    """Migrates all primary instances from a node.
878

879
    @type node: str
880
    @param node: node to migrate
881
    @type live: bool
882
    @param live: whether to use live migration
883
    @type dry_run: bool
884
    @param dry_run: whether to perform a dry run
885

886
    @rtype: int
887
    @return: job id
888

889
    """
890
    query = []
891
    if live:
892
      query.append(("live", 1))
893
    if dry_run:
894
      query.append(("dry-run", 1))
895

    
896
    return self._SendRequest(HTTP_POST,
897
                             ("/%s/nodes/%s/migrate" %
898
                              (GANETI_RAPI_VERSION, node)), query, None)
899

    
900
  def GetNodeRole(self, node):
901
    """Gets the current role for a node.
902

903
    @type node: str
904
    @param node: node whose role to return
905

906
    @rtype: str
907
    @return: the current role for a node
908

909
    """
910
    return self._SendRequest(HTTP_GET,
911
                             ("/%s/nodes/%s/role" %
912
                              (GANETI_RAPI_VERSION, node)), None, None)
913

    
914
  def SetNodeRole(self, node, role, force=False):
915
    """Sets the role for a node.
916

917
    @type node: str
918
    @param node: the node whose role to set
919
    @type role: str
920
    @param role: the role to set for the node
921
    @type force: bool
922
    @param force: whether to force the role change
923

924
    @rtype: int
925
    @return: job id
926

927
    @raise InvalidNodeRole: If an invalid node role is specified
928

929
    """
930
    if role not in VALID_NODE_ROLES:
931
      raise InvalidNodeRole("%s is not a valid node role" % role)
932

    
933
    query = [("force", force)]
934

    
935
    return self._SendRequest(HTTP_PUT,
936
                             ("/%s/nodes/%s/role" %
937
                              (GANETI_RAPI_VERSION, node)), query, role)
938

    
939
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
940
    """Gets the storage units for a node.
941

942
    @type node: str
943
    @param node: the node whose storage units to return
944
    @type storage_type: str
945
    @param storage_type: storage type whose units to return
946
    @type output_fields: str
947
    @param output_fields: storage type fields to return
948

949
    @rtype: int
950
    @return: job id where results can be retrieved
951

952
    """
953
    query = [
954
      ("storage_type", storage_type),
955
      ("output_fields", output_fields),
956
      ]
957

    
958
    return self._SendRequest(HTTP_GET,
959
                             ("/%s/nodes/%s/storage" %
960
                              (GANETI_RAPI_VERSION, node)), query, None)
961

    
962
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=True):
963
    """Modifies parameters of storage units on the node.
964

965
    @type node: str
966
    @param node: node whose storage units to modify
967
    @type storage_type: str
968
    @param storage_type: storage type whose units to modify
969
    @type name: str
970
    @param name: name of the storage unit
971
    @type allocatable: bool
972
    @param allocatable: TODO: Document me
973

974
    @rtype: int
975
    @return: job id
976

977
    """
978
    query = [
979
      ("storage_type", storage_type),
980
      ("name", name),
981
      ("allocatable", allocatable),
982
      ]
983

    
984
    return self._SendRequest(HTTP_PUT,
985
                             ("/%s/nodes/%s/storage/modify" %
986
                              (GANETI_RAPI_VERSION, node)), query, None)
987

    
988
  def RepairNodeStorageUnits(self, node, storage_type, name):
989
    """Repairs a storage unit on the node.
990

991
    @type node: str
992
    @param node: node whose storage units to repair
993
    @type storage_type: str
994
    @param storage_type: storage type to repair
995
    @type name: str
996
    @param name: name of the storage unit to repair
997

998
    @rtype: int
999
    @return: job id
1000

1001
    """
1002
    query = [
1003
      ("storage_type", storage_type),
1004
      ("name", name),
1005
      ]
1006

    
1007
    return self._SendRequest(HTTP_PUT,
1008
                             ("/%s/nodes/%s/storage/repair" %
1009
                              (GANETI_RAPI_VERSION, node)), query, None)
1010

    
1011
  def GetNodeTags(self, node):
1012
    """Gets the tags for a node.
1013

1014
    @type node: str
1015
    @param node: node whose tags to return
1016

1017
    @rtype: list of str
1018
    @return: tags for the node
1019

1020
    """
1021
    return self._SendRequest(HTTP_GET,
1022
                             ("/%s/nodes/%s/tags" %
1023
                              (GANETI_RAPI_VERSION, node)), None, None)
1024

    
1025
  def AddNodeTags(self, node, tags, dry_run=False):
1026
    """Adds tags to a node.
1027

1028
    @type node: str
1029
    @param node: node to add tags to
1030
    @type tags: list of str
1031
    @param tags: tags to add to the node
1032
    @type dry_run: bool
1033
    @param dry_run: whether to perform a dry run
1034

1035
    @rtype: int
1036
    @return: job id
1037

1038
    """
1039
    query = [("tag", t) for t in tags]
1040
    if dry_run:
1041
      query.append(("dry-run", 1))
1042

    
1043
    return self._SendRequest(HTTP_PUT,
1044
                             ("/%s/nodes/%s/tags" %
1045
                              (GANETI_RAPI_VERSION, node)), query, tags)
1046

    
1047
  def DeleteNodeTags(self, node, tags, dry_run=False):
1048
    """Delete tags from a node.
1049

1050
    @type node: str
1051
    @param node: node to remove tags from
1052
    @type tags: list of str
1053
    @param tags: tags to remove from the node
1054
    @type dry_run: bool
1055
    @param dry_run: whether to perform a dry run
1056

1057
    @rtype: int
1058
    @return: job id
1059

1060
    """
1061
    query = [("tag", t) for t in tags]
1062
    if dry_run:
1063
      query.append(("dry-run", 1))
1064

    
1065
    return self._SendRequest(HTTP_DELETE,
1066
                             ("/%s/nodes/%s/tags" %
1067
                              (GANETI_RAPI_VERSION, node)), query, None)