Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / rapi.py @ 883c1f94

History | View | Annotate | Download (52.7 kB)

1
#
2
#
3

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

    
22

    
23
"""Ganeti RAPI client."""
24

    
25
# No Ganeti-specific modules should be imported. The RAPI client is supposed to
26
# be standalone.
27

    
28
import requests
29
import logging
30
import simplejson
31
import time
32

    
33
GANETI_RAPI_PORT = 5080
34
GANETI_RAPI_VERSION = 2
35

    
36
HTTP_DELETE = "DELETE"
37
HTTP_GET = "GET"
38
HTTP_PUT = "PUT"
39
HTTP_POST = "POST"
40
HTTP_OK = 200
41
HTTP_NOT_FOUND = 404
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

    
49
NODE_EVAC_PRI = "primary-only"
50
NODE_EVAC_SEC = "secondary-only"
51
NODE_EVAC_ALL = "all"
52

    
53
NODE_ROLE_DRAINED = "drained"
54
NODE_ROLE_MASTER_CANDIATE = "master-candidate"
55
NODE_ROLE_MASTER = "master"
56
NODE_ROLE_OFFLINE = "offline"
57
NODE_ROLE_REGULAR = "regular"
58

    
59
JOB_STATUS_QUEUED = "queued"
60
JOB_STATUS_WAITING = "waiting"
61
JOB_STATUS_CANCELING = "canceling"
62
JOB_STATUS_RUNNING = "running"
63
JOB_STATUS_CANCELED = "canceled"
64
JOB_STATUS_SUCCESS = "success"
65
JOB_STATUS_ERROR = "error"
66
JOB_STATUS_FINALIZED = frozenset([
67
  JOB_STATUS_CANCELED,
68
  JOB_STATUS_SUCCESS,
69
  JOB_STATUS_ERROR,
70
  ])
71
JOB_STATUS_ALL = frozenset([
72
  JOB_STATUS_QUEUED,
73
  JOB_STATUS_WAITING,
74
  JOB_STATUS_CANCELING,
75
  JOB_STATUS_RUNNING,
76
  ]) | JOB_STATUS_FINALIZED
77

    
78
# Legacy name
79
JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
80

    
81
# Internal constants
82
_REQ_DATA_VERSION_FIELD = "__version__"
83
_QPARAM_DRY_RUN = "dry-run"
84
_QPARAM_FORCE = "force"
85

    
86
# Feature strings
87
INST_CREATE_REQV1 = "instance-create-reqv1"
88
INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
89
NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
90
NODE_EVAC_RES1 = "node-evac-res1"
91

    
92
# Old feature constant names in case they're references by users of this module
93
_INST_CREATE_REQV1 = INST_CREATE_REQV1
94
_INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
95
_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
96
_NODE_EVAC_RES1 = NODE_EVAC_RES1
97

    
98
#: Not enough resources (iallocator failure, disk space, memory, etc.)
99
ECODE_NORES = "insufficient_resources"
100

    
101
#: Temporarily out of resources; operation can be tried again
102
ECODE_TEMP_NORES = "temp_insufficient_resources"
103

    
104

    
105
class Error(Exception):
106
  """Base error class for this module.
107

108
  """
109
  pass
110

    
111

    
112
class GanetiApiError(Error):
113
  """Generic error raised from Ganeti API.
114

115
  """
116
  def __init__(self, msg, code=None):
117
    Error.__init__(self, msg)
118
    self.code = code
119

    
120

    
121
class CertificateError(GanetiApiError):
122
  """Raised when a problem is found with the SSL certificate.
123

124
  """
125
  pass
126

    
127

    
128
def _AppendIf(container, condition, value):
129
  """Appends to a list if a condition evaluates to truth.
130

131
  """
132
  if condition:
133
    container.append(value)
134

    
135
  return condition
136

    
137

    
138
def _AppendDryRunIf(container, condition):
139
  """Appends a "dry-run" parameter if a condition evaluates to truth.
140

141
  """
142
  return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
143

    
144

    
145
def _AppendForceIf(container, condition):
146
  """Appends a "force" parameter if a condition evaluates to truth.
147

148
  """
149
  return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
150

    
151

    
152
def _SetItemIf(container, condition, item, value):
153
  """Sets an item if a condition evaluates to truth.
154

155
  """
156
  if condition:
157
    container[item] = value
158

    
159
  return condition
160

    
161

    
162
class GanetiRapiClient(object): # pylint: disable=R0904
163
  """Ganeti RAPI client.
164

165
  """
166
  USER_AGENT = "Ganeti RAPI Client"
167
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
168

    
169
  def __init__(self, host, port=GANETI_RAPI_PORT,
170
               username=None, password=None, logger=logging):
171
    """Initializes this class.
172

173
    @type host: string
174
    @param host: the ganeti cluster master to interact with
175
    @type port: int
176
    @param port: the port on which the RAPI is running (default is 5080)
177
    @type username: string
178
    @param username: the username to connect with
179
    @type password: string
180
    @param password: the password to connect with
181
    @param logger: Logging object
182

183
    """
184
    self._logger = logger
185
    self._base_url = "https://%s:%s" % (host, port)
186

    
187
    if username is not None:
188
      if password is None:
189
        raise Error("Password not specified")
190
    elif password:
191
      raise Error("Specified password without username")
192

    
193
    self._auth = (username, password)
194

    
195
  def _SendRequest(self, method, path, query, content):
196
    """Sends an HTTP request.
197

198
    This constructs a full URL, encodes and decodes HTTP bodies, and
199
    handles invalid responses in a pythonic way.
200

201
    @type method: string
202
    @param method: HTTP method to use
203
    @type path: string
204
    @param path: HTTP URL path
205
    @type query: list of two-tuples
206
    @param query: query arguments to pass to urllib.urlencode
207
    @type content: str or None
208
    @param content: HTTP body content
209

210
    @rtype: str
211
    @return: JSON-Decoded response
212

213
    @raises CertificateError: If an invalid SSL certificate is found
214
    @raises GanetiApiError: If an invalid response is returned
215

216
    """
217
    assert path.startswith("/")
218
    url = "%s%s" % (self._base_url, path)
219

    
220
    headers = {}
221
    if content is not None:
222
      encoded_content = self._json_encoder.encode(content)
223
      headers = {"content-type": HTTP_APP_JSON,
224
                 "accept": HTTP_APP_JSON}
225
    else:
226
      encoded_content = ""
227

    
228
    if query is not None:
229
        query = dict(query)
230

    
231
    self._logger.debug("Sending request %s %s (query=%r) (content=%r)",
232
                       method, url, query, encoded_content)
233

    
234
    req_method = getattr(requests, method.lower())
235
    r = req_method(url, auth=self._auth, headers=headers, params=query,
236
                   data=encoded_content, verify=False)
237

    
238

    
239
    http_code = r.status_code
240
    if r.content is not None:
241
        response_content = simplejson.loads(r.content)
242
    else:
243
        response_content = None
244

    
245
    if http_code != HTTP_OK:
246
      if isinstance(response_content, dict):
247
        msg = ("%s %s: %s" %
248
               (response_content["code"],
249
                response_content["message"],
250
                response_content["explain"]))
251
      else:
252
        msg = str(response_content)
253

    
254
      raise GanetiApiError(msg, code=http_code)
255

    
256
    return response_content
257

    
258
  def GetVersion(self):
259
    """Gets the Remote API version running on the cluster.
260

261
    @rtype: int
262
    @return: Ganeti Remote API version
263

264
    """
265
    return self._SendRequest(HTTP_GET, "/version", None, None)
266

    
267
  def GetFeatures(self):
268
    """Gets the list of optional features supported by RAPI server.
269

270
    @rtype: list
271
    @return: List of optional features
272

273
    """
274
    try:
275
      return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
276
                               None, None)
277
    except GanetiApiError, err:
278
      # Older RAPI servers don't support this resource
279
      if err.code == HTTP_NOT_FOUND:
280
        return []
281

    
282
      raise
283

    
284
  def GetOperatingSystems(self):
285
    """Gets the Operating Systems running in the Ganeti cluster.
286

287
    @rtype: list of str
288
    @return: operating systems
289

290
    """
291
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
292
                             None, None)
293

    
294
  def GetInfo(self):
295
    """Gets info about the cluster.
296

297
    @rtype: dict
298
    @return: information about the cluster
299

300
    """
301
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
302
                             None, None)
303

    
304
  def RedistributeConfig(self):
305
    """Tells the cluster to redistribute its configuration files.
306

307
    @rtype: string
308
    @return: job id
309

310
    """
311
    return self._SendRequest(HTTP_PUT,
312
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
313
                             None, None)
314

    
315
  def ModifyCluster(self, **kwargs):
316
    """Modifies cluster parameters.
317

318
    More details for parameters can be found in the RAPI documentation.
319

320
    @rtype: string
321
    @return: job id
322

323
    """
324
    body = kwargs
325

    
326
    return self._SendRequest(HTTP_PUT,
327
                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
328

    
329
  def GetClusterTags(self):
330
    """Gets the cluster tags.
331

332
    @rtype: list of str
333
    @return: cluster tags
334

335
    """
336
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
337
                             None, None)
338

    
339
  def AddClusterTags(self, tags, dry_run=False):
340
    """Adds tags to the cluster.
341

342
    @type tags: list of str
343
    @param tags: tags to add to the cluster
344
    @type dry_run: bool
345
    @param dry_run: whether to perform a dry run
346

347
    @rtype: string
348
    @return: job id
349

350
    """
351
    query = [("tag", t) for t in tags]
352
    _AppendDryRunIf(query, dry_run)
353

    
354
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
355
                             query, None)
356

    
357
  def DeleteClusterTags(self, tags, dry_run=False):
358
    """Deletes tags from the cluster.
359

360
    @type tags: list of str
361
    @param tags: tags to delete
362
    @type dry_run: bool
363
    @param dry_run: whether to perform a dry run
364
    @rtype: string
365
    @return: job id
366

367
    """
368
    query = [("tag", t) for t in tags]
369
    _AppendDryRunIf(query, dry_run)
370

    
371
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
372
                             query, None)
373

    
374
  def GetInstances(self, bulk=False):
375
    """Gets information about instances on the cluster.
376

377
    @type bulk: bool
378
    @param bulk: whether to return all information about all instances
379

380
    @rtype: list of dict or list of str
381
    @return: if bulk is True, info about the instances, else a list of instances
382

383
    """
384
    query = []
385
    _AppendIf(query, bulk, ("bulk", 1))
386

    
387
    instances = self._SendRequest(HTTP_GET,
388
                                  "/%s/instances" % GANETI_RAPI_VERSION,
389
                                  query, None)
390
    if bulk:
391
      return instances
392
    else:
393
      return [i["id"] for i in instances]
394

    
395
  def GetInstance(self, instance):
396
    """Gets information about an instance.
397

398
    @type instance: str
399
    @param instance: instance whose info to return
400

401
    @rtype: dict
402
    @return: info about the instance
403

404
    """
405
    return self._SendRequest(HTTP_GET,
406
                             ("/%s/instances/%s" %
407
                              (GANETI_RAPI_VERSION, instance)), None, None)
408

    
409
  def GetInstanceInfo(self, instance, static=None):
410
    """Gets information about an instance.
411

412
    @type instance: string
413
    @param instance: Instance name
414
    @rtype: string
415
    @return: Job ID
416

417
    """
418
    if static is not None:
419
      query = [("static", static)]
420
    else:
421
      query = None
422

    
423
    return self._SendRequest(HTTP_GET,
424
                             ("/%s/instances/%s/info" %
425
                              (GANETI_RAPI_VERSION, instance)), query, None)
426

    
427
  def CreateInstance(self, mode, name, disk_template, disks, nics,
428
                     **kwargs):
429
    """Creates a new instance.
430

431
    More details for parameters can be found in the RAPI documentation.
432

433
    @type mode: string
434
    @param mode: Instance creation mode
435
    @type name: string
436
    @param name: Hostname of the instance to create
437
    @type disk_template: string
438
    @param disk_template: Disk template for instance (e.g. plain, diskless,
439
                          file, or drbd)
440
    @type disks: list of dicts
441
    @param disks: List of disk definitions
442
    @type nics: list of dicts
443
    @param nics: List of NIC definitions
444
    @type dry_run: bool
445
    @keyword dry_run: whether to perform a dry run
446

447
    @rtype: string
448
    @return: job id
449

450
    """
451
    query = []
452

    
453
    _AppendDryRunIf(query, kwargs.get("dry_run"))
454

    
455
    if _INST_CREATE_REQV1 in self.GetFeatures():
456
      # All required fields for request data version 1
457
      body = {
458
        _REQ_DATA_VERSION_FIELD: 1,
459
        "mode": mode,
460
        "name": name,
461
        "disk_template": disk_template,
462
        "disks": disks,
463
        "nics": nics,
464
        }
465

    
466
      conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
467
      if conflicts:
468
        raise GanetiApiError("Required fields cannot be specified as"
469
                             " keywords: %s" % ", ".join(conflicts))
470

    
471
      body.update((key, value) for key, value in kwargs.iteritems()
472
                  if key != "dry_run")
473
    else:
474
      raise GanetiApiError("Server does not support new-style (version 1)"
475
                           " instance creation requests")
476

    
477
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
478
                             query, body)
479

    
480
  def DeleteInstance(self, instance, dry_run=False, shutdown_timeout=None):
481
    """Deletes an instance.
482

483
    @type instance: str
484
    @param instance: the instance to delete
485

486
    @rtype: string
487
    @return: job id
488

489
    """
490
    query = []
491
    _AppendDryRunIf(query, dry_run)
492

    
493
    body = None
494
    if shutdown_timeout is not None:
495
        body = {"shutdown_timeout": shutdown_timeout}
496

    
497
    return self._SendRequest(HTTP_DELETE,
498
                             ("/%s/instances/%s" %
499
                              (GANETI_RAPI_VERSION, instance)), query, body)
500

    
501
  def ModifyInstance(self, instance, **kwargs):
502
    """Modifies an instance.
503

504
    More details for parameters can be found in the RAPI documentation.
505

506
    @type instance: string
507
    @param instance: Instance name
508
    @rtype: string
509
    @return: job id
510

511
    """
512
    body = kwargs
513

    
514
    return self._SendRequest(HTTP_PUT,
515
                             ("/%s/instances/%s/modify" %
516
                              (GANETI_RAPI_VERSION, instance)), None, body)
517

    
518
  def ActivateInstanceDisks(self, instance, ignore_size=None):
519
    """Activates an instance's disks.
520

521
    @type instance: string
522
    @param instance: Instance name
523
    @type ignore_size: bool
524
    @param ignore_size: Whether to ignore recorded size
525
    @rtype: string
526
    @return: job id
527

528
    """
529
    query = []
530
    _AppendIf(query, ignore_size, ("ignore_size", 1))
531

    
532
    return self._SendRequest(HTTP_PUT,
533
                             ("/%s/instances/%s/activate-disks" %
534
                              (GANETI_RAPI_VERSION, instance)), query, None)
535

    
536
  def DeactivateInstanceDisks(self, instance):
537
    """Deactivates an instance's disks.
538

539
    @type instance: string
540
    @param instance: Instance name
541
    @rtype: string
542
    @return: job id
543

544
    """
545
    return self._SendRequest(HTTP_PUT,
546
                             ("/%s/instances/%s/deactivate-disks" %
547
                              (GANETI_RAPI_VERSION, instance)), None, None)
548

    
549
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
550
    """Recreate an instance's disks.
551

552
    @type instance: string
553
    @param instance: Instance name
554
    @type disks: list of int
555
    @param disks: List of disk indexes
556
    @type nodes: list of string
557
    @param nodes: New instance nodes, if relocation is desired
558
    @rtype: string
559
    @return: job id
560

561
    """
562
    body = {}
563
    _SetItemIf(body, disks is not None, "disks", disks)
564
    _SetItemIf(body, nodes is not None, "nodes", nodes)
565

    
566
    return self._SendRequest(HTTP_POST,
567
                             ("/%s/instances/%s/recreate-disks" %
568
                              (GANETI_RAPI_VERSION, instance)), None, body)
569

    
570
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
571
    """Grows a disk of an instance.
572

573
    More details for parameters can be found in the RAPI documentation.
574

575
    @type instance: string
576
    @param instance: Instance name
577
    @type disk: integer
578
    @param disk: Disk index
579
    @type amount: integer
580
    @param amount: Grow disk by this amount (MiB)
581
    @type wait_for_sync: bool
582
    @param wait_for_sync: Wait for disk to synchronize
583
    @rtype: string
584
    @return: job id
585

586
    """
587
    body = {
588
      "amount": amount,
589
      }
590

    
591
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
592

    
593
    return self._SendRequest(HTTP_POST,
594
                             ("/%s/instances/%s/disk/%s/grow" %
595
                              (GANETI_RAPI_VERSION, instance, disk)),
596
                             None, body)
597

    
598
  def GetInstanceTags(self, instance):
599
    """Gets tags for an instance.
600

601
    @type instance: str
602
    @param instance: instance whose tags to return
603

604
    @rtype: list of str
605
    @return: tags for the instance
606

607
    """
608
    return self._SendRequest(HTTP_GET,
609
                             ("/%s/instances/%s/tags" %
610
                              (GANETI_RAPI_VERSION, instance)), None, None)
611

    
612
  def AddInstanceTags(self, instance, tags, dry_run=False):
613
    """Adds tags to an instance.
614

615
    @type instance: str
616
    @param instance: instance to add tags to
617
    @type tags: list of str
618
    @param tags: tags to add to the instance
619
    @type dry_run: bool
620
    @param dry_run: whether to perform a dry run
621

622
    @rtype: string
623
    @return: job id
624

625
    """
626
    query = [("tag", t) for t in tags]
627
    _AppendDryRunIf(query, dry_run)
628

    
629
    return self._SendRequest(HTTP_PUT,
630
                             ("/%s/instances/%s/tags" %
631
                              (GANETI_RAPI_VERSION, instance)), query, None)
632

    
633
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
634
    """Deletes tags from an instance.
635

636
    @type instance: str
637
    @param instance: instance to delete tags from
638
    @type tags: list of str
639
    @param tags: tags to delete
640
    @type dry_run: bool
641
    @param dry_run: whether to perform a dry run
642
    @rtype: string
643
    @return: job id
644

645
    """
646
    query = [("tag", t) for t in tags]
647
    _AppendDryRunIf(query, dry_run)
648

    
649
    return self._SendRequest(HTTP_DELETE,
650
                             ("/%s/instances/%s/tags" %
651
                              (GANETI_RAPI_VERSION, instance)), query, None)
652

    
653
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
654
                     dry_run=False, shutdown_timeout=None):
655
    """Reboots an instance.
656

657
    @type instance: str
658
    @param instance: instance to rebot
659
    @type reboot_type: str
660
    @param reboot_type: one of: hard, soft, full
661
    @type ignore_secondaries: bool
662
    @param ignore_secondaries: if True, ignores errors for the secondary node
663
        while re-assembling disks (in hard-reboot mode only)
664
    @type dry_run: bool
665
    @param dry_run: whether to perform a dry run
666
    @rtype: string
667
    @return: job id
668

669
    """
670
    query = []
671
    _AppendDryRunIf(query, dry_run)
672
    _AppendIf(query, reboot_type, ("type", reboot_type))
673
    _AppendIf(query, ignore_secondaries is not None,
674
              ("ignore_secondaries", ignore_secondaries))
675

    
676
    body = None
677
    if shutdown_timeout is not None:
678
        body = {"shutdown_timeout": shutdown_timeout}
679

    
680
    return self._SendRequest(HTTP_POST,
681
                             ("/%s/instances/%s/reboot" %
682
                              (GANETI_RAPI_VERSION, instance)), query, body)
683

    
684
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
685
                       timeout=None):
686
    """Shuts down an instance.
687

688
    @type instance: str
689
    @param instance: the instance to shut down
690
    @type dry_run: bool
691
    @param dry_run: whether to perform a dry run
692
    @type no_remember: bool
693
    @param no_remember: if true, will not record the state change
694
    @rtype: string
695
    @return: job id
696

697
    """
698
    query = []
699
    _AppendDryRunIf(query, dry_run)
700
    _AppendIf(query, no_remember, ("no-remember", 1))
701
    body = None
702
    if timeout is not None:
703
        body = {"timeout": timeout}
704

    
705

    
706
    return self._SendRequest(HTTP_PUT,
707
                             ("/%s/instances/%s/shutdown" %
708
                              (GANETI_RAPI_VERSION, instance)), query, body)
709

    
710
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
711
    """Starts up an instance.
712

713
    @type instance: str
714
    @param instance: the instance to start up
715
    @type dry_run: bool
716
    @param dry_run: whether to perform a dry run
717
    @type no_remember: bool
718
    @param no_remember: if true, will not record the state change
719
    @rtype: string
720
    @return: job id
721

722
    """
723
    query = []
724
    _AppendDryRunIf(query, dry_run)
725
    _AppendIf(query, no_remember, ("no-remember", 1))
726

    
727
    return self._SendRequest(HTTP_PUT,
728
                             ("/%s/instances/%s/startup" %
729
                              (GANETI_RAPI_VERSION, instance)), query, None)
730

    
731
  def ReinstallInstance(self, instance, os=None, no_startup=False,
732
                        osparams=None):
733
    """Reinstalls an instance.
734

735
    @type instance: str
736
    @param instance: The instance to reinstall
737
    @type os: str or None
738
    @param os: The operating system to reinstall. If None, the instance's
739
        current operating system will be installed again
740
    @type no_startup: bool
741
    @param no_startup: Whether to start the instance automatically
742
    @rtype: string
743
    @return: job id
744

745
    """
746
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
747
      body = {
748
        "start": not no_startup,
749
        }
750
      _SetItemIf(body, os is not None, "os", os)
751
      _SetItemIf(body, osparams is not None, "osparams", osparams)
752
      return self._SendRequest(HTTP_POST,
753
                               ("/%s/instances/%s/reinstall" %
754
                                (GANETI_RAPI_VERSION, instance)), None, body)
755

    
756
    # Use old request format
757
    if osparams:
758
      raise GanetiApiError("Server does not support specifying OS parameters"
759
                           " for instance reinstallation")
760

    
761
    query = []
762
    _AppendIf(query, os, ("os", os))
763
    _AppendIf(query, no_startup, ("nostartup", 1))
764

    
765
    return self._SendRequest(HTTP_POST,
766
                             ("/%s/instances/%s/reinstall" %
767
                              (GANETI_RAPI_VERSION, instance)), query, None)
768

    
769
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
770
                           remote_node=None, iallocator=None):
771
    """Replaces disks on an instance.
772

773
    @type instance: str
774
    @param instance: instance whose disks to replace
775
    @type disks: list of ints
776
    @param disks: Indexes of disks to replace
777
    @type mode: str
778
    @param mode: replacement mode to use (defaults to replace_auto)
779
    @type remote_node: str or None
780
    @param remote_node: new secondary node to use (for use with
781
        replace_new_secondary mode)
782
    @type iallocator: str or None
783
    @param iallocator: instance allocator plugin to use (for use with
784
                       replace_auto mode)
785

786
    @rtype: string
787
    @return: job id
788

789
    """
790
    query = [
791
      ("mode", mode),
792
      ]
793

    
794
    # TODO: Convert to body parameters
795

    
796
    if disks is not None:
797
      _AppendIf(query, True,
798
                ("disks", ",".join(str(idx) for idx in disks)))
799

    
800
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
801
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
802

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

    
807
  def PrepareExport(self, instance, mode):
808
    """Prepares an instance for an export.
809

810
    @type instance: string
811
    @param instance: Instance name
812
    @type mode: string
813
    @param mode: Export mode
814
    @rtype: string
815
    @return: Job ID
816

817
    """
818
    query = [("mode", mode)]
819
    return self._SendRequest(HTTP_PUT,
820
                             ("/%s/instances/%s/prepare-export" %
821
                              (GANETI_RAPI_VERSION, instance)), query, None)
822

    
823
  def ExportInstance(self, instance, mode, destination, shutdown=None,
824
                     remove_instance=None,
825
                     x509_key_name=None, destination_x509_ca=None):
826
    """Exports an instance.
827

828
    @type instance: string
829
    @param instance: Instance name
830
    @type mode: string
831
    @param mode: Export mode
832
    @rtype: string
833
    @return: Job ID
834

835
    """
836
    body = {
837
      "destination": destination,
838
      "mode": mode,
839
      }
840

    
841
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
842
    _SetItemIf(body, remove_instance is not None,
843
               "remove_instance", remove_instance)
844
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
845
    _SetItemIf(body, destination_x509_ca is not None,
846
               "destination_x509_ca", destination_x509_ca)
847

    
848
    return self._SendRequest(HTTP_PUT,
849
                             ("/%s/instances/%s/export" %
850
                              (GANETI_RAPI_VERSION, instance)), None, body)
851

    
852
  def MigrateInstance(self, instance, mode=None, cleanup=None):
853
    """Migrates an instance.
854

855
    @type instance: string
856
    @param instance: Instance name
857
    @type mode: string
858
    @param mode: Migration mode
859
    @type cleanup: bool
860
    @param cleanup: Whether to clean up a previously failed migration
861
    @rtype: string
862
    @return: job id
863

864
    """
865
    body = {}
866
    _SetItemIf(body, mode is not None, "mode", mode)
867
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
868

    
869
    return self._SendRequest(HTTP_PUT,
870
                             ("/%s/instances/%s/migrate" %
871
                              (GANETI_RAPI_VERSION, instance)), None, body)
872

    
873
  def FailoverInstance(self, instance, iallocator=None,
874
                       ignore_consistency=None, target_node=None):
875
    """Does a failover of an instance.
876

877
    @type instance: string
878
    @param instance: Instance name
879
    @type iallocator: string
880
    @param iallocator: Iallocator for deciding the target node for
881
      shared-storage instances
882
    @type ignore_consistency: bool
883
    @param ignore_consistency: Whether to ignore disk consistency
884
    @type target_node: string
885
    @param target_node: Target node for shared-storage instances
886
    @rtype: string
887
    @return: job id
888

889
    """
890
    body = {}
891
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
892
    _SetItemIf(body, ignore_consistency is not None,
893
               "ignore_consistency", ignore_consistency)
894
    _SetItemIf(body, target_node is not None, "target_node", target_node)
895

    
896
    return self._SendRequest(HTTP_PUT,
897
                             ("/%s/instances/%s/failover" %
898
                              (GANETI_RAPI_VERSION, instance)), None, body)
899

    
900
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
901
    """Changes the name of an instance.
902

903
    @type instance: string
904
    @param instance: Instance name
905
    @type new_name: string
906
    @param new_name: New instance name
907
    @type ip_check: bool
908
    @param ip_check: Whether to ensure instance's IP address is inactive
909
    @type name_check: bool
910
    @param name_check: Whether to ensure instance's name is resolvable
911
    @rtype: string
912
    @return: job id
913

914
    """
915
    body = {
916
      "new_name": new_name,
917
      }
918

    
919
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
920
    _SetItemIf(body, name_check is not None, "name_check", name_check)
921

    
922
    return self._SendRequest(HTTP_PUT,
923
                             ("/%s/instances/%s/rename" %
924
                              (GANETI_RAPI_VERSION, instance)), None, body)
925

    
926
  def GetInstanceConsole(self, instance):
927
    """Request information for connecting to instance's console.
928

929
    @type instance: string
930
    @param instance: Instance name
931
    @rtype: dict
932
    @return: dictionary containing information about instance's console
933

934
    """
935
    return self._SendRequest(HTTP_GET,
936
                             ("/%s/instances/%s/console" %
937
                              (GANETI_RAPI_VERSION, instance)), None, None)
938

    
939
  def GetJobs(self, bulk=False):
940
    """Gets all jobs for the cluster.
941

942
    @rtype: list of int
943
    @return: job ids for the cluster
944

945
    """
946
    query = []
947
    _AppendIf(query, bulk, ("bulk", 1))
948

    
949
    jobs = self._SendRequest(HTTP_GET, "/%s/jobs" % GANETI_RAPI_VERSION,
950
                             query, None)
951
    if bulk:
952
        return jobs
953
    else:
954
        return [int(j["id"]) for j in jobs]
955

    
956

    
957
  def GetJobStatus(self, job_id):
958
    """Gets the status of a job.
959

960
    @type job_id: string
961
    @param job_id: job id whose status to query
962

963
    @rtype: dict
964
    @return: job status
965

966
    """
967
    return self._SendRequest(HTTP_GET,
968
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
969
                             None, None)
970

    
971
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
972
    """Polls cluster for job status until completion.
973

974
    Completion is defined as any of the following states listed in
975
    L{JOB_STATUS_FINALIZED}.
976

977
    @type job_id: string
978
    @param job_id: job id to watch
979
    @type period: int
980
    @param period: how often to poll for status (optional, default 5s)
981
    @type retries: int
982
    @param retries: how many time to poll before giving up
983
                    (optional, default -1 means unlimited)
984

985
    @rtype: bool
986
    @return: C{True} if job succeeded or C{False} if failed/status timeout
987
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
988
      possible; L{WaitForJobChange} returns immediately after a job changed and
989
      does not use polling
990

991
    """
992
    while retries != 0:
993
      job_result = self.GetJobStatus(job_id)
994

    
995
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
996
        return True
997
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
998
        return False
999

    
1000
      if period:
1001
        time.sleep(period)
1002

    
1003
      if retries > 0:
1004
        retries -= 1
1005

    
1006
    return False
1007

    
1008
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1009
    """Waits for job changes.
1010

1011
    @type job_id: string
1012
    @param job_id: Job ID for which to wait
1013
    @return: C{None} if no changes have been detected and a dict with two keys,
1014
      C{job_info} and C{log_entries} otherwise.
1015
    @rtype: dict
1016

1017
    """
1018
    body = {
1019
      "fields": fields,
1020
      "previous_job_info": prev_job_info,
1021
      "previous_log_serial": prev_log_serial,
1022
      }
1023

    
1024
    return self._SendRequest(HTTP_GET,
1025
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1026
                             None, body)
1027

    
1028
  def CancelJob(self, job_id, dry_run=False):
1029
    """Cancels a job.
1030

1031
    @type job_id: string
1032
    @param job_id: id of the job to delete
1033
    @type dry_run: bool
1034
    @param dry_run: whether to perform a dry run
1035
    @rtype: tuple
1036
    @return: tuple containing the result, and a message (bool, string)
1037

1038
    """
1039
    query = []
1040
    _AppendDryRunIf(query, dry_run)
1041

    
1042
    return self._SendRequest(HTTP_DELETE,
1043
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1044
                             query, None)
1045

    
1046
  def GetNodes(self, bulk=False):
1047
    """Gets all nodes in the cluster.
1048

1049
    @type bulk: bool
1050
    @param bulk: whether to return all information about all instances
1051

1052
    @rtype: list of dict or str
1053
    @return: if bulk is true, info about nodes in the cluster,
1054
        else list of nodes in the cluster
1055

1056
    """
1057
    query = []
1058
    _AppendIf(query, bulk, ("bulk", 1))
1059

    
1060
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1061
                              query, None)
1062
    if bulk:
1063
      return nodes
1064
    else:
1065
      return [n["id"] for n in nodes]
1066

    
1067
  def GetNode(self, node):
1068
    """Gets information about a node.
1069

1070
    @type node: str
1071
    @param node: node whose info to return
1072

1073
    @rtype: dict
1074
    @return: info about the node
1075

1076
    """
1077
    return self._SendRequest(HTTP_GET,
1078
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1079
                             None, None)
1080

    
1081
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1082
                   dry_run=False, early_release=None,
1083
                   mode=None, accept_old=False):
1084
    """Evacuates instances from a Ganeti node.
1085

1086
    @type node: str
1087
    @param node: node to evacuate
1088
    @type iallocator: str or None
1089
    @param iallocator: instance allocator to use
1090
    @type remote_node: str
1091
    @param remote_node: node to evaucate to
1092
    @type dry_run: bool
1093
    @param dry_run: whether to perform a dry run
1094
    @type early_release: bool
1095
    @param early_release: whether to enable parallelization
1096
    @type mode: string
1097
    @param mode: Node evacuation mode
1098
    @type accept_old: bool
1099
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1100
        results
1101

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

1108
    @raises GanetiApiError: if an iallocator and remote_node are both
1109
        specified
1110

1111
    """
1112
    if iallocator and remote_node:
1113
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1114

    
1115
    query = []
1116
    _AppendDryRunIf(query, dry_run)
1117

    
1118
    if _NODE_EVAC_RES1 in self.GetFeatures():
1119
      # Server supports body parameters
1120
      body = {}
1121

    
1122
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1123
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1124
      _SetItemIf(body, early_release is not None,
1125
                 "early_release", early_release)
1126
      _SetItemIf(body, mode is not None, "mode", mode)
1127
    else:
1128
      # Pre-2.5 request format
1129
      body = None
1130

    
1131
      if not accept_old:
1132
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1133
                             " not accept old-style results (parameter"
1134
                             " accept_old)")
1135

    
1136
      # Pre-2.5 servers can only evacuate secondaries
1137
      if mode is not None and mode != NODE_EVAC_SEC:
1138
        raise GanetiApiError("Server can only evacuate secondary instances")
1139

    
1140
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1141
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1142
      _AppendIf(query, early_release, ("early_release", 1))
1143

    
1144
    return self._SendRequest(HTTP_POST,
1145
                             ("/%s/nodes/%s/evacuate" %
1146
                              (GANETI_RAPI_VERSION, node)), query, body)
1147

    
1148
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1149
                  target_node=None):
1150
    """Migrates all primary instances from a node.
1151

1152
    @type node: str
1153
    @param node: node to migrate
1154
    @type mode: string
1155
    @param mode: if passed, it will overwrite the live migration type,
1156
        otherwise the hypervisor default will be used
1157
    @type dry_run: bool
1158
    @param dry_run: whether to perform a dry run
1159
    @type iallocator: string
1160
    @param iallocator: instance allocator to use
1161
    @type target_node: string
1162
    @param target_node: Target node for shared-storage instances
1163

1164
    @rtype: string
1165
    @return: job id
1166

1167
    """
1168
    query = []
1169
    _AppendDryRunIf(query, dry_run)
1170

    
1171
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1172
      body = {}
1173

    
1174
      _SetItemIf(body, mode is not None, "mode", mode)
1175
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1176
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1177

    
1178
      assert len(query) <= 1
1179

    
1180
      return self._SendRequest(HTTP_POST,
1181
                               ("/%s/nodes/%s/migrate" %
1182
                                (GANETI_RAPI_VERSION, node)), query, body)
1183
    else:
1184
      # Use old request format
1185
      if target_node is not None:
1186
        raise GanetiApiError("Server does not support specifying target node"
1187
                             " for node migration")
1188

    
1189
      _AppendIf(query, mode is not None, ("mode", mode))
1190

    
1191
      return self._SendRequest(HTTP_POST,
1192
                               ("/%s/nodes/%s/migrate" %
1193
                                (GANETI_RAPI_VERSION, node)), query, None)
1194

    
1195
  def GetNodeRole(self, node):
1196
    """Gets the current role for a node.
1197

1198
    @type node: str
1199
    @param node: node whose role to return
1200

1201
    @rtype: str
1202
    @return: the current role for a node
1203

1204
    """
1205
    return self._SendRequest(HTTP_GET,
1206
                             ("/%s/nodes/%s/role" %
1207
                              (GANETI_RAPI_VERSION, node)), None, None)
1208

    
1209
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1210
    """Sets the role for a node.
1211

1212
    @type node: str
1213
    @param node: the node whose role to set
1214
    @type role: str
1215
    @param role: the role to set for the node
1216
    @type force: bool
1217
    @param force: whether to force the role change
1218
    @type auto_promote: bool
1219
    @param auto_promote: Whether node(s) should be promoted to master candidate
1220
                         if necessary
1221

1222
    @rtype: string
1223
    @return: job id
1224

1225
    """
1226
    query = []
1227
    _AppendForceIf(query, force)
1228
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1229

    
1230
    return self._SendRequest(HTTP_PUT,
1231
                             ("/%s/nodes/%s/role" %
1232
                              (GANETI_RAPI_VERSION, node)), query, role)
1233

    
1234
  def PowercycleNode(self, node, force=False):
1235
    """Powercycles a node.
1236

1237
    @type node: string
1238
    @param node: Node name
1239
    @type force: bool
1240
    @param force: Whether to force the operation
1241
    @rtype: string
1242
    @return: job id
1243

1244
    """
1245
    query = []
1246
    _AppendForceIf(query, force)
1247

    
1248
    return self._SendRequest(HTTP_POST,
1249
                             ("/%s/nodes/%s/powercycle" %
1250
                              (GANETI_RAPI_VERSION, node)), query, None)
1251

    
1252
  def ModifyNode(self, node, **kwargs):
1253
    """Modifies a node.
1254

1255
    More details for parameters can be found in the RAPI documentation.
1256

1257
    @type node: string
1258
    @param node: Node name
1259
    @rtype: string
1260
    @return: job id
1261

1262
    """
1263
    return self._SendRequest(HTTP_POST,
1264
                             ("/%s/nodes/%s/modify" %
1265
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1266

    
1267
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1268
    """Gets the storage units for a node.
1269

1270
    @type node: str
1271
    @param node: the node whose storage units to return
1272
    @type storage_type: str
1273
    @param storage_type: storage type whose units to return
1274
    @type output_fields: str
1275
    @param output_fields: storage type fields to return
1276

1277
    @rtype: string
1278
    @return: job id where results can be retrieved
1279

1280
    """
1281
    query = [
1282
      ("storage_type", storage_type),
1283
      ("output_fields", output_fields),
1284
      ]
1285

    
1286
    return self._SendRequest(HTTP_GET,
1287
                             ("/%s/nodes/%s/storage" %
1288
                              (GANETI_RAPI_VERSION, node)), query, None)
1289

    
1290
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1291
    """Modifies parameters of storage units on the node.
1292

1293
    @type node: str
1294
    @param node: node whose storage units to modify
1295
    @type storage_type: str
1296
    @param storage_type: storage type whose units to modify
1297
    @type name: str
1298
    @param name: name of the storage unit
1299
    @type allocatable: bool or None
1300
    @param allocatable: Whether to set the "allocatable" flag on the storage
1301
                        unit (None=no modification, True=set, False=unset)
1302

1303
    @rtype: string
1304
    @return: job id
1305

1306
    """
1307
    query = [
1308
      ("storage_type", storage_type),
1309
      ("name", name),
1310
      ]
1311

    
1312
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1313

    
1314
    return self._SendRequest(HTTP_PUT,
1315
                             ("/%s/nodes/%s/storage/modify" %
1316
                              (GANETI_RAPI_VERSION, node)), query, None)
1317

    
1318
  def RepairNodeStorageUnits(self, node, storage_type, name):
1319
    """Repairs a storage unit on the node.
1320

1321
    @type node: str
1322
    @param node: node whose storage units to repair
1323
    @type storage_type: str
1324
    @param storage_type: storage type to repair
1325
    @type name: str
1326
    @param name: name of the storage unit to repair
1327

1328
    @rtype: string
1329
    @return: job id
1330

1331
    """
1332
    query = [
1333
      ("storage_type", storage_type),
1334
      ("name", name),
1335
      ]
1336

    
1337
    return self._SendRequest(HTTP_PUT,
1338
                             ("/%s/nodes/%s/storage/repair" %
1339
                              (GANETI_RAPI_VERSION, node)), query, None)
1340

    
1341
  def GetNodeTags(self, node):
1342
    """Gets the tags for a node.
1343

1344
    @type node: str
1345
    @param node: node whose tags to return
1346

1347
    @rtype: list of str
1348
    @return: tags for the node
1349

1350
    """
1351
    return self._SendRequest(HTTP_GET,
1352
                             ("/%s/nodes/%s/tags" %
1353
                              (GANETI_RAPI_VERSION, node)), None, None)
1354

    
1355
  def AddNodeTags(self, node, tags, dry_run=False):
1356
    """Adds tags to a node.
1357

1358
    @type node: str
1359
    @param node: node to add tags to
1360
    @type tags: list of str
1361
    @param tags: tags to add to the node
1362
    @type dry_run: bool
1363
    @param dry_run: whether to perform a dry run
1364

1365
    @rtype: string
1366
    @return: job id
1367

1368
    """
1369
    query = [("tag", t) for t in tags]
1370
    _AppendDryRunIf(query, dry_run)
1371

    
1372
    return self._SendRequest(HTTP_PUT,
1373
                             ("/%s/nodes/%s/tags" %
1374
                              (GANETI_RAPI_VERSION, node)), query, tags)
1375

    
1376
  def DeleteNodeTags(self, node, tags, dry_run=False):
1377
    """Delete tags from a node.
1378

1379
    @type node: str
1380
    @param node: node to remove tags from
1381
    @type tags: list of str
1382
    @param tags: tags to remove from the node
1383
    @type dry_run: bool
1384
    @param dry_run: whether to perform a dry run
1385

1386
    @rtype: string
1387
    @return: job id
1388

1389
    """
1390
    query = [("tag", t) for t in tags]
1391
    _AppendDryRunIf(query, dry_run)
1392

    
1393
    return self._SendRequest(HTTP_DELETE,
1394
                             ("/%s/nodes/%s/tags" %
1395
                              (GANETI_RAPI_VERSION, node)), query, None)
1396

    
1397
  def GetNetworks(self, bulk=False):
1398
    """Gets all networks in the cluster.
1399

1400
    @type bulk: bool
1401
    @param bulk: whether to return all information about the networks
1402

1403
    @rtype: list of dict or str
1404
    @return: if bulk is true, a list of dictionaries with info about all
1405
        networks in the cluster, else a list of names of those networks
1406

1407
    """
1408
    query = []
1409
    _AppendIf(query, bulk, ("bulk", 1))
1410

    
1411
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1412
                               query, None)
1413
    if bulk:
1414
      return networks
1415
    else:
1416
      return [n["name"] for n in networks]
1417

    
1418
  def GetNetwork(self, network):
1419
    """Gets information about a network.
1420

1421
    @type group: str
1422
    @param group: name of the network whose info to return
1423

1424
    @rtype: dict
1425
    @return: info about the network
1426

1427
    """
1428
    return self._SendRequest(HTTP_GET,
1429
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1430
                             None, None)
1431

    
1432
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1433
                    gateway6=None, mac_prefix=None,
1434
                    add_reserved_ips=None, tags=[],
1435
                    conflicts_check=False, dry_run=False):
1436
    """Creates a new network.
1437

1438
    @type name: str
1439
    @param name: the name of network to create
1440
    @type dry_run: bool
1441
    @param dry_run: whether to peform a dry run
1442

1443
    @rtype: string
1444
    @return: job id
1445

1446
    """
1447
    query = []
1448
    _AppendDryRunIf(query, dry_run)
1449

    
1450
    body = {
1451
      "network_name": network_name,
1452
      "gateway": gateway,
1453
      "network": network,
1454
      "gateway6": gateway6,
1455
      "network6": network6,
1456
      "mac_prefix": mac_prefix,
1457
      "add_reserved_ips": add_reserved_ips,
1458
      "conflicts_check": conflicts_check,
1459
      "tags": tags,
1460
      }
1461

    
1462
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1463
                             query, body)
1464

    
1465
  def ConnectNetwork(self, network_name, group_name, mode, link,
1466
                     conflicts_check=False, depends=None, dry_run=False):
1467
    """Connects a Network to a NodeGroup with the given netparams
1468

1469
    """
1470
    body = {
1471
      "group_name": group_name,
1472
      "network_mode": mode,
1473
      "network_link": link,
1474
      "conflicts_check": conflicts_check,
1475
      }
1476

    
1477
    if depends:
1478
      body['depends'] = depends
1479

    
1480
    query = []
1481
    _AppendDryRunIf(query, dry_run)
1482

    
1483
    return self._SendRequest(HTTP_PUT,
1484
                             ("/%s/networks/%s/connect" %
1485
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1486

    
1487
  def DisconnectNetwork(self, network_name, group_name,
1488
                        depends=None, dry_run=False):
1489
    """Connects a Network to a NodeGroup with the given netparams
1490

1491
    """
1492
    body = {
1493
      "group_name": group_name
1494
      }
1495

    
1496
    if depends:
1497
      body['depends'] = depends
1498

    
1499
    query = []
1500
    _AppendDryRunIf(query, dry_run)
1501

    
1502
    return self._SendRequest(HTTP_PUT,
1503
                             ("/%s/networks/%s/disconnect" %
1504
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1505

    
1506

    
1507
  def ModifyNetwork(self, network, **kwargs):
1508
    """Modifies a network.
1509

1510
    More details for parameters can be found in the RAPI documentation.
1511

1512
    @type network: string
1513
    @param network: Network name
1514
    @rtype: string
1515
    @return: job id
1516

1517
    """
1518
    return self._SendRequest(HTTP_PUT,
1519
                             ("/%s/networks/%s/modify" %
1520
                              (GANETI_RAPI_VERSION, network)), None, kwargs)
1521

    
1522
  def DeleteNetwork(self, network, depends=None, dry_run=False):
1523
    """Deletes a network.
1524

1525
    @type group: str
1526
    @param group: the network to delete
1527
    @type dry_run: bool
1528
    @param dry_run: whether to peform a dry run
1529

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

1533
    """
1534
    body = {}
1535
    if depends:
1536
      body['depends'] = depends
1537

    
1538
    query = []
1539
    _AppendDryRunIf(query, dry_run)
1540

    
1541
    return self._SendRequest(HTTP_DELETE,
1542
                             ("/%s/networks/%s" %
1543
                              (GANETI_RAPI_VERSION, network)), query, body)
1544

    
1545
  def GetNetworkTags(self, network):
1546
    """Gets tags for a network.
1547

1548
    @type network: string
1549
    @param network: Node group whose tags to return
1550

1551
    @rtype: list of strings
1552
    @return: tags for the network
1553

1554
    """
1555
    return self._SendRequest(HTTP_GET,
1556
                             ("/%s/networks/%s/tags" %
1557
                              (GANETI_RAPI_VERSION, network)), None, None)
1558

    
1559
  def AddNetworkTags(self, network, tags, dry_run=False):
1560
    """Adds tags to a network.
1561

1562
    @type network: str
1563
    @param network: network to add tags to
1564
    @type tags: list of string
1565
    @param tags: tags to add to the network
1566
    @type dry_run: bool
1567
    @param dry_run: whether to perform a dry run
1568

1569
    @rtype: string
1570
    @return: job id
1571

1572
    """
1573
    query = [("tag", t) for t in tags]
1574
    _AppendDryRunIf(query, dry_run)
1575

    
1576
    return self._SendRequest(HTTP_PUT,
1577
                             ("/%s/networks/%s/tags" %
1578
                              (GANETI_RAPI_VERSION, network)), query, None)
1579

    
1580
  def DeleteNetworkTags(self, network, tags, dry_run=False):
1581
    """Deletes tags from a network.
1582

1583
    @type network: str
1584
    @param network: network to delete tags from
1585
    @type tags: list of string
1586
    @param tags: tags to delete
1587
    @type dry_run: bool
1588
    @param dry_run: whether to perform a dry run
1589
    @rtype: string
1590
    @return: job id
1591

1592
    """
1593
    query = [("tag", t) for t in tags]
1594
    _AppendDryRunIf(query, dry_run)
1595

    
1596
    return self._SendRequest(HTTP_DELETE,
1597
                             ("/%s/networks/%s/tags" %
1598
                              (GANETI_RAPI_VERSION, network)), query, None)
1599

    
1600

    
1601
  def GetGroups(self, bulk=False):
1602
    """Gets all node groups in the cluster.
1603

1604
    @type bulk: bool
1605
    @param bulk: whether to return all information about the groups
1606

1607
    @rtype: list of dict or str
1608
    @return: if bulk is true, a list of dictionaries with info about all node
1609
        groups in the cluster, else a list of names of those node groups
1610

1611
    """
1612
    query = []
1613
    _AppendIf(query, bulk, ("bulk", 1))
1614

    
1615
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1616
                               query, None)
1617
    if bulk:
1618
      return groups
1619
    else:
1620
      return [g["name"] for g in groups]
1621

    
1622
  def GetGroup(self, group):
1623
    """Gets information about a node group.
1624

1625
    @type group: str
1626
    @param group: name of the node group whose info to return
1627

1628
    @rtype: dict
1629
    @return: info about the node group
1630

1631
    """
1632
    return self._SendRequest(HTTP_GET,
1633
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1634
                             None, None)
1635

    
1636
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1637
    """Creates a new node group.
1638

1639
    @type name: str
1640
    @param name: the name of node group to create
1641
    @type alloc_policy: str
1642
    @param alloc_policy: the desired allocation policy for the group, if any
1643
    @type dry_run: bool
1644
    @param dry_run: whether to peform a dry run
1645

1646
    @rtype: string
1647
    @return: job id
1648

1649
    """
1650
    query = []
1651
    _AppendDryRunIf(query, dry_run)
1652

    
1653
    body = {
1654
      "name": name,
1655
      "alloc_policy": alloc_policy
1656
      }
1657

    
1658
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1659
                             query, body)
1660

    
1661
  def ModifyGroup(self, group, **kwargs):
1662
    """Modifies a node group.
1663

1664
    More details for parameters can be found in the RAPI documentation.
1665

1666
    @type group: string
1667
    @param group: Node group name
1668
    @rtype: string
1669
    @return: job id
1670

1671
    """
1672
    return self._SendRequest(HTTP_PUT,
1673
                             ("/%s/groups/%s/modify" %
1674
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
1675

    
1676
  def DeleteGroup(self, group, dry_run=False):
1677
    """Deletes a node group.
1678

1679
    @type group: str
1680
    @param group: the node group to delete
1681
    @type dry_run: bool
1682
    @param dry_run: whether to peform a dry run
1683

1684
    @rtype: string
1685
    @return: job id
1686

1687
    """
1688
    query = []
1689
    _AppendDryRunIf(query, dry_run)
1690

    
1691
    return self._SendRequest(HTTP_DELETE,
1692
                             ("/%s/groups/%s" %
1693
                              (GANETI_RAPI_VERSION, group)), query, None)
1694

    
1695
  def RenameGroup(self, group, new_name):
1696
    """Changes the name of a node group.
1697

1698
    @type group: string
1699
    @param group: Node group name
1700
    @type new_name: string
1701
    @param new_name: New node group name
1702

1703
    @rtype: string
1704
    @return: job id
1705

1706
    """
1707
    body = {
1708
      "new_name": new_name,
1709
      }
1710

    
1711
    return self._SendRequest(HTTP_PUT,
1712
                             ("/%s/groups/%s/rename" %
1713
                              (GANETI_RAPI_VERSION, group)), None, body)
1714

    
1715
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1716
    """Assigns nodes to a group.
1717

1718
    @type group: string
1719
    @param group: Node gropu name
1720
    @type nodes: list of strings
1721
    @param nodes: List of nodes to assign to the group
1722

1723
    @rtype: string
1724
    @return: job id
1725

1726
    """
1727
    query = []
1728
    _AppendForceIf(query, force)
1729
    _AppendDryRunIf(query, dry_run)
1730

    
1731
    body = {
1732
      "nodes": nodes,
1733
      }
1734

    
1735
    return self._SendRequest(HTTP_PUT,
1736
                             ("/%s/groups/%s/assign-nodes" %
1737
                             (GANETI_RAPI_VERSION, group)), query, body)
1738

    
1739
  def GetGroupTags(self, group):
1740
    """Gets tags for a node group.
1741

1742
    @type group: string
1743
    @param group: Node group whose tags to return
1744

1745
    @rtype: list of strings
1746
    @return: tags for the group
1747

1748
    """
1749
    return self._SendRequest(HTTP_GET,
1750
                             ("/%s/groups/%s/tags" %
1751
                              (GANETI_RAPI_VERSION, group)), None, None)
1752

    
1753
  def AddGroupTags(self, group, tags, dry_run=False):
1754
    """Adds tags to a node group.
1755

1756
    @type group: str
1757
    @param group: group to add tags to
1758
    @type tags: list of string
1759
    @param tags: tags to add to the group
1760
    @type dry_run: bool
1761
    @param dry_run: whether to perform a dry run
1762

1763
    @rtype: string
1764
    @return: job id
1765

1766
    """
1767
    query = [("tag", t) for t in tags]
1768
    _AppendDryRunIf(query, dry_run)
1769

    
1770
    return self._SendRequest(HTTP_PUT,
1771
                             ("/%s/groups/%s/tags" %
1772
                              (GANETI_RAPI_VERSION, group)), query, None)
1773

    
1774
  def DeleteGroupTags(self, group, tags, dry_run=False):
1775
    """Deletes tags from a node group.
1776

1777
    @type group: str
1778
    @param group: group to delete tags from
1779
    @type tags: list of string
1780
    @param tags: tags to delete
1781
    @type dry_run: bool
1782
    @param dry_run: whether to perform a dry run
1783
    @rtype: string
1784
    @return: job id
1785

1786
    """
1787
    query = [("tag", t) for t in tags]
1788
    _AppendDryRunIf(query, dry_run)
1789

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

    
1794
  def Query(self, what, fields, qfilter=None):
1795
    """Retrieves information about resources.
1796

1797
    @type what: string
1798
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1799
    @type fields: list of string
1800
    @param fields: Requested fields
1801
    @type qfilter: None or list
1802
    @param qfilter: Query filter
1803

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

1807
    """
1808
    body = {
1809
      "fields": fields,
1810
      }
1811

    
1812
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
1813
    # TODO: remove "filter" after 2.7
1814
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
1815

    
1816
    return self._SendRequest(HTTP_PUT,
1817
                             ("/%s/query/%s" %
1818
                              (GANETI_RAPI_VERSION, what)), None, body)
1819

    
1820
  def QueryFields(self, what, fields=None):
1821
    """Retrieves available fields for a resource.
1822

1823
    @type what: string
1824
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1825
    @type fields: list of string
1826
    @param fields: Requested fields
1827

1828
    @rtype: string
1829
    @return: job id
1830

1831
    """
1832
    query = []
1833

    
1834
    if fields is not None:
1835
      _AppendIf(query, True, ("fields", ",".join(fields)))
1836

    
1837
    return self._SendRequest(HTTP_GET,
1838
                             ("/%s/query/%s/fields" %
1839
                              (GANETI_RAPI_VERSION, what)), query, None)