Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (52.5 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 cannot 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, shutdown_timeout=None):
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
    body = None
489
    if shutdown_timeout is not None:
490
        body = {"shutdown_timeout": shutdown_timeout}
491

    
492
    return self._SendRequest(HTTP_DELETE,
493
                             ("/%s/instances/%s" %
494
                              (GANETI_RAPI_VERSION, instance)), query, body)
495

    
496
  def ModifyInstance(self, instance, **kwargs):
497
    """Modifies an instance.
498

499
    More details for parameters can be found in the RAPI documentation.
500

501
    @type instance: string
502
    @param instance: Instance name
503
    @rtype: string
504
    @return: job id
505

506
    """
507
    body = kwargs
508

    
509
    return self._SendRequest(HTTP_PUT,
510
                             ("/%s/instances/%s/modify" %
511
                              (GANETI_RAPI_VERSION, instance)), None, body)
512

    
513
  def ActivateInstanceDisks(self, instance, ignore_size=None):
514
    """Activates an instance's disks.
515

516
    @type instance: string
517
    @param instance: Instance name
518
    @type ignore_size: bool
519
    @param ignore_size: Whether to ignore recorded size
520
    @rtype: string
521
    @return: job id
522

523
    """
524
    query = []
525
    _AppendIf(query, ignore_size, ("ignore_size", 1))
526

    
527
    return self._SendRequest(HTTP_PUT,
528
                             ("/%s/instances/%s/activate-disks" %
529
                              (GANETI_RAPI_VERSION, instance)), query, None)
530

    
531
  def DeactivateInstanceDisks(self, instance):
532
    """Deactivates an instance's disks.
533

534
    @type instance: string
535
    @param instance: Instance name
536
    @rtype: string
537
    @return: job id
538

539
    """
540
    return self._SendRequest(HTTP_PUT,
541
                             ("/%s/instances/%s/deactivate-disks" %
542
                              (GANETI_RAPI_VERSION, instance)), None, None)
543

    
544
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
545
    """Recreate an instance's disks.
546

547
    @type instance: string
548
    @param instance: Instance name
549
    @type disks: list of int
550
    @param disks: List of disk indexes
551
    @type nodes: list of string
552
    @param nodes: New instance nodes, if relocation is desired
553
    @rtype: string
554
    @return: job id
555

556
    """
557
    body = {}
558
    _SetItemIf(body, disks is not None, "disks", disks)
559
    _SetItemIf(body, nodes is not None, "nodes", nodes)
560

    
561
    return self._SendRequest(HTTP_POST,
562
                             ("/%s/instances/%s/recreate-disks" %
563
                              (GANETI_RAPI_VERSION, instance)), None, body)
564

    
565
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
566
    """Grows a disk of an instance.
567

568
    More details for parameters can be found in the RAPI documentation.
569

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

581
    """
582
    body = {
583
      "amount": amount,
584
      }
585

    
586
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
587

    
588
    return self._SendRequest(HTTP_POST,
589
                             ("/%s/instances/%s/disk/%s/grow" %
590
                              (GANETI_RAPI_VERSION, instance, disk)),
591
                             None, body)
592

    
593
  def GetInstanceTags(self, instance):
594
    """Gets tags for an instance.
595

596
    @type instance: str
597
    @param instance: instance whose tags to return
598

599
    @rtype: list of str
600
    @return: tags for the instance
601

602
    """
603
    return self._SendRequest(HTTP_GET,
604
                             ("/%s/instances/%s/tags" %
605
                              (GANETI_RAPI_VERSION, instance)), None, None)
606

    
607
  def AddInstanceTags(self, instance, tags, dry_run=False):
608
    """Adds tags to an instance.
609

610
    @type instance: str
611
    @param instance: instance to add tags to
612
    @type tags: list of str
613
    @param tags: tags to add to the instance
614
    @type dry_run: bool
615
    @param dry_run: whether to perform a dry run
616

617
    @rtype: string
618
    @return: job id
619

620
    """
621
    query = [("tag", t) for t in tags]
622
    _AppendDryRunIf(query, dry_run)
623

    
624
    return self._SendRequest(HTTP_PUT,
625
                             ("/%s/instances/%s/tags" %
626
                              (GANETI_RAPI_VERSION, instance)), query, None)
627

    
628
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
629
    """Deletes tags from an instance.
630

631
    @type instance: str
632
    @param instance: instance to delete tags from
633
    @type tags: list of str
634
    @param tags: tags to delete
635
    @type dry_run: bool
636
    @param dry_run: whether to perform a dry run
637
    @rtype: string
638
    @return: job id
639

640
    """
641
    query = [("tag", t) for t in tags]
642
    _AppendDryRunIf(query, dry_run)
643

    
644
    return self._SendRequest(HTTP_DELETE,
645
                             ("/%s/instances/%s/tags" %
646
                              (GANETI_RAPI_VERSION, instance)), query, None)
647

    
648
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
649
                     dry_run=False, shutdown_timeout=None):
650
    """Reboots an instance.
651

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

664
    """
665
    query = []
666
    _AppendDryRunIf(query, dry_run)
667
    _AppendIf(query, reboot_type, ("type", reboot_type))
668
    _AppendIf(query, ignore_secondaries is not None,
669
              ("ignore_secondaries", ignore_secondaries))
670

    
671
    body = None
672
    if shutdown_timeout is not None:
673
        body = {"shutdown_timeout": shutdown_timeout}
674

    
675
    return self._SendRequest(HTTP_POST,
676
                             ("/%s/instances/%s/reboot" %
677
                              (GANETI_RAPI_VERSION, instance)), query, body)
678

    
679
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
680
                       timeout=None):
681
    """Shuts down an instance.
682

683
    @type instance: str
684
    @param instance: the instance to shut down
685
    @type dry_run: bool
686
    @param dry_run: whether to perform a dry run
687
    @type no_remember: bool
688
    @param no_remember: if true, will not record the state change
689
    @rtype: string
690
    @return: job id
691

692
    """
693
    query = []
694
    _AppendDryRunIf(query, dry_run)
695
    _AppendIf(query, no_remember, ("no-remember", 1))
696
    body = None
697
    if timeout is not None:
698
        body = {"timeout": timeout}
699

    
700

    
701
    return self._SendRequest(HTTP_PUT,
702
                             ("/%s/instances/%s/shutdown" %
703
                              (GANETI_RAPI_VERSION, instance)), query, body)
704

    
705
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
706
    """Starts up an instance.
707

708
    @type instance: str
709
    @param instance: the instance to start up
710
    @type dry_run: bool
711
    @param dry_run: whether to perform a dry run
712
    @type no_remember: bool
713
    @param no_remember: if true, will not record the state change
714
    @rtype: string
715
    @return: job id
716

717
    """
718
    query = []
719
    _AppendDryRunIf(query, dry_run)
720
    _AppendIf(query, no_remember, ("no-remember", 1))
721

    
722
    return self._SendRequest(HTTP_PUT,
723
                             ("/%s/instances/%s/startup" %
724
                              (GANETI_RAPI_VERSION, instance)), query, None)
725

    
726
  def ReinstallInstance(self, instance, os=None, no_startup=False,
727
                        osparams=None):
728
    """Reinstalls an instance.
729

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

740
    """
741
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
742
      body = {
743
        "start": not no_startup,
744
        }
745
      _SetItemIf(body, os is not None, "os", os)
746
      _SetItemIf(body, osparams is not None, "osparams", osparams)
747
      return self._SendRequest(HTTP_POST,
748
                               ("/%s/instances/%s/reinstall" %
749
                                (GANETI_RAPI_VERSION, instance)), None, body)
750

    
751
    # Use old request format
752
    if osparams:
753
      raise GanetiApiError("Server does not support specifying OS parameters"
754
                           " for instance reinstallation")
755

    
756
    query = []
757
    _AppendIf(query, os, ("os", os))
758
    _AppendIf(query, no_startup, ("nostartup", 1))
759

    
760
    return self._SendRequest(HTTP_POST,
761
                             ("/%s/instances/%s/reinstall" %
762
                              (GANETI_RAPI_VERSION, instance)), query, None)
763

    
764
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
765
                           remote_node=None, iallocator=None):
766
    """Replaces disks on an instance.
767

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

781
    @rtype: string
782
    @return: job id
783

784
    """
785
    query = [
786
      ("mode", mode),
787
      ]
788

    
789
    # TODO: Convert to body parameters
790

    
791
    if disks is not None:
792
      _AppendIf(query, True,
793
                ("disks", ",".join(str(idx) for idx in disks)))
794

    
795
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
796
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
797

    
798
    return self._SendRequest(HTTP_POST,
799
                             ("/%s/instances/%s/replace-disks" %
800
                              (GANETI_RAPI_VERSION, instance)), query, None)
801

    
802
  def PrepareExport(self, instance, mode):
803
    """Prepares an instance for an export.
804

805
    @type instance: string
806
    @param instance: Instance name
807
    @type mode: string
808
    @param mode: Export mode
809
    @rtype: string
810
    @return: Job ID
811

812
    """
813
    query = [("mode", mode)]
814
    return self._SendRequest(HTTP_PUT,
815
                             ("/%s/instances/%s/prepare-export" %
816
                              (GANETI_RAPI_VERSION, instance)), query, None)
817

    
818
  def ExportInstance(self, instance, mode, destination, shutdown=None,
819
                     remove_instance=None,
820
                     x509_key_name=None, destination_x509_ca=None):
821
    """Exports an instance.
822

823
    @type instance: string
824
    @param instance: Instance name
825
    @type mode: string
826
    @param mode: Export mode
827
    @rtype: string
828
    @return: Job ID
829

830
    """
831
    body = {
832
      "destination": destination,
833
      "mode": mode,
834
      }
835

    
836
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
837
    _SetItemIf(body, remove_instance is not None,
838
               "remove_instance", remove_instance)
839
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
840
    _SetItemIf(body, destination_x509_ca is not None,
841
               "destination_x509_ca", destination_x509_ca)
842

    
843
    return self._SendRequest(HTTP_PUT,
844
                             ("/%s/instances/%s/export" %
845
                              (GANETI_RAPI_VERSION, instance)), None, body)
846

    
847
  def MigrateInstance(self, instance, mode=None, cleanup=None):
848
    """Migrates an instance.
849

850
    @type instance: string
851
    @param instance: Instance name
852
    @type mode: string
853
    @param mode: Migration mode
854
    @type cleanup: bool
855
    @param cleanup: Whether to clean up a previously failed migration
856
    @rtype: string
857
    @return: job id
858

859
    """
860
    body = {}
861
    _SetItemIf(body, mode is not None, "mode", mode)
862
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
863

    
864
    return self._SendRequest(HTTP_PUT,
865
                             ("/%s/instances/%s/migrate" %
866
                              (GANETI_RAPI_VERSION, instance)), None, body)
867

    
868
  def FailoverInstance(self, instance, iallocator=None,
869
                       ignore_consistency=None, target_node=None):
870
    """Does a failover of an instance.
871

872
    @type instance: string
873
    @param instance: Instance name
874
    @type iallocator: string
875
    @param iallocator: Iallocator for deciding the target node for
876
      shared-storage instances
877
    @type ignore_consistency: bool
878
    @param ignore_consistency: Whether to ignore disk consistency
879
    @type target_node: string
880
    @param target_node: Target node for shared-storage instances
881
    @rtype: string
882
    @return: job id
883

884
    """
885
    body = {}
886
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
887
    _SetItemIf(body, ignore_consistency is not None,
888
               "ignore_consistency", ignore_consistency)
889
    _SetItemIf(body, target_node is not None, "target_node", target_node)
890

    
891
    return self._SendRequest(HTTP_PUT,
892
                             ("/%s/instances/%s/failover" %
893
                              (GANETI_RAPI_VERSION, instance)), None, body)
894

    
895
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
896
    """Changes the name of an instance.
897

898
    @type instance: string
899
    @param instance: Instance name
900
    @type new_name: string
901
    @param new_name: New instance name
902
    @type ip_check: bool
903
    @param ip_check: Whether to ensure instance's IP address is inactive
904
    @type name_check: bool
905
    @param name_check: Whether to ensure instance's name is resolvable
906
    @rtype: string
907
    @return: job id
908

909
    """
910
    body = {
911
      "new_name": new_name,
912
      }
913

    
914
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
915
    _SetItemIf(body, name_check is not None, "name_check", name_check)
916

    
917
    return self._SendRequest(HTTP_PUT,
918
                             ("/%s/instances/%s/rename" %
919
                              (GANETI_RAPI_VERSION, instance)), None, body)
920

    
921
  def GetInstanceConsole(self, instance):
922
    """Request information for connecting to instance's console.
923

924
    @type instance: string
925
    @param instance: Instance name
926
    @rtype: dict
927
    @return: dictionary containing information about instance's console
928

929
    """
930
    return self._SendRequest(HTTP_GET,
931
                             ("/%s/instances/%s/console" %
932
                              (GANETI_RAPI_VERSION, instance)), None, None)
933

    
934
  def GetJobs(self, bulk=False):
935
    """Gets all jobs for the cluster.
936

937
    @rtype: list of int
938
    @return: job ids for the cluster
939

940
    """
941
    query = []
942
    _AppendIf(query, bulk, ("bulk", 1))
943

    
944
    jobs = self._SendRequest(HTTP_GET, "/%s/jobs" % GANETI_RAPI_VERSION,
945
                             query, None)
946
    if bulk:
947
        return jobs
948
    else:
949
        return [int(j["id"]) for j in jobs]
950

    
951

    
952
  def GetJobStatus(self, job_id):
953
    """Gets the status of a job.
954

955
    @type job_id: string
956
    @param job_id: job id whose status to query
957

958
    @rtype: dict
959
    @return: job status
960

961
    """
962
    return self._SendRequest(HTTP_GET,
963
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
964
                             None, None)
965

    
966
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
967
    """Polls cluster for job status until completion.
968

969
    Completion is defined as any of the following states listed in
970
    L{JOB_STATUS_FINALIZED}.
971

972
    @type job_id: string
973
    @param job_id: job id to watch
974
    @type period: int
975
    @param period: how often to poll for status (optional, default 5s)
976
    @type retries: int
977
    @param retries: how many time to poll before giving up
978
                    (optional, default -1 means unlimited)
979

980
    @rtype: bool
981
    @return: C{True} if job succeeded or C{False} if failed/status timeout
982
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
983
      possible; L{WaitForJobChange} returns immediately after a job changed and
984
      does not use polling
985

986
    """
987
    while retries != 0:
988
      job_result = self.GetJobStatus(job_id)
989

    
990
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
991
        return True
992
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
993
        return False
994

    
995
      if period:
996
        time.sleep(period)
997

    
998
      if retries > 0:
999
        retries -= 1
1000

    
1001
    return False
1002

    
1003
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1004
    """Waits for job changes.
1005

1006
    @type job_id: string
1007
    @param job_id: Job ID for which to wait
1008
    @return: C{None} if no changes have been detected and a dict with two keys,
1009
      C{job_info} and C{log_entries} otherwise.
1010
    @rtype: dict
1011

1012
    """
1013
    body = {
1014
      "fields": fields,
1015
      "previous_job_info": prev_job_info,
1016
      "previous_log_serial": prev_log_serial,
1017
      }
1018

    
1019
    return self._SendRequest(HTTP_GET,
1020
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1021
                             None, body)
1022

    
1023
  def CancelJob(self, job_id, dry_run=False):
1024
    """Cancels a job.
1025

1026
    @type job_id: string
1027
    @param job_id: id of the job to delete
1028
    @type dry_run: bool
1029
    @param dry_run: whether to perform a dry run
1030
    @rtype: tuple
1031
    @return: tuple containing the result, and a message (bool, string)
1032

1033
    """
1034
    query = []
1035
    _AppendDryRunIf(query, dry_run)
1036

    
1037
    return self._SendRequest(HTTP_DELETE,
1038
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1039
                             query, None)
1040

    
1041
  def GetNodes(self, bulk=False):
1042
    """Gets all nodes in the cluster.
1043

1044
    @type bulk: bool
1045
    @param bulk: whether to return all information about all instances
1046

1047
    @rtype: list of dict or str
1048
    @return: if bulk is true, info about nodes in the cluster,
1049
        else list of nodes in the cluster
1050

1051
    """
1052
    query = []
1053
    _AppendIf(query, bulk, ("bulk", 1))
1054

    
1055
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1056
                              query, None)
1057
    if bulk:
1058
      return nodes
1059
    else:
1060
      return [n["id"] for n in nodes]
1061

    
1062
  def GetNode(self, node):
1063
    """Gets information about a node.
1064

1065
    @type node: str
1066
    @param node: node whose info to return
1067

1068
    @rtype: dict
1069
    @return: info about the node
1070

1071
    """
1072
    return self._SendRequest(HTTP_GET,
1073
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1074
                             None, None)
1075

    
1076
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1077
                   dry_run=False, early_release=None,
1078
                   mode=None, accept_old=False):
1079
    """Evacuates instances from a Ganeti node.
1080

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

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

1103
    @raises GanetiApiError: if an iallocator and remote_node are both
1104
        specified
1105

1106
    """
1107
    if iallocator and remote_node:
1108
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1109

    
1110
    query = []
1111
    _AppendDryRunIf(query, dry_run)
1112

    
1113
    if _NODE_EVAC_RES1 in self.GetFeatures():
1114
      # Server supports body parameters
1115
      body = {}
1116

    
1117
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1118
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1119
      _SetItemIf(body, early_release is not None,
1120
                 "early_release", early_release)
1121
      _SetItemIf(body, mode is not None, "mode", mode)
1122
    else:
1123
      # Pre-2.5 request format
1124
      body = None
1125

    
1126
      if not accept_old:
1127
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1128
                             " not accept old-style results (parameter"
1129
                             " accept_old)")
1130

    
1131
      # Pre-2.5 servers can only evacuate secondaries
1132
      if mode is not None and mode != NODE_EVAC_SEC:
1133
        raise GanetiApiError("Server can only evacuate secondary instances")
1134

    
1135
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1136
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1137
      _AppendIf(query, early_release, ("early_release", 1))
1138

    
1139
    return self._SendRequest(HTTP_POST,
1140
                             ("/%s/nodes/%s/evacuate" %
1141
                              (GANETI_RAPI_VERSION, node)), query, body)
1142

    
1143
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1144
                  target_node=None):
1145
    """Migrates all primary instances from a node.
1146

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

1159
    @rtype: string
1160
    @return: job id
1161

1162
    """
1163
    query = []
1164
    _AppendDryRunIf(query, dry_run)
1165

    
1166
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1167
      body = {}
1168

    
1169
      _SetItemIf(body, mode is not None, "mode", mode)
1170
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1171
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1172

    
1173
      assert len(query) <= 1
1174

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

    
1184
      _AppendIf(query, mode is not None, ("mode", mode))
1185

    
1186
      return self._SendRequest(HTTP_POST,
1187
                               ("/%s/nodes/%s/migrate" %
1188
                                (GANETI_RAPI_VERSION, node)), query, None)
1189

    
1190
  def GetNodeRole(self, node):
1191
    """Gets the current role for a node.
1192

1193
    @type node: str
1194
    @param node: node whose role to return
1195

1196
    @rtype: str
1197
    @return: the current role for a node
1198

1199
    """
1200
    return self._SendRequest(HTTP_GET,
1201
                             ("/%s/nodes/%s/role" %
1202
                              (GANETI_RAPI_VERSION, node)), None, None)
1203

    
1204
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1205
    """Sets the role for a node.
1206

1207
    @type node: str
1208
    @param node: the node whose role to set
1209
    @type role: str
1210
    @param role: the role to set for the node
1211
    @type force: bool
1212
    @param force: whether to force the role change
1213
    @type auto_promote: bool
1214
    @param auto_promote: Whether node(s) should be promoted to master candidate
1215
                         if necessary
1216

1217
    @rtype: string
1218
    @return: job id
1219

1220
    """
1221
    query = []
1222
    _AppendForceIf(query, force)
1223
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1224

    
1225
    return self._SendRequest(HTTP_PUT,
1226
                             ("/%s/nodes/%s/role" %
1227
                              (GANETI_RAPI_VERSION, node)), query, role)
1228

    
1229
  def PowercycleNode(self, node, force=False):
1230
    """Powercycles a node.
1231

1232
    @type node: string
1233
    @param node: Node name
1234
    @type force: bool
1235
    @param force: Whether to force the operation
1236
    @rtype: string
1237
    @return: job id
1238

1239
    """
1240
    query = []
1241
    _AppendForceIf(query, force)
1242

    
1243
    return self._SendRequest(HTTP_POST,
1244
                             ("/%s/nodes/%s/powercycle" %
1245
                              (GANETI_RAPI_VERSION, node)), query, None)
1246

    
1247
  def ModifyNode(self, node, **kwargs):
1248
    """Modifies a node.
1249

1250
    More details for parameters can be found in the RAPI documentation.
1251

1252
    @type node: string
1253
    @param node: Node name
1254
    @rtype: string
1255
    @return: job id
1256

1257
    """
1258
    return self._SendRequest(HTTP_POST,
1259
                             ("/%s/nodes/%s/modify" %
1260
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1261

    
1262
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1263
    """Gets the storage units for a node.
1264

1265
    @type node: str
1266
    @param node: the node whose storage units to return
1267
    @type storage_type: str
1268
    @param storage_type: storage type whose units to return
1269
    @type output_fields: str
1270
    @param output_fields: storage type fields to return
1271

1272
    @rtype: string
1273
    @return: job id where results can be retrieved
1274

1275
    """
1276
    query = [
1277
      ("storage_type", storage_type),
1278
      ("output_fields", output_fields),
1279
      ]
1280

    
1281
    return self._SendRequest(HTTP_GET,
1282
                             ("/%s/nodes/%s/storage" %
1283
                              (GANETI_RAPI_VERSION, node)), query, None)
1284

    
1285
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1286
    """Modifies parameters of storage units on the node.
1287

1288
    @type node: str
1289
    @param node: node whose storage units to modify
1290
    @type storage_type: str
1291
    @param storage_type: storage type whose units to modify
1292
    @type name: str
1293
    @param name: name of the storage unit
1294
    @type allocatable: bool or None
1295
    @param allocatable: Whether to set the "allocatable" flag on the storage
1296
                        unit (None=no modification, True=set, False=unset)
1297

1298
    @rtype: string
1299
    @return: job id
1300

1301
    """
1302
    query = [
1303
      ("storage_type", storage_type),
1304
      ("name", name),
1305
      ]
1306

    
1307
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1308

    
1309
    return self._SendRequest(HTTP_PUT,
1310
                             ("/%s/nodes/%s/storage/modify" %
1311
                              (GANETI_RAPI_VERSION, node)), query, None)
1312

    
1313
  def RepairNodeStorageUnits(self, node, storage_type, name):
1314
    """Repairs a storage unit on the node.
1315

1316
    @type node: str
1317
    @param node: node whose storage units to repair
1318
    @type storage_type: str
1319
    @param storage_type: storage type to repair
1320
    @type name: str
1321
    @param name: name of the storage unit to repair
1322

1323
    @rtype: string
1324
    @return: job id
1325

1326
    """
1327
    query = [
1328
      ("storage_type", storage_type),
1329
      ("name", name),
1330
      ]
1331

    
1332
    return self._SendRequest(HTTP_PUT,
1333
                             ("/%s/nodes/%s/storage/repair" %
1334
                              (GANETI_RAPI_VERSION, node)), query, None)
1335

    
1336
  def GetNodeTags(self, node):
1337
    """Gets the tags for a node.
1338

1339
    @type node: str
1340
    @param node: node whose tags to return
1341

1342
    @rtype: list of str
1343
    @return: tags for the node
1344

1345
    """
1346
    return self._SendRequest(HTTP_GET,
1347
                             ("/%s/nodes/%s/tags" %
1348
                              (GANETI_RAPI_VERSION, node)), None, None)
1349

    
1350
  def AddNodeTags(self, node, tags, dry_run=False):
1351
    """Adds tags to a node.
1352

1353
    @type node: str
1354
    @param node: node to add tags to
1355
    @type tags: list of str
1356
    @param tags: tags to add to the node
1357
    @type dry_run: bool
1358
    @param dry_run: whether to perform a dry run
1359

1360
    @rtype: string
1361
    @return: job id
1362

1363
    """
1364
    query = [("tag", t) for t in tags]
1365
    _AppendDryRunIf(query, dry_run)
1366

    
1367
    return self._SendRequest(HTTP_PUT,
1368
                             ("/%s/nodes/%s/tags" %
1369
                              (GANETI_RAPI_VERSION, node)), query, tags)
1370

    
1371
  def DeleteNodeTags(self, node, tags, dry_run=False):
1372
    """Delete tags from a node.
1373

1374
    @type node: str
1375
    @param node: node to remove tags from
1376
    @type tags: list of str
1377
    @param tags: tags to remove from the node
1378
    @type dry_run: bool
1379
    @param dry_run: whether to perform a dry run
1380

1381
    @rtype: string
1382
    @return: job id
1383

1384
    """
1385
    query = [("tag", t) for t in tags]
1386
    _AppendDryRunIf(query, dry_run)
1387

    
1388
    return self._SendRequest(HTTP_DELETE,
1389
                             ("/%s/nodes/%s/tags" %
1390
                              (GANETI_RAPI_VERSION, node)), query, None)
1391

    
1392
  def GetNetworks(self, bulk=False):
1393
    """Gets all networks in the cluster.
1394

1395
    @type bulk: bool
1396
    @param bulk: whether to return all information about the networks
1397

1398
    @rtype: list of dict or str
1399
    @return: if bulk is true, a list of dictionaries with info about all
1400
        networks in the cluster, else a list of names of those networks
1401

1402
    """
1403
    query = []
1404
    _AppendIf(query, bulk, ("bulk", 1))
1405

    
1406
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1407
                               query, None)
1408
    if bulk:
1409
      return networks
1410
    else:
1411
      return [n["name"] for n in networks]
1412

    
1413
  def GetNetwork(self, network):
1414
    """Gets information about a network.
1415

1416
    @type group: str
1417
    @param group: name of the network whose info to return
1418

1419
    @rtype: dict
1420
    @return: info about the network
1421

1422
    """
1423
    return self._SendRequest(HTTP_GET,
1424
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1425
                             None, None)
1426

    
1427
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1428
                    gateway6=None, mac_prefix=None,
1429
                    add_reserved_ips=None, tags=[],
1430
                    conflicts_check=False, dry_run=False):
1431
    """Creates a new network.
1432

1433
    @type name: str
1434
    @param name: the name of network to create
1435
    @type dry_run: bool
1436
    @param dry_run: whether to peform a dry run
1437

1438
    @rtype: string
1439
    @return: job id
1440

1441
    """
1442
    query = []
1443
    _AppendDryRunIf(query, dry_run)
1444

    
1445
    body = {
1446
      "network_name": network_name,
1447
      "gateway": gateway,
1448
      "network": network,
1449
      "gateway6": gateway6,
1450
      "network6": network6,
1451
      "mac_prefix": mac_prefix,
1452
      "add_reserved_ips": add_reserved_ips,
1453
      "conflicts_check": conflicts_check,
1454
      "tags": tags,
1455
      }
1456

    
1457
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1458
                             query, body)
1459

    
1460
  def ConnectNetwork(self, network_name, group_name, mode, link,
1461
                     conflicts_check=False, depends=None, dry_run=False):
1462
    """Connects a Network to a NodeGroup with the given netparams
1463

1464
    """
1465
    body = {
1466
      "group_name": group_name,
1467
      "network_mode": mode,
1468
      "network_link": link,
1469
      "conflicts_check": conflicts_check,
1470
      }
1471

    
1472
    if depends:
1473
      body['depends'] = depends
1474

    
1475
    query = []
1476
    _AppendDryRunIf(query, dry_run)
1477

    
1478
    return self._SendRequest(HTTP_PUT,
1479
                             ("/%s/networks/%s/connect" %
1480
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1481

    
1482
  def DisconnectNetwork(self, network_name, group_name,
1483
                        depends=None, dry_run=False):
1484
    """Connects a Network to a NodeGroup with the given netparams
1485

1486
    """
1487
    body = {
1488
      "group_name": group_name
1489
      }
1490

    
1491
    if depends:
1492
      body['depends'] = depends
1493

    
1494
    query = []
1495
    _AppendDryRunIf(query, dry_run)
1496

    
1497
    return self._SendRequest(HTTP_PUT,
1498
                             ("/%s/networks/%s/disconnect" %
1499
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1500

    
1501

    
1502
  def ModifyNetwork(self, network, **kwargs):
1503
    """Modifies a network.
1504

1505
    More details for parameters can be found in the RAPI documentation.
1506

1507
    @type network: string
1508
    @param network: Network name
1509
    @rtype: string
1510
    @return: job id
1511

1512
    """
1513
    return self._SendRequest(HTTP_PUT,
1514
                             ("/%s/networks/%s/modify" %
1515
                              (GANETI_RAPI_VERSION, network)), None, kwargs)
1516

    
1517
  def DeleteNetwork(self, network, depends=None, dry_run=False):
1518
    """Deletes a network.
1519

1520
    @type group: str
1521
    @param group: the network to delete
1522
    @type dry_run: bool
1523
    @param dry_run: whether to peform a dry run
1524

1525
    @rtype: string
1526
    @return: job id
1527

1528
    """
1529
    body = {}
1530
    if depends:
1531
      body['depends'] = depends
1532

    
1533
    query = []
1534
    _AppendDryRunIf(query, dry_run)
1535

    
1536
    return self._SendRequest(HTTP_DELETE,
1537
                             ("/%s/networks/%s" %
1538
                              (GANETI_RAPI_VERSION, network)), query, body)
1539

    
1540
  def GetNetworkTags(self, network):
1541
    """Gets tags for a network.
1542

1543
    @type network: string
1544
    @param network: Node group whose tags to return
1545

1546
    @rtype: list of strings
1547
    @return: tags for the network
1548

1549
    """
1550
    return self._SendRequest(HTTP_GET,
1551
                             ("/%s/networks/%s/tags" %
1552
                              (GANETI_RAPI_VERSION, network)), None, None)
1553

    
1554
  def AddNetworkTags(self, network, tags, dry_run=False):
1555
    """Adds tags to a network.
1556

1557
    @type network: str
1558
    @param network: network to add tags to
1559
    @type tags: list of string
1560
    @param tags: tags to add to the network
1561
    @type dry_run: bool
1562
    @param dry_run: whether to perform a dry run
1563

1564
    @rtype: string
1565
    @return: job id
1566

1567
    """
1568
    query = [("tag", t) for t in tags]
1569
    _AppendDryRunIf(query, dry_run)
1570

    
1571
    return self._SendRequest(HTTP_PUT,
1572
                             ("/%s/networks/%s/tags" %
1573
                              (GANETI_RAPI_VERSION, network)), query, None)
1574

    
1575
  def DeleteNetworkTags(self, network, tags, dry_run=False):
1576
    """Deletes tags from a network.
1577

1578
    @type network: str
1579
    @param network: network to delete tags from
1580
    @type tags: list of string
1581
    @param tags: tags to delete
1582
    @type dry_run: bool
1583
    @param dry_run: whether to perform a dry run
1584
    @rtype: string
1585
    @return: job id
1586

1587
    """
1588
    query = [("tag", t) for t in tags]
1589
    _AppendDryRunIf(query, dry_run)
1590

    
1591
    return self._SendRequest(HTTP_DELETE,
1592
                             ("/%s/networks/%s/tags" %
1593
                              (GANETI_RAPI_VERSION, network)), query, None)
1594

    
1595

    
1596
  def GetGroups(self, bulk=False):
1597
    """Gets all node groups in the cluster.
1598

1599
    @type bulk: bool
1600
    @param bulk: whether to return all information about the groups
1601

1602
    @rtype: list of dict or str
1603
    @return: if bulk is true, a list of dictionaries with info about all node
1604
        groups in the cluster, else a list of names of those node groups
1605

1606
    """
1607
    query = []
1608
    _AppendIf(query, bulk, ("bulk", 1))
1609

    
1610
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1611
                               query, None)
1612
    if bulk:
1613
      return groups
1614
    else:
1615
      return [g["name"] for g in groups]
1616

    
1617
  def GetGroup(self, group):
1618
    """Gets information about a node group.
1619

1620
    @type group: str
1621
    @param group: name of the node group whose info to return
1622

1623
    @rtype: dict
1624
    @return: info about the node group
1625

1626
    """
1627
    return self._SendRequest(HTTP_GET,
1628
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1629
                             None, None)
1630

    
1631
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1632
    """Creates a new node group.
1633

1634
    @type name: str
1635
    @param name: the name of node group to create
1636
    @type alloc_policy: str
1637
    @param alloc_policy: the desired allocation policy for the group, if any
1638
    @type dry_run: bool
1639
    @param dry_run: whether to peform a dry run
1640

1641
    @rtype: string
1642
    @return: job id
1643

1644
    """
1645
    query = []
1646
    _AppendDryRunIf(query, dry_run)
1647

    
1648
    body = {
1649
      "name": name,
1650
      "alloc_policy": alloc_policy
1651
      }
1652

    
1653
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1654
                             query, body)
1655

    
1656
  def ModifyGroup(self, group, **kwargs):
1657
    """Modifies a node group.
1658

1659
    More details for parameters can be found in the RAPI documentation.
1660

1661
    @type group: string
1662
    @param group: Node group name
1663
    @rtype: string
1664
    @return: job id
1665

1666
    """
1667
    return self._SendRequest(HTTP_PUT,
1668
                             ("/%s/groups/%s/modify" %
1669
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
1670

    
1671
  def DeleteGroup(self, group, dry_run=False):
1672
    """Deletes a node group.
1673

1674
    @type group: str
1675
    @param group: the node group to delete
1676
    @type dry_run: bool
1677
    @param dry_run: whether to peform a dry run
1678

1679
    @rtype: string
1680
    @return: job id
1681

1682
    """
1683
    query = []
1684
    _AppendDryRunIf(query, dry_run)
1685

    
1686
    return self._SendRequest(HTTP_DELETE,
1687
                             ("/%s/groups/%s" %
1688
                              (GANETI_RAPI_VERSION, group)), query, None)
1689

    
1690
  def RenameGroup(self, group, new_name):
1691
    """Changes the name of a node group.
1692

1693
    @type group: string
1694
    @param group: Node group name
1695
    @type new_name: string
1696
    @param new_name: New node group name
1697

1698
    @rtype: string
1699
    @return: job id
1700

1701
    """
1702
    body = {
1703
      "new_name": new_name,
1704
      }
1705

    
1706
    return self._SendRequest(HTTP_PUT,
1707
                             ("/%s/groups/%s/rename" %
1708
                              (GANETI_RAPI_VERSION, group)), None, body)
1709

    
1710
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1711
    """Assigns nodes to a group.
1712

1713
    @type group: string
1714
    @param group: Node gropu name
1715
    @type nodes: list of strings
1716
    @param nodes: List of nodes to assign to the group
1717

1718
    @rtype: string
1719
    @return: job id
1720

1721
    """
1722
    query = []
1723
    _AppendForceIf(query, force)
1724
    _AppendDryRunIf(query, dry_run)
1725

    
1726
    body = {
1727
      "nodes": nodes,
1728
      }
1729

    
1730
    return self._SendRequest(HTTP_PUT,
1731
                             ("/%s/groups/%s/assign-nodes" %
1732
                             (GANETI_RAPI_VERSION, group)), query, body)
1733

    
1734
  def GetGroupTags(self, group):
1735
    """Gets tags for a node group.
1736

1737
    @type group: string
1738
    @param group: Node group whose tags to return
1739

1740
    @rtype: list of strings
1741
    @return: tags for the group
1742

1743
    """
1744
    return self._SendRequest(HTTP_GET,
1745
                             ("/%s/groups/%s/tags" %
1746
                              (GANETI_RAPI_VERSION, group)), None, None)
1747

    
1748
  def AddGroupTags(self, group, tags, dry_run=False):
1749
    """Adds tags to a node group.
1750

1751
    @type group: str
1752
    @param group: group to add tags to
1753
    @type tags: list of string
1754
    @param tags: tags to add to the group
1755
    @type dry_run: bool
1756
    @param dry_run: whether to perform a dry run
1757

1758
    @rtype: string
1759
    @return: job id
1760

1761
    """
1762
    query = [("tag", t) for t in tags]
1763
    _AppendDryRunIf(query, dry_run)
1764

    
1765
    return self._SendRequest(HTTP_PUT,
1766
                             ("/%s/groups/%s/tags" %
1767
                              (GANETI_RAPI_VERSION, group)), query, None)
1768

    
1769
  def DeleteGroupTags(self, group, tags, dry_run=False):
1770
    """Deletes tags from a node group.
1771

1772
    @type group: str
1773
    @param group: group to delete tags from
1774
    @type tags: list of string
1775
    @param tags: tags to delete
1776
    @type dry_run: bool
1777
    @param dry_run: whether to perform a dry run
1778
    @rtype: string
1779
    @return: job id
1780

1781
    """
1782
    query = [("tag", t) for t in tags]
1783
    _AppendDryRunIf(query, dry_run)
1784

    
1785
    return self._SendRequest(HTTP_DELETE,
1786
                             ("/%s/groups/%s/tags" %
1787
                              (GANETI_RAPI_VERSION, group)), query, None)
1788

    
1789
  def Query(self, what, fields, qfilter=None):
1790
    """Retrieves information about resources.
1791

1792
    @type what: string
1793
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1794
    @type fields: list of string
1795
    @param fields: Requested fields
1796
    @type qfilter: None or list
1797
    @param qfilter: Query filter
1798

1799
    @rtype: string
1800
    @return: job id
1801

1802
    """
1803
    body = {
1804
      "fields": fields,
1805
      }
1806

    
1807
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
1808
    # TODO: remove "filter" after 2.7
1809
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
1810

    
1811
    return self._SendRequest(HTTP_PUT,
1812
                             ("/%s/query/%s" %
1813
                              (GANETI_RAPI_VERSION, what)), None, body)
1814

    
1815
  def QueryFields(self, what, fields=None):
1816
    """Retrieves available fields for a resource.
1817

1818
    @type what: string
1819
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1820
    @type fields: list of string
1821
    @param fields: Requested fields
1822

1823
    @rtype: string
1824
    @return: job id
1825

1826
    """
1827
    query = []
1828

    
1829
    if fields is not None:
1830
      _AppendIf(query, True, ("fields", ",".join(fields)))
1831

    
1832
    return self._SendRequest(HTTP_GET,
1833
                             ("/%s/query/%s/fields" %
1834
                              (GANETI_RAPI_VERSION, what)), query, None)