Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (52.1 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

    
99

    
100
class Error(Exception):
101
  """Base error class for this module.
102

103
  """
104
  pass
105

    
106

    
107
class GanetiApiError(Error):
108
  """Generic error raised from Ganeti API.
109

110
  """
111
  def __init__(self, msg, code=None):
112
    Error.__init__(self, msg)
113
    self.code = code
114

    
115

    
116
class CertificateError(GanetiApiError):
117
  """Raised when a problem is found with the SSL certificate.
118

119
  """
120
  pass
121

    
122

    
123
def _AppendIf(container, condition, value):
124
  """Appends to a list if a condition evaluates to truth.
125

126
  """
127
  if condition:
128
    container.append(value)
129

    
130
  return condition
131

    
132

    
133
def _AppendDryRunIf(container, condition):
134
  """Appends a "dry-run" parameter if a condition evaluates to truth.
135

136
  """
137
  return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
138

    
139

    
140
def _AppendForceIf(container, condition):
141
  """Appends a "force" parameter if a condition evaluates to truth.
142

143
  """
144
  return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
145

    
146

    
147
def _SetItemIf(container, condition, item, value):
148
  """Sets an item if a condition evaluates to truth.
149

150
  """
151
  if condition:
152
    container[item] = value
153

    
154
  return condition
155

    
156

    
157
class GanetiRapiClient(object): # pylint: disable=R0904
158
  """Ganeti RAPI client.
159

160
  """
161
  USER_AGENT = "Ganeti RAPI Client"
162
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
163

    
164
  def __init__(self, host, port=GANETI_RAPI_PORT,
165
               username=None, password=None, logger=logging):
166
    """Initializes this class.
167

168
    @type host: string
169
    @param host: the ganeti cluster master to interact with
170
    @type port: int
171
    @param port: the port on which the RAPI is running (default is 5080)
172
    @type username: string
173
    @param username: the username to connect with
174
    @type password: string
175
    @param password: the password to connect with
176
    @param logger: Logging object
177

178
    """
179
    self._logger = logger
180
    self._base_url = "https://%s:%s" % (host, port)
181

    
182
    if username is not None:
183
      if password is None:
184
        raise Error("Password not specified")
185
    elif password:
186
      raise Error("Specified password without username")
187

    
188
    self._auth = (username, password)
189

    
190
  def _SendRequest(self, method, path, query, content):
191
    """Sends an HTTP request.
192

193
    This constructs a full URL, encodes and decodes HTTP bodies, and
194
    handles invalid responses in a pythonic way.
195

196
    @type method: string
197
    @param method: HTTP method to use
198
    @type path: string
199
    @param path: HTTP URL path
200
    @type query: list of two-tuples
201
    @param query: query arguments to pass to urllib.urlencode
202
    @type content: str or None
203
    @param content: HTTP body content
204

205
    @rtype: str
206
    @return: JSON-Decoded response
207

208
    @raises CertificateError: If an invalid SSL certificate is found
209
    @raises GanetiApiError: If an invalid response is returned
210

211
    """
212
    assert path.startswith("/")
213
    url = "%s%s" % (self._base_url, path)
214

    
215
    headers = {}
216
    if content is not None:
217
      encoded_content = self._json_encoder.encode(content)
218
      headers = {"content-type": HTTP_APP_JSON,
219
                 "accept": HTTP_APP_JSON}
220
    else:
221
      encoded_content = ""
222

    
223
    if query is not None:
224
        query = dict(query)
225

    
226
    self._logger.debug("Sending request %s %s (query=%r) (content=%r)",
227
                       method, url, query, encoded_content)
228

    
229
    req_method = getattr(requests, method.lower())
230
    r = req_method(url, auth=self._auth, headers=headers, params=query,
231
                   data=encoded_content, verify=False)
232

    
233

    
234
    http_code = r.status_code
235
    if r.content is not None:
236
        response_content = simplejson.loads(r.content)
237
    else:
238
        response_content = None
239

    
240
    if http_code != HTTP_OK:
241
      if isinstance(response_content, dict):
242
        msg = ("%s %s: %s" %
243
               (response_content["code"],
244
                response_content["message"],
245
                response_content["explain"]))
246
      else:
247
        msg = str(response_content)
248

    
249
      raise GanetiApiError(msg, code=http_code)
250

    
251
    return response_content
252

    
253
  def GetVersion(self):
254
    """Gets the Remote API version running on the cluster.
255

256
    @rtype: int
257
    @return: Ganeti Remote API version
258

259
    """
260
    return self._SendRequest(HTTP_GET, "/version", None, None)
261

    
262
  def GetFeatures(self):
263
    """Gets the list of optional features supported by RAPI server.
264

265
    @rtype: list
266
    @return: List of optional features
267

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

    
277
      raise
278

    
279
  def GetOperatingSystems(self):
280
    """Gets the Operating Systems running in the Ganeti cluster.
281

282
    @rtype: list of str
283
    @return: operating systems
284

285
    """
286
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
287
                             None, None)
288

    
289
  def GetInfo(self):
290
    """Gets info about the cluster.
291

292
    @rtype: dict
293
    @return: information about the cluster
294

295
    """
296
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
297
                             None, None)
298

    
299
  def RedistributeConfig(self):
300
    """Tells the cluster to redistribute its configuration files.
301

302
    @rtype: string
303
    @return: job id
304

305
    """
306
    return self._SendRequest(HTTP_PUT,
307
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
308
                             None, None)
309

    
310
  def ModifyCluster(self, **kwargs):
311
    """Modifies cluster parameters.
312

313
    More details for parameters can be found in the RAPI documentation.
314

315
    @rtype: string
316
    @return: job id
317

318
    """
319
    body = kwargs
320

    
321
    return self._SendRequest(HTTP_PUT,
322
                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
323

    
324
  def GetClusterTags(self):
325
    """Gets the cluster tags.
326

327
    @rtype: list of str
328
    @return: cluster tags
329

330
    """
331
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
332
                             None, None)
333

    
334
  def AddClusterTags(self, tags, dry_run=False):
335
    """Adds tags to the cluster.
336

337
    @type tags: list of str
338
    @param tags: tags to add to the cluster
339
    @type dry_run: bool
340
    @param dry_run: whether to perform a dry run
341

342
    @rtype: string
343
    @return: job id
344

345
    """
346
    query = [("tag", t) for t in tags]
347
    _AppendDryRunIf(query, dry_run)
348

    
349
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
350
                             query, None)
351

    
352
  def DeleteClusterTags(self, tags, dry_run=False):
353
    """Deletes tags from the cluster.
354

355
    @type tags: list of str
356
    @param tags: tags to delete
357
    @type dry_run: bool
358
    @param dry_run: whether to perform a dry run
359
    @rtype: string
360
    @return: job id
361

362
    """
363
    query = [("tag", t) for t in tags]
364
    _AppendDryRunIf(query, dry_run)
365

    
366
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
367
                             query, None)
368

    
369
  def GetInstances(self, bulk=False):
370
    """Gets information about instances on the cluster.
371

372
    @type bulk: bool
373
    @param bulk: whether to return all information about all instances
374

375
    @rtype: list of dict or list of str
376
    @return: if bulk is True, info about the instances, else a list of instances
377

378
    """
379
    query = []
380
    _AppendIf(query, bulk, ("bulk", 1))
381

    
382
    instances = self._SendRequest(HTTP_GET,
383
                                  "/%s/instances" % GANETI_RAPI_VERSION,
384
                                  query, None)
385
    if bulk:
386
      return instances
387
    else:
388
      return [i["id"] for i in instances]
389

    
390
  def GetInstance(self, instance):
391
    """Gets information about an instance.
392

393
    @type instance: str
394
    @param instance: instance whose info to return
395

396
    @rtype: dict
397
    @return: info about the instance
398

399
    """
400
    return self._SendRequest(HTTP_GET,
401
                             ("/%s/instances/%s" %
402
                              (GANETI_RAPI_VERSION, instance)), None, None)
403

    
404
  def GetInstanceInfo(self, instance, static=None):
405
    """Gets information about an instance.
406

407
    @type instance: string
408
    @param instance: Instance name
409
    @rtype: string
410
    @return: Job ID
411

412
    """
413
    if static is not None:
414
      query = [("static", static)]
415
    else:
416
      query = None
417

    
418
    return self._SendRequest(HTTP_GET,
419
                             ("/%s/instances/%s/info" %
420
                              (GANETI_RAPI_VERSION, instance)), query, None)
421

    
422
  def CreateInstance(self, mode, name, disk_template, disks, nics,
423
                     **kwargs):
424
    """Creates a new instance.
425

426
    More details for parameters can be found in the RAPI documentation.
427

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

442
    @rtype: string
443
    @return: job id
444

445
    """
446
    query = []
447

    
448
    _AppendDryRunIf(query, kwargs.get("dry_run"))
449

    
450
    if _INST_CREATE_REQV1 in self.GetFeatures():
451
      # All required fields for request data version 1
452
      body = {
453
        _REQ_DATA_VERSION_FIELD: 1,
454
        "mode": mode,
455
        "name": name,
456
        "disk_template": disk_template,
457
        "disks": disks,
458
        "nics": nics,
459
        }
460

    
461
      conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
462
      if conflicts:
463
        raise GanetiApiError("Required fields can not be specified as"
464
                             " keywords: %s" % ", ".join(conflicts))
465

    
466
      body.update((key, value) for key, value in kwargs.iteritems()
467
                  if key != "dry_run")
468
    else:
469
      raise GanetiApiError("Server does not support new-style (version 1)"
470
                           " instance creation requests")
471

    
472
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
473
                             query, body)
474

    
475
  def DeleteInstance(self, instance, dry_run=False):
476
    """Deletes an instance.
477

478
    @type instance: str
479
    @param instance: the instance to delete
480

481
    @rtype: string
482
    @return: job id
483

484
    """
485
    query = []
486
    _AppendDryRunIf(query, dry_run)
487

    
488
    return self._SendRequest(HTTP_DELETE,
489
                             ("/%s/instances/%s" %
490
                              (GANETI_RAPI_VERSION, instance)), query, None)
491

    
492
  def ModifyInstance(self, instance, **kwargs):
493
    """Modifies an instance.
494

495
    More details for parameters can be found in the RAPI documentation.
496

497
    @type instance: string
498
    @param instance: Instance name
499
    @rtype: string
500
    @return: job id
501

502
    """
503
    body = kwargs
504

    
505
    return self._SendRequest(HTTP_PUT,
506
                             ("/%s/instances/%s/modify" %
507
                              (GANETI_RAPI_VERSION, instance)), None, body)
508

    
509
  def ActivateInstanceDisks(self, instance, ignore_size=None):
510
    """Activates an instance's disks.
511

512
    @type instance: string
513
    @param instance: Instance name
514
    @type ignore_size: bool
515
    @param ignore_size: Whether to ignore recorded size
516
    @rtype: string
517
    @return: job id
518

519
    """
520
    query = []
521
    _AppendIf(query, ignore_size, ("ignore_size", 1))
522

    
523
    return self._SendRequest(HTTP_PUT,
524
                             ("/%s/instances/%s/activate-disks" %
525
                              (GANETI_RAPI_VERSION, instance)), query, None)
526

    
527
  def DeactivateInstanceDisks(self, instance):
528
    """Deactivates an instance's disks.
529

530
    @type instance: string
531
    @param instance: Instance name
532
    @rtype: string
533
    @return: job id
534

535
    """
536
    return self._SendRequest(HTTP_PUT,
537
                             ("/%s/instances/%s/deactivate-disks" %
538
                              (GANETI_RAPI_VERSION, instance)), None, None)
539

    
540
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
541
    """Recreate an instance's disks.
542

543
    @type instance: string
544
    @param instance: Instance name
545
    @type disks: list of int
546
    @param disks: List of disk indexes
547
    @type nodes: list of string
548
    @param nodes: New instance nodes, if relocation is desired
549
    @rtype: string
550
    @return: job id
551

552
    """
553
    body = {}
554
    _SetItemIf(body, disks is not None, "disks", disks)
555
    _SetItemIf(body, nodes is not None, "nodes", nodes)
556

    
557
    return self._SendRequest(HTTP_POST,
558
                             ("/%s/instances/%s/recreate-disks" %
559
                              (GANETI_RAPI_VERSION, instance)), None, body)
560

    
561
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
562
    """Grows a disk of an instance.
563

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

566
    @type instance: string
567
    @param instance: Instance name
568
    @type disk: integer
569
    @param disk: Disk index
570
    @type amount: integer
571
    @param amount: Grow disk by this amount (MiB)
572
    @type wait_for_sync: bool
573
    @param wait_for_sync: Wait for disk to synchronize
574
    @rtype: string
575
    @return: job id
576

577
    """
578
    body = {
579
      "amount": amount,
580
      }
581

    
582
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
583

    
584
    return self._SendRequest(HTTP_POST,
585
                             ("/%s/instances/%s/disk/%s/grow" %
586
                              (GANETI_RAPI_VERSION, instance, disk)),
587
                             None, body)
588

    
589
  def GetInstanceTags(self, instance):
590
    """Gets tags for an instance.
591

592
    @type instance: str
593
    @param instance: instance whose tags to return
594

595
    @rtype: list of str
596
    @return: tags for the instance
597

598
    """
599
    return self._SendRequest(HTTP_GET,
600
                             ("/%s/instances/%s/tags" %
601
                              (GANETI_RAPI_VERSION, instance)), None, None)
602

    
603
  def AddInstanceTags(self, instance, tags, dry_run=False):
604
    """Adds tags to an instance.
605

606
    @type instance: str
607
    @param instance: instance to add tags to
608
    @type tags: list of str
609
    @param tags: tags to add to the instance
610
    @type dry_run: bool
611
    @param dry_run: whether to perform a dry run
612

613
    @rtype: string
614
    @return: job id
615

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

    
620
    return self._SendRequest(HTTP_PUT,
621
                             ("/%s/instances/%s/tags" %
622
                              (GANETI_RAPI_VERSION, instance)), query, None)
623

    
624
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
625
    """Deletes tags from an instance.
626

627
    @type instance: str
628
    @param instance: instance to delete tags from
629
    @type tags: list of str
630
    @param tags: tags to delete
631
    @type dry_run: bool
632
    @param dry_run: whether to perform a dry run
633
    @rtype: string
634
    @return: job id
635

636
    """
637
    query = [("tag", t) for t in tags]
638
    _AppendDryRunIf(query, dry_run)
639

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

    
644
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
645
                     dry_run=False):
646
    """Reboots an instance.
647

648
    @type instance: str
649
    @param instance: instance to rebot
650
    @type reboot_type: str
651
    @param reboot_type: one of: hard, soft, full
652
    @type ignore_secondaries: bool
653
    @param ignore_secondaries: if True, ignores errors for the secondary node
654
        while re-assembling disks (in hard-reboot mode only)
655
    @type dry_run: bool
656
    @param dry_run: whether to perform a dry run
657
    @rtype: string
658
    @return: job id
659

660
    """
661
    query = []
662
    _AppendDryRunIf(query, dry_run)
663
    _AppendIf(query, reboot_type, ("type", reboot_type))
664
    _AppendIf(query, ignore_secondaries is not None,
665
              ("ignore_secondaries", ignore_secondaries))
666

    
667
    return self._SendRequest(HTTP_POST,
668
                             ("/%s/instances/%s/reboot" %
669
                              (GANETI_RAPI_VERSION, instance)), query, None)
670

    
671
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
672
    """Shuts down an instance.
673

674
    @type instance: str
675
    @param instance: the instance to shut down
676
    @type dry_run: bool
677
    @param dry_run: whether to perform a dry run
678
    @type no_remember: bool
679
    @param no_remember: if true, will not record the state change
680
    @rtype: string
681
    @return: job id
682

683
    """
684
    query = []
685
    _AppendDryRunIf(query, dry_run)
686
    _AppendIf(query, no_remember, ("no-remember", 1))
687

    
688
    return self._SendRequest(HTTP_PUT,
689
                             ("/%s/instances/%s/shutdown" %
690
                              (GANETI_RAPI_VERSION, instance)), query, None)
691

    
692
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
693
    """Starts up an instance.
694

695
    @type instance: str
696
    @param instance: the instance to start up
697
    @type dry_run: bool
698
    @param dry_run: whether to perform a dry run
699
    @type no_remember: bool
700
    @param no_remember: if true, will not record the state change
701
    @rtype: string
702
    @return: job id
703

704
    """
705
    query = []
706
    _AppendDryRunIf(query, dry_run)
707
    _AppendIf(query, no_remember, ("no-remember", 1))
708

    
709
    return self._SendRequest(HTTP_PUT,
710
                             ("/%s/instances/%s/startup" %
711
                              (GANETI_RAPI_VERSION, instance)), query, None)
712

    
713
  def ReinstallInstance(self, instance, os=None, no_startup=False,
714
                        osparams=None):
715
    """Reinstalls an instance.
716

717
    @type instance: str
718
    @param instance: The instance to reinstall
719
    @type os: str or None
720
    @param os: The operating system to reinstall. If None, the instance's
721
        current operating system will be installed again
722
    @type no_startup: bool
723
    @param no_startup: Whether to start the instance automatically
724
    @rtype: string
725
    @return: job id
726

727
    """
728
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
729
      body = {
730
        "start": not no_startup,
731
        }
732
      _SetItemIf(body, os is not None, "os", os)
733
      _SetItemIf(body, osparams is not None, "osparams", osparams)
734
      return self._SendRequest(HTTP_POST,
735
                               ("/%s/instances/%s/reinstall" %
736
                                (GANETI_RAPI_VERSION, instance)), None, body)
737

    
738
    # Use old request format
739
    if osparams:
740
      raise GanetiApiError("Server does not support specifying OS parameters"
741
                           " for instance reinstallation")
742

    
743
    query = []
744
    _AppendIf(query, os, ("os", os))
745
    _AppendIf(query, no_startup, ("nostartup", 1))
746

    
747
    return self._SendRequest(HTTP_POST,
748
                             ("/%s/instances/%s/reinstall" %
749
                              (GANETI_RAPI_VERSION, instance)), query, None)
750

    
751
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
752
                           remote_node=None, iallocator=None):
753
    """Replaces disks on an instance.
754

755
    @type instance: str
756
    @param instance: instance whose disks to replace
757
    @type disks: list of ints
758
    @param disks: Indexes of disks to replace
759
    @type mode: str
760
    @param mode: replacement mode to use (defaults to replace_auto)
761
    @type remote_node: str or None
762
    @param remote_node: new secondary node to use (for use with
763
        replace_new_secondary mode)
764
    @type iallocator: str or None
765
    @param iallocator: instance allocator plugin to use (for use with
766
                       replace_auto mode)
767

768
    @rtype: string
769
    @return: job id
770

771
    """
772
    query = [
773
      ("mode", mode),
774
      ]
775

    
776
    # TODO: Convert to body parameters
777

    
778
    if disks is not None:
779
      _AppendIf(query, True,
780
                ("disks", ",".join(str(idx) for idx in disks)))
781

    
782
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
783
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
784

    
785
    return self._SendRequest(HTTP_POST,
786
                             ("/%s/instances/%s/replace-disks" %
787
                              (GANETI_RAPI_VERSION, instance)), query, None)
788

    
789
  def PrepareExport(self, instance, mode):
790
    """Prepares an instance for an export.
791

792
    @type instance: string
793
    @param instance: Instance name
794
    @type mode: string
795
    @param mode: Export mode
796
    @rtype: string
797
    @return: Job ID
798

799
    """
800
    query = [("mode", mode)]
801
    return self._SendRequest(HTTP_PUT,
802
                             ("/%s/instances/%s/prepare-export" %
803
                              (GANETI_RAPI_VERSION, instance)), query, None)
804

    
805
  def ExportInstance(self, instance, mode, destination, shutdown=None,
806
                     remove_instance=None,
807
                     x509_key_name=None, destination_x509_ca=None):
808
    """Exports an instance.
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
    body = {
819
      "destination": destination,
820
      "mode": mode,
821
      }
822

    
823
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
824
    _SetItemIf(body, remove_instance is not None,
825
               "remove_instance", remove_instance)
826
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
827
    _SetItemIf(body, destination_x509_ca is not None,
828
               "destination_x509_ca", destination_x509_ca)
829

    
830
    return self._SendRequest(HTTP_PUT,
831
                             ("/%s/instances/%s/export" %
832
                              (GANETI_RAPI_VERSION, instance)), None, body)
833

    
834
  def MigrateInstance(self, instance, mode=None, cleanup=None):
835
    """Migrates an instance.
836

837
    @type instance: string
838
    @param instance: Instance name
839
    @type mode: string
840
    @param mode: Migration mode
841
    @type cleanup: bool
842
    @param cleanup: Whether to clean up a previously failed migration
843
    @rtype: string
844
    @return: job id
845

846
    """
847
    body = {}
848
    _SetItemIf(body, mode is not None, "mode", mode)
849
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
850

    
851
    return self._SendRequest(HTTP_PUT,
852
                             ("/%s/instances/%s/migrate" %
853
                              (GANETI_RAPI_VERSION, instance)), None, body)
854

    
855
  def FailoverInstance(self, instance, iallocator=None,
856
                       ignore_consistency=None, target_node=None):
857
    """Does a failover of an instance.
858

859
    @type instance: string
860
    @param instance: Instance name
861
    @type iallocator: string
862
    @param iallocator: Iallocator for deciding the target node for
863
      shared-storage instances
864
    @type ignore_consistency: bool
865
    @param ignore_consistency: Whether to ignore disk consistency
866
    @type target_node: string
867
    @param target_node: Target node for shared-storage instances
868
    @rtype: string
869
    @return: job id
870

871
    """
872
    body = {}
873
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
874
    _SetItemIf(body, ignore_consistency is not None,
875
               "ignore_consistency", ignore_consistency)
876
    _SetItemIf(body, target_node is not None, "target_node", target_node)
877

    
878
    return self._SendRequest(HTTP_PUT,
879
                             ("/%s/instances/%s/failover" %
880
                              (GANETI_RAPI_VERSION, instance)), None, body)
881

    
882
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
883
    """Changes the name of an instance.
884

885
    @type instance: string
886
    @param instance: Instance name
887
    @type new_name: string
888
    @param new_name: New instance name
889
    @type ip_check: bool
890
    @param ip_check: Whether to ensure instance's IP address is inactive
891
    @type name_check: bool
892
    @param name_check: Whether to ensure instance's name is resolvable
893
    @rtype: string
894
    @return: job id
895

896
    """
897
    body = {
898
      "new_name": new_name,
899
      }
900

    
901
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
902
    _SetItemIf(body, name_check is not None, "name_check", name_check)
903

    
904
    return self._SendRequest(HTTP_PUT,
905
                             ("/%s/instances/%s/rename" %
906
                              (GANETI_RAPI_VERSION, instance)), None, body)
907

    
908
  def GetInstanceConsole(self, instance):
909
    """Request information for connecting to instance's console.
910

911
    @type instance: string
912
    @param instance: Instance name
913
    @rtype: dict
914
    @return: dictionary containing information about instance's console
915

916
    """
917
    return self._SendRequest(HTTP_GET,
918
                             ("/%s/instances/%s/console" %
919
                              (GANETI_RAPI_VERSION, instance)), None, None)
920

    
921
  def GetJobs(self, bulk=False):
922
    """Gets all jobs for the cluster.
923

924
    @rtype: list of int
925
    @return: job ids for the cluster
926

927
    """
928
    query = []
929
    _AppendIf(query, bulk, ("bulk", 1))
930

    
931
    jobs = self._SendRequest(HTTP_GET, "/%s/jobs" % GANETI_RAPI_VERSION,
932
                             query, None)
933
    if bulk:
934
        return jobs
935
    else:
936
        return [int(j["id"]) for j in jobs]
937

    
938

    
939
  def GetJobStatus(self, job_id):
940
    """Gets the status of a job.
941

942
    @type job_id: string
943
    @param job_id: job id whose status to query
944

945
    @rtype: dict
946
    @return: job status
947

948
    """
949
    return self._SendRequest(HTTP_GET,
950
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
951
                             None, None)
952

    
953
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
954
    """Polls cluster for job status until completion.
955

956
    Completion is defined as any of the following states listed in
957
    L{JOB_STATUS_FINALIZED}.
958

959
    @type job_id: string
960
    @param job_id: job id to watch
961
    @type period: int
962
    @param period: how often to poll for status (optional, default 5s)
963
    @type retries: int
964
    @param retries: how many time to poll before giving up
965
                    (optional, default -1 means unlimited)
966

967
    @rtype: bool
968
    @return: C{True} if job succeeded or C{False} if failed/status timeout
969
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
970
      possible; L{WaitForJobChange} returns immediately after a job changed and
971
      does not use polling
972

973
    """
974
    while retries != 0:
975
      job_result = self.GetJobStatus(job_id)
976

    
977
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
978
        return True
979
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
980
        return False
981

    
982
      if period:
983
        time.sleep(period)
984

    
985
      if retries > 0:
986
        retries -= 1
987

    
988
    return False
989

    
990
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
991
    """Waits for job changes.
992

993
    @type job_id: string
994
    @param job_id: Job ID for which to wait
995
    @return: C{None} if no changes have been detected and a dict with two keys,
996
      C{job_info} and C{log_entries} otherwise.
997
    @rtype: dict
998

999
    """
1000
    body = {
1001
      "fields": fields,
1002
      "previous_job_info": prev_job_info,
1003
      "previous_log_serial": prev_log_serial,
1004
      }
1005

    
1006
    return self._SendRequest(HTTP_GET,
1007
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1008
                             None, body)
1009

    
1010
  def CancelJob(self, job_id, dry_run=False):
1011
    """Cancels a job.
1012

1013
    @type job_id: string
1014
    @param job_id: id of the job to delete
1015
    @type dry_run: bool
1016
    @param dry_run: whether to perform a dry run
1017
    @rtype: tuple
1018
    @return: tuple containing the result, and a message (bool, string)
1019

1020
    """
1021
    query = []
1022
    _AppendDryRunIf(query, dry_run)
1023

    
1024
    return self._SendRequest(HTTP_DELETE,
1025
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1026
                             query, None)
1027

    
1028
  def GetNodes(self, bulk=False):
1029
    """Gets all nodes in the cluster.
1030

1031
    @type bulk: bool
1032
    @param bulk: whether to return all information about all instances
1033

1034
    @rtype: list of dict or str
1035
    @return: if bulk is true, info about nodes in the cluster,
1036
        else list of nodes in the cluster
1037

1038
    """
1039
    query = []
1040
    _AppendIf(query, bulk, ("bulk", 1))
1041

    
1042
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1043
                              query, None)
1044
    if bulk:
1045
      return nodes
1046
    else:
1047
      return [n["id"] for n in nodes]
1048

    
1049
  def GetNode(self, node):
1050
    """Gets information about a node.
1051

1052
    @type node: str
1053
    @param node: node whose info to return
1054

1055
    @rtype: dict
1056
    @return: info about the node
1057

1058
    """
1059
    return self._SendRequest(HTTP_GET,
1060
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1061
                             None, None)
1062

    
1063
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1064
                   dry_run=False, early_release=None,
1065
                   mode=None, accept_old=False):
1066
    """Evacuates instances from a Ganeti node.
1067

1068
    @type node: str
1069
    @param node: node to evacuate
1070
    @type iallocator: str or None
1071
    @param iallocator: instance allocator to use
1072
    @type remote_node: str
1073
    @param remote_node: node to evaucate to
1074
    @type dry_run: bool
1075
    @param dry_run: whether to perform a dry run
1076
    @type early_release: bool
1077
    @param early_release: whether to enable parallelization
1078
    @type mode: string
1079
    @param mode: Node evacuation mode
1080
    @type accept_old: bool
1081
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1082
        results
1083

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

1090
    @raises GanetiApiError: if an iallocator and remote_node are both
1091
        specified
1092

1093
    """
1094
    if iallocator and remote_node:
1095
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1096

    
1097
    query = []
1098
    _AppendDryRunIf(query, dry_run)
1099

    
1100
    if _NODE_EVAC_RES1 in self.GetFeatures():
1101
      # Server supports body parameters
1102
      body = {}
1103

    
1104
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1105
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1106
      _SetItemIf(body, early_release is not None,
1107
                 "early_release", early_release)
1108
      _SetItemIf(body, mode is not None, "mode", mode)
1109
    else:
1110
      # Pre-2.5 request format
1111
      body = None
1112

    
1113
      if not accept_old:
1114
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1115
                             " not accept old-style results (parameter"
1116
                             " accept_old)")
1117

    
1118
      # Pre-2.5 servers can only evacuate secondaries
1119
      if mode is not None and mode != NODE_EVAC_SEC:
1120
        raise GanetiApiError("Server can only evacuate secondary instances")
1121

    
1122
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1123
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1124
      _AppendIf(query, early_release, ("early_release", 1))
1125

    
1126
    return self._SendRequest(HTTP_POST,
1127
                             ("/%s/nodes/%s/evacuate" %
1128
                              (GANETI_RAPI_VERSION, node)), query, body)
1129

    
1130
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1131
                  target_node=None):
1132
    """Migrates all primary instances from a node.
1133

1134
    @type node: str
1135
    @param node: node to migrate
1136
    @type mode: string
1137
    @param mode: if passed, it will overwrite the live migration type,
1138
        otherwise the hypervisor default will be used
1139
    @type dry_run: bool
1140
    @param dry_run: whether to perform a dry run
1141
    @type iallocator: string
1142
    @param iallocator: instance allocator to use
1143
    @type target_node: string
1144
    @param target_node: Target node for shared-storage instances
1145

1146
    @rtype: string
1147
    @return: job id
1148

1149
    """
1150
    query = []
1151
    _AppendDryRunIf(query, dry_run)
1152

    
1153
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1154
      body = {}
1155

    
1156
      _SetItemIf(body, mode is not None, "mode", mode)
1157
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1158
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1159

    
1160
      assert len(query) <= 1
1161

    
1162
      return self._SendRequest(HTTP_POST,
1163
                               ("/%s/nodes/%s/migrate" %
1164
                                (GANETI_RAPI_VERSION, node)), query, body)
1165
    else:
1166
      # Use old request format
1167
      if target_node is not None:
1168
        raise GanetiApiError("Server does not support specifying target node"
1169
                             " for node migration")
1170

    
1171
      _AppendIf(query, mode is not None, ("mode", mode))
1172

    
1173
      return self._SendRequest(HTTP_POST,
1174
                               ("/%s/nodes/%s/migrate" %
1175
                                (GANETI_RAPI_VERSION, node)), query, None)
1176

    
1177
  def GetNodeRole(self, node):
1178
    """Gets the current role for a node.
1179

1180
    @type node: str
1181
    @param node: node whose role to return
1182

1183
    @rtype: str
1184
    @return: the current role for a node
1185

1186
    """
1187
    return self._SendRequest(HTTP_GET,
1188
                             ("/%s/nodes/%s/role" %
1189
                              (GANETI_RAPI_VERSION, node)), None, None)
1190

    
1191
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1192
    """Sets the role for a node.
1193

1194
    @type node: str
1195
    @param node: the node whose role to set
1196
    @type role: str
1197
    @param role: the role to set for the node
1198
    @type force: bool
1199
    @param force: whether to force the role change
1200
    @type auto_promote: bool
1201
    @param auto_promote: Whether node(s) should be promoted to master candidate
1202
                         if necessary
1203

1204
    @rtype: string
1205
    @return: job id
1206

1207
    """
1208
    query = []
1209
    _AppendForceIf(query, force)
1210
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1211

    
1212
    return self._SendRequest(HTTP_PUT,
1213
                             ("/%s/nodes/%s/role" %
1214
                              (GANETI_RAPI_VERSION, node)), query, role)
1215

    
1216
  def PowercycleNode(self, node, force=False):
1217
    """Powercycles a node.
1218

1219
    @type node: string
1220
    @param node: Node name
1221
    @type force: bool
1222
    @param force: Whether to force the operation
1223
    @rtype: string
1224
    @return: job id
1225

1226
    """
1227
    query = []
1228
    _AppendForceIf(query, force)
1229

    
1230
    return self._SendRequest(HTTP_POST,
1231
                             ("/%s/nodes/%s/powercycle" %
1232
                              (GANETI_RAPI_VERSION, node)), query, None)
1233

    
1234
  def ModifyNode(self, node, **kwargs):
1235
    """Modifies a node.
1236

1237
    More details for parameters can be found in the RAPI documentation.
1238

1239
    @type node: string
1240
    @param node: Node name
1241
    @rtype: string
1242
    @return: job id
1243

1244
    """
1245
    return self._SendRequest(HTTP_POST,
1246
                             ("/%s/nodes/%s/modify" %
1247
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1248

    
1249
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1250
    """Gets the storage units for a node.
1251

1252
    @type node: str
1253
    @param node: the node whose storage units to return
1254
    @type storage_type: str
1255
    @param storage_type: storage type whose units to return
1256
    @type output_fields: str
1257
    @param output_fields: storage type fields to return
1258

1259
    @rtype: string
1260
    @return: job id where results can be retrieved
1261

1262
    """
1263
    query = [
1264
      ("storage_type", storage_type),
1265
      ("output_fields", output_fields),
1266
      ]
1267

    
1268
    return self._SendRequest(HTTP_GET,
1269
                             ("/%s/nodes/%s/storage" %
1270
                              (GANETI_RAPI_VERSION, node)), query, None)
1271

    
1272
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1273
    """Modifies parameters of storage units on the node.
1274

1275
    @type node: str
1276
    @param node: node whose storage units to modify
1277
    @type storage_type: str
1278
    @param storage_type: storage type whose units to modify
1279
    @type name: str
1280
    @param name: name of the storage unit
1281
    @type allocatable: bool or None
1282
    @param allocatable: Whether to set the "allocatable" flag on the storage
1283
                        unit (None=no modification, True=set, False=unset)
1284

1285
    @rtype: string
1286
    @return: job id
1287

1288
    """
1289
    query = [
1290
      ("storage_type", storage_type),
1291
      ("name", name),
1292
      ]
1293

    
1294
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1295

    
1296
    return self._SendRequest(HTTP_PUT,
1297
                             ("/%s/nodes/%s/storage/modify" %
1298
                              (GANETI_RAPI_VERSION, node)), query, None)
1299

    
1300
  def RepairNodeStorageUnits(self, node, storage_type, name):
1301
    """Repairs a storage unit on the node.
1302

1303
    @type node: str
1304
    @param node: node whose storage units to repair
1305
    @type storage_type: str
1306
    @param storage_type: storage type to repair
1307
    @type name: str
1308
    @param name: name of the storage unit to repair
1309

1310
    @rtype: string
1311
    @return: job id
1312

1313
    """
1314
    query = [
1315
      ("storage_type", storage_type),
1316
      ("name", name),
1317
      ]
1318

    
1319
    return self._SendRequest(HTTP_PUT,
1320
                             ("/%s/nodes/%s/storage/repair" %
1321
                              (GANETI_RAPI_VERSION, node)), query, None)
1322

    
1323
  def GetNodeTags(self, node):
1324
    """Gets the tags for a node.
1325

1326
    @type node: str
1327
    @param node: node whose tags to return
1328

1329
    @rtype: list of str
1330
    @return: tags for the node
1331

1332
    """
1333
    return self._SendRequest(HTTP_GET,
1334
                             ("/%s/nodes/%s/tags" %
1335
                              (GANETI_RAPI_VERSION, node)), None, None)
1336

    
1337
  def AddNodeTags(self, node, tags, dry_run=False):
1338
    """Adds tags to a node.
1339

1340
    @type node: str
1341
    @param node: node to add tags to
1342
    @type tags: list of str
1343
    @param tags: tags to add to the node
1344
    @type dry_run: bool
1345
    @param dry_run: whether to perform a dry run
1346

1347
    @rtype: string
1348
    @return: job id
1349

1350
    """
1351
    query = [("tag", t) for t in tags]
1352
    _AppendDryRunIf(query, dry_run)
1353

    
1354
    return self._SendRequest(HTTP_PUT,
1355
                             ("/%s/nodes/%s/tags" %
1356
                              (GANETI_RAPI_VERSION, node)), query, tags)
1357

    
1358
  def DeleteNodeTags(self, node, tags, dry_run=False):
1359
    """Delete tags from a node.
1360

1361
    @type node: str
1362
    @param node: node to remove tags from
1363
    @type tags: list of str
1364
    @param tags: tags to remove from the node
1365
    @type dry_run: bool
1366
    @param dry_run: whether to perform a dry run
1367

1368
    @rtype: string
1369
    @return: job id
1370

1371
    """
1372
    query = [("tag", t) for t in tags]
1373
    _AppendDryRunIf(query, dry_run)
1374

    
1375
    return self._SendRequest(HTTP_DELETE,
1376
                             ("/%s/nodes/%s/tags" %
1377
                              (GANETI_RAPI_VERSION, node)), query, None)
1378

    
1379
  def GetNetworks(self, bulk=False):
1380
    """Gets all networks in the cluster.
1381

1382
    @type bulk: bool
1383
    @param bulk: whether to return all information about the networks
1384

1385
    @rtype: list of dict or str
1386
    @return: if bulk is true, a list of dictionaries with info about all
1387
        networks in the cluster, else a list of names of those networks
1388

1389
    """
1390
    query = []
1391
    _AppendIf(query, bulk, ("bulk", 1))
1392

    
1393
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1394
                               query, None)
1395
    if bulk:
1396
      return networks
1397
    else:
1398
      return [n["name"] for n in networks]
1399

    
1400
  def GetNetwork(self, network):
1401
    """Gets information about a network.
1402

1403
    @type group: str
1404
    @param group: name of the network whose info to return
1405

1406
    @rtype: dict
1407
    @return: info about the network
1408

1409
    """
1410
    return self._SendRequest(HTTP_GET,
1411
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1412
                             None, None)
1413

    
1414
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1415
                    gateway6=None, mac_prefix=None,
1416
                    add_reserved_ips=None, tags=[],
1417
                    conflicts_check=False, dry_run=False):
1418
    """Creates a new network.
1419

1420
    @type name: str
1421
    @param name: the name of network to create
1422
    @type dry_run: bool
1423
    @param dry_run: whether to peform a dry run
1424

1425
    @rtype: string
1426
    @return: job id
1427

1428
    """
1429
    query = []
1430
    _AppendDryRunIf(query, dry_run)
1431

    
1432
    body = {
1433
      "network_name": network_name,
1434
      "gateway": gateway,
1435
      "network": network,
1436
      "gateway6": gateway6,
1437
      "network6": network6,
1438
      "mac_prefix": mac_prefix,
1439
      "add_reserved_ips": add_reserved_ips,
1440
      "conflicts_check": conflicts_check,
1441
      "tags": tags,
1442
      }
1443

    
1444
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1445
                             query, body)
1446

    
1447
  def ConnectNetwork(self, network_name, group_name, mode, link,
1448
                     conflicts_check=False, depends=None, dry_run=False):
1449
    """Connects a Network to a NodeGroup with the given netparams
1450

1451
    """
1452
    body = {
1453
      "group_name": group_name,
1454
      "network_mode": mode,
1455
      "network_link": link,
1456
      "conflicts_check": conflicts_check,
1457
      }
1458

    
1459
    if depends:
1460
      body['depends'] = depends
1461

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

    
1465
    return self._SendRequest(HTTP_PUT,
1466
                             ("/%s/networks/%s/connect" %
1467
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1468

    
1469
  def DisconnectNetwork(self, network_name, group_name,
1470
                        depends=None, dry_run=False):
1471
    """Connects a Network to a NodeGroup with the given netparams
1472

1473
    """
1474
    body = {
1475
      "group_name": group_name
1476
      }
1477

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

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

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

    
1488

    
1489
  def ModifyNetwork(self, network, **kwargs):
1490
    """Modifies a network.
1491

1492
    More details for parameters can be found in the RAPI documentation.
1493

1494
    @type network: string
1495
    @param network: Network name
1496
    @rtype: string
1497
    @return: job id
1498

1499
    """
1500
    return self._SendRequest(HTTP_PUT,
1501
                             ("/%s/networks/%s/modify" %
1502
                              (GANETI_RAPI_VERSION, network)), None, kwargs)
1503

    
1504
  def DeleteNetwork(self, network, depends=None, dry_run=False):
1505
    """Deletes a network.
1506

1507
    @type group: str
1508
    @param group: the network to delete
1509
    @type dry_run: bool
1510
    @param dry_run: whether to peform a dry run
1511

1512
    @rtype: string
1513
    @return: job id
1514

1515
    """
1516
    body = {}
1517
    if depends:
1518
      body['depends'] = depends
1519

    
1520
    query = []
1521
    _AppendDryRunIf(query, dry_run)
1522

    
1523
    return self._SendRequest(HTTP_DELETE,
1524
                             ("/%s/networks/%s" %
1525
                              (GANETI_RAPI_VERSION, network)), query, body)
1526

    
1527
  def GetNetworkTags(self, network):
1528
    """Gets tags for a network.
1529

1530
    @type network: string
1531
    @param network: Node group whose tags to return
1532

1533
    @rtype: list of strings
1534
    @return: tags for the network
1535

1536
    """
1537
    return self._SendRequest(HTTP_GET,
1538
                             ("/%s/networks/%s/tags" %
1539
                              (GANETI_RAPI_VERSION, network)), None, None)
1540

    
1541
  def AddNetworkTags(self, network, tags, dry_run=False):
1542
    """Adds tags to a network.
1543

1544
    @type network: str
1545
    @param network: network to add tags to
1546
    @type tags: list of string
1547
    @param tags: tags to add to the network
1548
    @type dry_run: bool
1549
    @param dry_run: whether to perform a dry run
1550

1551
    @rtype: string
1552
    @return: job id
1553

1554
    """
1555
    query = [("tag", t) for t in tags]
1556
    _AppendDryRunIf(query, dry_run)
1557

    
1558
    return self._SendRequest(HTTP_PUT,
1559
                             ("/%s/networks/%s/tags" %
1560
                              (GANETI_RAPI_VERSION, network)), query, None)
1561

    
1562
  def DeleteNetworkTags(self, network, tags, dry_run=False):
1563
    """Deletes tags from a network.
1564

1565
    @type network: str
1566
    @param network: network to delete tags from
1567
    @type tags: list of string
1568
    @param tags: tags to delete
1569
    @type dry_run: bool
1570
    @param dry_run: whether to perform a dry run
1571
    @rtype: string
1572
    @return: job id
1573

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

    
1578
    return self._SendRequest(HTTP_DELETE,
1579
                             ("/%s/networks/%s/tags" %
1580
                              (GANETI_RAPI_VERSION, network)), query, None)
1581

    
1582

    
1583
  def GetGroups(self, bulk=False):
1584
    """Gets all node groups in the cluster.
1585

1586
    @type bulk: bool
1587
    @param bulk: whether to return all information about the groups
1588

1589
    @rtype: list of dict or str
1590
    @return: if bulk is true, a list of dictionaries with info about all node
1591
        groups in the cluster, else a list of names of those node groups
1592

1593
    """
1594
    query = []
1595
    _AppendIf(query, bulk, ("bulk", 1))
1596

    
1597
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1598
                               query, None)
1599
    if bulk:
1600
      return groups
1601
    else:
1602
      return [g["name"] for g in groups]
1603

    
1604
  def GetGroup(self, group):
1605
    """Gets information about a node group.
1606

1607
    @type group: str
1608
    @param group: name of the node group whose info to return
1609

1610
    @rtype: dict
1611
    @return: info about the node group
1612

1613
    """
1614
    return self._SendRequest(HTTP_GET,
1615
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1616
                             None, None)
1617

    
1618
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1619
    """Creates a new node group.
1620

1621
    @type name: str
1622
    @param name: the name of node group to create
1623
    @type alloc_policy: str
1624
    @param alloc_policy: the desired allocation policy for the group, if any
1625
    @type dry_run: bool
1626
    @param dry_run: whether to peform a dry run
1627

1628
    @rtype: string
1629
    @return: job id
1630

1631
    """
1632
    query = []
1633
    _AppendDryRunIf(query, dry_run)
1634

    
1635
    body = {
1636
      "name": name,
1637
      "alloc_policy": alloc_policy
1638
      }
1639

    
1640
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1641
                             query, body)
1642

    
1643
  def ModifyGroup(self, group, **kwargs):
1644
    """Modifies a node group.
1645

1646
    More details for parameters can be found in the RAPI documentation.
1647

1648
    @type group: string
1649
    @param group: Node group name
1650
    @rtype: string
1651
    @return: job id
1652

1653
    """
1654
    return self._SendRequest(HTTP_PUT,
1655
                             ("/%s/groups/%s/modify" %
1656
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
1657

    
1658
  def DeleteGroup(self, group, dry_run=False):
1659
    """Deletes a node group.
1660

1661
    @type group: str
1662
    @param group: the node group to delete
1663
    @type dry_run: bool
1664
    @param dry_run: whether to peform a dry run
1665

1666
    @rtype: string
1667
    @return: job id
1668

1669
    """
1670
    query = []
1671
    _AppendDryRunIf(query, dry_run)
1672

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

    
1677
  def RenameGroup(self, group, new_name):
1678
    """Changes the name of a node group.
1679

1680
    @type group: string
1681
    @param group: Node group name
1682
    @type new_name: string
1683
    @param new_name: New node group name
1684

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

1688
    """
1689
    body = {
1690
      "new_name": new_name,
1691
      }
1692

    
1693
    return self._SendRequest(HTTP_PUT,
1694
                             ("/%s/groups/%s/rename" %
1695
                              (GANETI_RAPI_VERSION, group)), None, body)
1696

    
1697
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1698
    """Assigns nodes to a group.
1699

1700
    @type group: string
1701
    @param group: Node gropu name
1702
    @type nodes: list of strings
1703
    @param nodes: List of nodes to assign to the group
1704

1705
    @rtype: string
1706
    @return: job id
1707

1708
    """
1709
    query = []
1710
    _AppendForceIf(query, force)
1711
    _AppendDryRunIf(query, dry_run)
1712

    
1713
    body = {
1714
      "nodes": nodes,
1715
      }
1716

    
1717
    return self._SendRequest(HTTP_PUT,
1718
                             ("/%s/groups/%s/assign-nodes" %
1719
                             (GANETI_RAPI_VERSION, group)), query, body)
1720

    
1721
  def GetGroupTags(self, group):
1722
    """Gets tags for a node group.
1723

1724
    @type group: string
1725
    @param group: Node group whose tags to return
1726

1727
    @rtype: list of strings
1728
    @return: tags for the group
1729

1730
    """
1731
    return self._SendRequest(HTTP_GET,
1732
                             ("/%s/groups/%s/tags" %
1733
                              (GANETI_RAPI_VERSION, group)), None, None)
1734

    
1735
  def AddGroupTags(self, group, tags, dry_run=False):
1736
    """Adds tags to a node group.
1737

1738
    @type group: str
1739
    @param group: group to add tags to
1740
    @type tags: list of string
1741
    @param tags: tags to add to the group
1742
    @type dry_run: bool
1743
    @param dry_run: whether to perform a dry run
1744

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

1748
    """
1749
    query = [("tag", t) for t in tags]
1750
    _AppendDryRunIf(query, dry_run)
1751

    
1752
    return self._SendRequest(HTTP_PUT,
1753
                             ("/%s/groups/%s/tags" %
1754
                              (GANETI_RAPI_VERSION, group)), query, None)
1755

    
1756
  def DeleteGroupTags(self, group, tags, dry_run=False):
1757
    """Deletes tags from a node group.
1758

1759
    @type group: str
1760
    @param group: group to delete tags from
1761
    @type tags: list of string
1762
    @param tags: tags to delete
1763
    @type dry_run: bool
1764
    @param dry_run: whether to perform a dry run
1765
    @rtype: string
1766
    @return: job id
1767

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

    
1772
    return self._SendRequest(HTTP_DELETE,
1773
                             ("/%s/groups/%s/tags" %
1774
                              (GANETI_RAPI_VERSION, group)), query, None)
1775

    
1776
  def Query(self, what, fields, qfilter=None):
1777
    """Retrieves information about resources.
1778

1779
    @type what: string
1780
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1781
    @type fields: list of string
1782
    @param fields: Requested fields
1783
    @type qfilter: None or list
1784
    @param qfilter: Query filter
1785

1786
    @rtype: string
1787
    @return: job id
1788

1789
    """
1790
    body = {
1791
      "fields": fields,
1792
      }
1793

    
1794
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
1795
    # TODO: remove "filter" after 2.7
1796
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
1797

    
1798
    return self._SendRequest(HTTP_PUT,
1799
                             ("/%s/query/%s" %
1800
                              (GANETI_RAPI_VERSION, what)), None, body)
1801

    
1802
  def QueryFields(self, what, fields=None):
1803
    """Retrieves available fields for a resource.
1804

1805
    @type what: string
1806
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1807
    @type fields: list of string
1808
    @param fields: Requested fields
1809

1810
    @rtype: string
1811
    @return: job id
1812

1813
    """
1814
    query = []
1815

    
1816
    if fields is not None:
1817
      _AppendIf(query, True, ("fields", ",".join(fields)))
1818

    
1819
    return self._SendRequest(HTTP_GET,
1820
                             ("/%s/query/%s/fields" %
1821
                              (GANETI_RAPI_VERSION, what)), query, None)