Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ dde771ba

History | View | Annotate | Download (39 kB)

1
#
2
#
3

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

    
21

    
22
"""Remote API resource implementations.
23

24
PUT or POST?
25
============
26

27
According to RFC2616 the main difference between PUT and POST is that
28
POST can create new resources but PUT can only create the resource the
29
URI was pointing to on the PUT request.
30

31
In the context of this module POST on ``/2/instances`` to change an existing
32
entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
33
new instance) with a name specified in the request.
34

35
Quoting from RFC2616, section 9.6::
36

37
  The fundamental difference between the POST and PUT requests is reflected in
38
  the different meaning of the Request-URI. The URI in a POST request
39
  identifies the resource that will handle the enclosed entity. That resource
40
  might be a data-accepting process, a gateway to some other protocol, or a
41
  separate entity that accepts annotations. In contrast, the URI in a PUT
42
  request identifies the entity enclosed with the request -- the user agent
43
  knows what URI is intended and the server MUST NOT attempt to apply the
44
  request to some other resource. If the server desires that the request be
45
  applied to a different URI, it MUST send a 301 (Moved Permanently) response;
46
  the user agent MAY then make its own decision regarding whether or not to
47
  redirect the request.
48

49
So when adding new methods, if they are operating on the URI entity itself,
50
PUT should be prefered over POST.
51

52
"""
53

    
54
# pylint: disable=C0103
55

    
56
# C0103: Invalid name, since the R_* names are not conforming
57

    
58
from ganeti import opcodes
59
from ganeti import objects
60
from ganeti import http
61
from ganeti import constants
62
from ganeti import cli
63
from ganeti import rapi
64
from ganeti import ht
65
from ganeti import compat
66
from ganeti import ssconf
67
from ganeti.rapi import baserlib
68

    
69

    
70
_COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
71
I_FIELDS = ["name", "admin_state", "os",
72
            "pnode", "snodes",
73
            "disk_template",
74
            "nic.ips", "nic.macs", "nic.modes", "nic.uuids", "nic.names",
75
            "nic.links", "nic.networks", "nic.networks.names", "nic.bridges",
76
            "network_port",
77
            "disk.sizes", "disk.spindles", "disk_usage", "disk.uuids",
78
            "disk.names",
79
            "beparams", "hvparams",
80
            "oper_state", "oper_ram", "oper_vcpus", "status",
81
            "custom_hvparams", "custom_beparams", "custom_nicparams",
82
            ] + _COMMON_FIELDS
83

    
84
N_FIELDS = ["name", "offline", "master_candidate", "drained",
85
            "dtotal", "dfree", "sptotal", "spfree",
86
            "mtotal", "mnode", "mfree",
87
            "pinst_cnt", "sinst_cnt",
88
            "ctotal", "cnos", "cnodes", "csockets",
89
            "pip", "sip", "role",
90
            "pinst_list", "sinst_list",
91
            "master_capable", "vm_capable",
92
            "ndparams",
93
            "group.uuid",
94
            ] + _COMMON_FIELDS
95

    
96
NET_FIELDS = ["name", "network", "gateway",
97
              "network6", "gateway6",
98
              "mac_prefix",
99
              "free_count", "reserved_count",
100
              "map", "group_list", "inst_list",
101
              "external_reservations",
102
              ] + _COMMON_FIELDS
103

    
104
G_FIELDS = [
105
  "alloc_policy",
106
  "name",
107
  "node_cnt",
108
  "node_list",
109
  "ipolicy",
110
  "custom_ipolicy",
111
  "diskparams",
112
  "custom_diskparams",
113
  "ndparams",
114
  "custom_ndparams",
115
  ] + _COMMON_FIELDS
116

    
117
J_FIELDS_BULK = [
118
  "id", "ops", "status", "summary",
119
  "opstatus",
120
  "received_ts", "start_ts", "end_ts",
121
  ]
122

    
123
J_FIELDS = J_FIELDS_BULK + [
124
  "oplog",
125
  "opresult",
126
  ]
127

    
128
_NR_DRAINED = "drained"
129
_NR_MASTER_CANDIDATE = "master-candidate"
130
_NR_MASTER = "master"
131
_NR_OFFLINE = "offline"
132
_NR_REGULAR = "regular"
133

    
134
_NR_MAP = {
135
  constants.NR_MASTER: _NR_MASTER,
136
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
137
  constants.NR_DRAINED: _NR_DRAINED,
138
  constants.NR_OFFLINE: _NR_OFFLINE,
139
  constants.NR_REGULAR: _NR_REGULAR,
140
  }
141

    
142
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
143

    
144
# Request data version field
145
_REQ_DATA_VERSION = "__version__"
146

    
147
# Feature string for instance creation request data version 1
148
_INST_CREATE_REQV1 = "instance-create-reqv1"
149

    
150
# Feature string for instance reinstall request version 1
151
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
152

    
153
# Feature string for node migration version 1
154
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
155

    
156
# Feature string for node evacuation with LU-generated jobs
157
_NODE_EVAC_RES1 = "node-evac-res1"
158

    
159
ALL_FEATURES = compat.UniqueFrozenset([
160
  _INST_CREATE_REQV1,
161
  _INST_REINSTALL_REQV1,
162
  _NODE_MIGRATE_REQV1,
163
  _NODE_EVAC_RES1,
164
  ])
165

    
166
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
167
_WFJC_TIMEOUT = 10
168

    
169

    
170
# FIXME: For compatibility we update the beparams/memory field. Needs to be
171
#        removed in Ganeti 2.8
172
def _UpdateBeparams(inst):
173
  """Updates the beparams dict of inst to support the memory field.
174

175
  @param inst: Inst dict
176
  @return: Updated inst dict
177

178
  """
179
  beparams = inst["beparams"]
180
  beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
181

    
182
  return inst
183

    
184

    
185
class R_root(baserlib.ResourceBase):
186
  """/ resource.
187

188
  """
189
  @staticmethod
190
  def GET():
191
    """Supported for legacy reasons.
192

193
    """
194
    return None
195

    
196

    
197
class R_2(R_root):
198
  """/2 resource.
199

200
  """
201

    
202

    
203
class R_version(baserlib.ResourceBase):
204
  """/version resource.
205

206
  This resource should be used to determine the remote API version and
207
  to adapt clients accordingly.
208

209
  """
210
  @staticmethod
211
  def GET():
212
    """Returns the remote API version.
213

214
    """
215
    return constants.RAPI_VERSION
216

    
217

    
218
class R_2_info(baserlib.OpcodeResource):
219
  """/2/info resource.
220

221
  """
222
  GET_OPCODE = opcodes.OpClusterQuery
223

    
224
  def GET(self):
225
    """Returns cluster information.
226

227
    """
228
    client = self.GetClient(query=True)
229
    return client.QueryClusterInfo()
230

    
231

    
232
class R_2_features(baserlib.ResourceBase):
233
  """/2/features resource.
234

235
  """
236
  @staticmethod
237
  def GET():
238
    """Returns list of optional RAPI features implemented.
239

240
    """
241
    return list(ALL_FEATURES)
242

    
243

    
244
class R_2_os(baserlib.OpcodeResource):
245
  """/2/os resource.
246

247
  """
248
  GET_OPCODE = opcodes.OpOsDiagnose
249

    
250
  def GET(self):
251
    """Return a list of all OSes.
252

253
    Can return error 500 in case of a problem.
254

255
    Example: ["debian-etch"]
256

257
    """
258
    cl = self.GetClient()
259
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
260
    job_id = self.SubmitJob([op], cl=cl)
261
    # we use custom feedback function, instead of print we log the status
262
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
263
    diagnose_data = result[0]
264

    
265
    if not isinstance(diagnose_data, list):
266
      raise http.HttpBadGateway(message="Can't get OS list")
267

    
268
    os_names = []
269
    for (name, variants) in diagnose_data:
270
      os_names.extend(cli.CalculateOSNames(name, variants))
271

    
272
    return os_names
273

    
274

    
275
class R_2_redist_config(baserlib.OpcodeResource):
276
  """/2/redistribute-config resource.
277

278
  """
279
  PUT_OPCODE = opcodes.OpClusterRedistConf
280

    
281

    
282
class R_2_cluster_modify(baserlib.OpcodeResource):
283
  """/2/modify resource.
284

285
  """
286
  PUT_OPCODE = opcodes.OpClusterSetParams
287

    
288

    
289
class R_2_jobs(baserlib.ResourceBase):
290
  """/2/jobs resource.
291

292
  """
293
  def GET(self):
294
    """Returns a dictionary of jobs.
295

296
    @return: a dictionary with jobs id and uri.
297

298
    """
299
    client = self.GetClient(query=True)
300

    
301
    if self.useBulk():
302
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
303
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
304
    else:
305
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
306
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
307
                                   uri_fields=("id", "uri"))
308

    
309

    
310
class R_2_jobs_id(baserlib.ResourceBase):
311
  """/2/jobs/[job_id] resource.
312

313
  """
314
  def GET(self):
315
    """Returns a job status.
316

317
    @return: a dictionary with job parameters.
318
        The result includes:
319
            - id: job ID as a number
320
            - status: current job status as a string
321
            - ops: involved OpCodes as a list of dictionaries for each
322
              opcodes in the job
323
            - opstatus: OpCodes status as a list
324
            - opresult: OpCodes results as a list of lists
325

326
    """
327
    job_id = self.items[0]
328
    result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
329
    if result is None:
330
      raise http.HttpNotFound()
331
    return baserlib.MapFields(J_FIELDS, result)
332

    
333
  def DELETE(self):
334
    """Cancel not-yet-started job.
335

336
    """
337
    job_id = self.items[0]
338
    result = self.GetClient().CancelJob(job_id)
339
    return result
340

    
341

    
342
class R_2_jobs_id_wait(baserlib.ResourceBase):
343
  """/2/jobs/[job_id]/wait resource.
344

345
  """
346
  # WaitForJobChange provides access to sensitive information and blocks
347
  # machine resources (it's a blocking RAPI call), hence restricting access.
348
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
349

    
350
  def GET(self):
351
    """Waits for job changes.
352

353
    """
354
    job_id = self.items[0]
355

    
356
    fields = self.getBodyParameter("fields")
357
    prev_job_info = self.getBodyParameter("previous_job_info", None)
358
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
359

    
360
    if not isinstance(fields, list):
361
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
362

    
363
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
364
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
365
                                " be a list")
366

    
367
    if not (prev_log_serial is None or
368
            isinstance(prev_log_serial, (int, long))):
369
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
370
                                " be a number")
371

    
372
    client = self.GetClient()
373
    result = client.WaitForJobChangeOnce(job_id, fields,
374
                                         prev_job_info, prev_log_serial,
375
                                         timeout=_WFJC_TIMEOUT)
376
    if not result:
377
      raise http.HttpNotFound()
378

    
379
    if result == constants.JOB_NOTCHANGED:
380
      # No changes
381
      return None
382

    
383
    (job_info, log_entries) = result
384

    
385
    return {
386
      "job_info": job_info,
387
      "log_entries": log_entries,
388
      }
389

    
390

    
391
class R_2_nodes(baserlib.OpcodeResource):
392
  """/2/nodes resource.
393

394
  """
395

    
396
  def GET(self):
397
    """Returns a list of all nodes.
398

399
    """
400
    client = self.GetClient(query=True)
401

    
402
    if self.useBulk():
403
      bulkdata = client.QueryNodes([], N_FIELDS, False)
404
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
405
    else:
406
      nodesdata = client.QueryNodes([], ["name"], False)
407
      nodeslist = [row[0] for row in nodesdata]
408
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
409
                                   uri_fields=("id", "uri"))
410

    
411

    
412
class R_2_nodes_name(baserlib.OpcodeResource):
413
  """/2/nodes/[node_name] resource.
414

415
  """
416

    
417
  def GET(self):
418
    """Send information about a node.
419

420
    """
421
    node_name = self.items[0]
422
    client = self.GetClient(query=True)
423

    
424
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
425
                                            names=[node_name], fields=N_FIELDS,
426
                                            use_locking=self.useLocking())
427

    
428
    return baserlib.MapFields(N_FIELDS, result[0])
429

    
430

    
431
class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
432
  """/2/nodes/[node_name]/powercycle resource.
433

434
  """
435
  POST_OPCODE = opcodes.OpNodePowercycle
436

    
437
  def GetPostOpInput(self):
438
    """Tries to powercycle a node.
439

440
    """
441
    return (self.request_body, {
442
      "node_name": self.items[0],
443
      "force": self.useForce(),
444
      })
445

    
446

    
447
class R_2_nodes_name_role(baserlib.OpcodeResource):
448
  """/2/nodes/[node_name]/role resource.
449

450
  """
451
  PUT_OPCODE = opcodes.OpNodeSetParams
452

    
453
  def GET(self):
454
    """Returns the current node role.
455

456
    @return: Node role
457

458
    """
459
    node_name = self.items[0]
460
    client = self.GetClient(query=True)
461
    result = client.QueryNodes(names=[node_name], fields=["role"],
462
                               use_locking=self.useLocking())
463

    
464
    return _NR_MAP[result[0][0]]
465

    
466
  def GetPutOpInput(self):
467
    """Sets the node role.
468

469
    """
470
    baserlib.CheckType(self.request_body, basestring, "Body contents")
471

    
472
    role = self.request_body
473

    
474
    if role == _NR_REGULAR:
475
      candidate = False
476
      offline = False
477
      drained = False
478

    
479
    elif role == _NR_MASTER_CANDIDATE:
480
      candidate = True
481
      offline = drained = None
482

    
483
    elif role == _NR_DRAINED:
484
      drained = True
485
      candidate = offline = None
486

    
487
    elif role == _NR_OFFLINE:
488
      offline = True
489
      candidate = drained = None
490

    
491
    else:
492
      raise http.HttpBadRequest("Can't set '%s' role" % role)
493

    
494
    assert len(self.items) == 1
495

    
496
    return ({}, {
497
      "node_name": self.items[0],
498
      "master_candidate": candidate,
499
      "offline": offline,
500
      "drained": drained,
501
      "force": self.useForce(),
502
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
503
      })
504

    
505

    
506
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
507
  """/2/nodes/[node_name]/evacuate resource.
508

509
  """
510
  POST_OPCODE = opcodes.OpNodeEvacuate
511

    
512
  def GetPostOpInput(self):
513
    """Evacuate all instances off a node.
514

515
    """
516
    return (self.request_body, {
517
      "node_name": self.items[0],
518
      "dry_run": self.dryRun(),
519
      })
520

    
521

    
522
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
523
  """/2/nodes/[node_name]/migrate resource.
524

525
  """
526
  POST_OPCODE = opcodes.OpNodeMigrate
527

    
528
  def GetPostOpInput(self):
529
    """Migrate all primary instances from a node.
530

531
    """
532
    if self.queryargs:
533
      # Support old-style requests
534
      if "live" in self.queryargs and "mode" in self.queryargs:
535
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
536
                                  " be passed")
537

    
538
      if "live" in self.queryargs:
539
        if self._checkIntVariable("live", default=1):
540
          mode = constants.HT_MIGRATION_LIVE
541
        else:
542
          mode = constants.HT_MIGRATION_NONLIVE
543
      else:
544
        mode = self._checkStringVariable("mode", default=None)
545

    
546
      data = {
547
        "mode": mode,
548
        }
549
    else:
550
      data = self.request_body
551

    
552
    return (data, {
553
      "node_name": self.items[0],
554
      })
555

    
556

    
557
class R_2_nodes_name_modify(baserlib.OpcodeResource):
558
  """/2/nodes/[node_name]/modify resource.
559

560
  """
561
  POST_OPCODE = opcodes.OpNodeSetParams
562

    
563
  def GetPostOpInput(self):
564
    """Changes parameters of a node.
565

566
    """
567
    assert len(self.items) == 1
568

    
569
    return (self.request_body, {
570
      "node_name": self.items[0],
571
      })
572

    
573

    
574
class R_2_nodes_name_storage(baserlib.OpcodeResource):
575
  """/2/nodes/[node_name]/storage resource.
576

577
  """
578
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
579
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
580
  GET_OPCODE = opcodes.OpNodeQueryStorage
581

    
582
  def GetGetOpInput(self):
583
    """List storage available on a node.
584

585
    """
586
    storage_type = self._checkStringVariable("storage_type", None)
587
    output_fields = self._checkStringVariable("output_fields", None)
588

    
589
    if not output_fields:
590
      raise http.HttpBadRequest("Missing the required 'output_fields'"
591
                                " parameter")
592

    
593
    return ({}, {
594
      "nodes": [self.items[0]],
595
      "storage_type": storage_type,
596
      "output_fields": output_fields.split(","),
597
      })
598

    
599

    
600
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
601
  """/2/nodes/[node_name]/storage/modify resource.
602

603
  """
604
  PUT_OPCODE = opcodes.OpNodeModifyStorage
605

    
606
  def GetPutOpInput(self):
607
    """Modifies a storage volume on a node.
608

609
    """
610
    storage_type = self._checkStringVariable("storage_type", None)
611
    name = self._checkStringVariable("name", None)
612

    
613
    if not name:
614
      raise http.HttpBadRequest("Missing the required 'name'"
615
                                " parameter")
616

    
617
    changes = {}
618

    
619
    if "allocatable" in self.queryargs:
620
      changes[constants.SF_ALLOCATABLE] = \
621
        bool(self._checkIntVariable("allocatable", default=1))
622

    
623
    return ({}, {
624
      "node_name": self.items[0],
625
      "storage_type": storage_type,
626
      "name": name,
627
      "changes": changes,
628
      })
629

    
630

    
631
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
632
  """/2/nodes/[node_name]/storage/repair resource.
633

634
  """
635
  PUT_OPCODE = opcodes.OpRepairNodeStorage
636

    
637
  def GetPutOpInput(self):
638
    """Repairs a storage volume on a node.
639

640
    """
641
    storage_type = self._checkStringVariable("storage_type", None)
642
    name = self._checkStringVariable("name", None)
643
    if not name:
644
      raise http.HttpBadRequest("Missing the required 'name'"
645
                                " parameter")
646

    
647
    return ({}, {
648
      "node_name": self.items[0],
649
      "storage_type": storage_type,
650
      "name": name,
651
      })
652

    
653

    
654
class R_2_networks(baserlib.OpcodeResource):
655
  """/2/networks resource.
656

657
  """
658
  GET_OPCODE = opcodes.OpNetworkQuery
659
  POST_OPCODE = opcodes.OpNetworkAdd
660
  POST_RENAME = {
661
    "name": "network_name",
662
    }
663

    
664
  def GetPostOpInput(self):
665
    """Create a network.
666

667
    """
668
    assert not self.items
669
    return (self.request_body, {
670
      "dry_run": self.dryRun(),
671
      })
672

    
673
  def GET(self):
674
    """Returns a list of all networks.
675

676
    """
677
    client = self.GetClient(query=True)
678

    
679
    if self.useBulk():
680
      bulkdata = client.QueryNetworks([], NET_FIELDS, False)
681
      return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
682
    else:
683
      data = client.QueryNetworks([], ["name"], False)
684
      networknames = [row[0] for row in data]
685
      return baserlib.BuildUriList(networknames, "/2/networks/%s",
686
                                   uri_fields=("name", "uri"))
687

    
688

    
689
class R_2_networks_name(baserlib.OpcodeResource):
690
  """/2/networks/[network_name] resource.
691

692
  """
693
  DELETE_OPCODE = opcodes.OpNetworkRemove
694

    
695
  def GET(self):
696
    """Send information about a network.
697

698
    """
699
    network_name = self.items[0]
700
    client = self.GetClient(query=True)
701

    
702
    result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
703
                                            names=[network_name],
704
                                            fields=NET_FIELDS,
705
                                            use_locking=self.useLocking())
706

    
707
    return baserlib.MapFields(NET_FIELDS, result[0])
708

    
709
  def GetDeleteOpInput(self):
710
    """Delete a network.
711

712
    """
713
    assert len(self.items) == 1
714
    return (self.request_body, {
715
      "network_name": self.items[0],
716
      "dry_run": self.dryRun(),
717
      })
718

    
719

    
720
class R_2_networks_name_connect(baserlib.OpcodeResource):
721
  """/2/networks/[network_name]/connect resource.
722

723
  """
724
  PUT_OPCODE = opcodes.OpNetworkConnect
725

    
726
  def GetPutOpInput(self):
727
    """Changes some parameters of node group.
728

729
    """
730
    assert self.items
731
    return (self.request_body, {
732
      "network_name": self.items[0],
733
      "dry_run": self.dryRun(),
734
      })
735

    
736

    
737
class R_2_networks_name_disconnect(baserlib.OpcodeResource):
738
  """/2/networks/[network_name]/disconnect resource.
739

740
  """
741
  PUT_OPCODE = opcodes.OpNetworkDisconnect
742

    
743
  def GetPutOpInput(self):
744
    """Changes some parameters of node group.
745

746
    """
747
    assert self.items
748
    return (self.request_body, {
749
      "network_name": self.items[0],
750
      "dry_run": self.dryRun(),
751
      })
752

    
753

    
754
class R_2_networks_name_modify(baserlib.OpcodeResource):
755
  """/2/networks/[network_name]/modify resource.
756

757
  """
758
  PUT_OPCODE = opcodes.OpNetworkSetParams
759

    
760
  def GetPutOpInput(self):
761
    """Changes some parameters of network.
762

763
    """
764
    assert self.items
765
    return (self.request_body, {
766
      "network_name": self.items[0],
767
      })
768

    
769

    
770
class R_2_groups(baserlib.OpcodeResource):
771
  """/2/groups resource.
772

773
  """
774
  GET_OPCODE = opcodes.OpGroupQuery
775
  POST_OPCODE = opcodes.OpGroupAdd
776
  POST_RENAME = {
777
    "name": "group_name",
778
    }
779

    
780
  def GetPostOpInput(self):
781
    """Create a node group.
782

783

784
    """
785
    assert not self.items
786
    return (self.request_body, {
787
      "dry_run": self.dryRun(),
788
      })
789

    
790
  def GET(self):
791
    """Returns a list of all node groups.
792

793
    """
794
    client = self.GetClient(query=True)
795

    
796
    if self.useBulk():
797
      bulkdata = client.QueryGroups([], G_FIELDS, False)
798
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
799
    else:
800
      data = client.QueryGroups([], ["name"], False)
801
      groupnames = [row[0] for row in data]
802
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
803
                                   uri_fields=("name", "uri"))
804

    
805

    
806
class R_2_groups_name(baserlib.OpcodeResource):
807
  """/2/groups/[group_name] resource.
808

809
  """
810
  DELETE_OPCODE = opcodes.OpGroupRemove
811

    
812
  def GET(self):
813
    """Send information about a node group.
814

815
    """
816
    group_name = self.items[0]
817
    client = self.GetClient(query=True)
818

    
819
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
820
                                            names=[group_name], fields=G_FIELDS,
821
                                            use_locking=self.useLocking())
822

    
823
    return baserlib.MapFields(G_FIELDS, result[0])
824

    
825
  def GetDeleteOpInput(self):
826
    """Delete a node group.
827

828
    """
829
    assert len(self.items) == 1
830
    return ({}, {
831
      "group_name": self.items[0],
832
      "dry_run": self.dryRun(),
833
      })
834

    
835

    
836
class R_2_groups_name_modify(baserlib.OpcodeResource):
837
  """/2/groups/[group_name]/modify resource.
838

839
  """
840
  PUT_OPCODE = opcodes.OpGroupSetParams
841

    
842
  def GetPutOpInput(self):
843
    """Changes some parameters of node group.
844

845
    """
846
    assert self.items
847
    return (self.request_body, {
848
      "group_name": self.items[0],
849
      })
850

    
851

    
852
class R_2_groups_name_rename(baserlib.OpcodeResource):
853
  """/2/groups/[group_name]/rename resource.
854

855
  """
856
  PUT_OPCODE = opcodes.OpGroupRename
857

    
858
  def GetPutOpInput(self):
859
    """Changes the name of a node group.
860

861
    """
862
    assert len(self.items) == 1
863
    return (self.request_body, {
864
      "group_name": self.items[0],
865
      "dry_run": self.dryRun(),
866
      })
867

    
868

    
869
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
870
  """/2/groups/[group_name]/assign-nodes resource.
871

872
  """
873
  PUT_OPCODE = opcodes.OpGroupAssignNodes
874

    
875
  def GetPutOpInput(self):
876
    """Assigns nodes to a group.
877

878
    """
879
    assert len(self.items) == 1
880
    return (self.request_body, {
881
      "group_name": self.items[0],
882
      "dry_run": self.dryRun(),
883
      "force": self.useForce(),
884
      })
885

    
886

    
887
class R_2_instances(baserlib.OpcodeResource):
888
  """/2/instances resource.
889

890
  """
891
  GET_OPCODE = opcodes.OpInstanceQuery
892
  POST_OPCODE = opcodes.OpInstanceCreate
893
  POST_RENAME = {
894
    "os": "os_type",
895
    "name": "instance_name",
896
    }
897

    
898
  def GET(self):
899
    """Returns a list of all available instances.
900

901
    """
902
    client = self.GetClient(query=True)
903

    
904
    use_locking = self.useLocking()
905
    if self.useBulk():
906
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
907
      return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
908
    else:
909
      instancesdata = client.QueryInstances([], ["name"], use_locking)
910
      instanceslist = [row[0] for row in instancesdata]
911
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
912
                                   uri_fields=("id", "uri"))
913

    
914
  def GetPostOpInput(self):
915
    """Create an instance.
916

917
    @return: a job id
918

919
    """
920
    baserlib.CheckType(self.request_body, dict, "Body contents")
921

    
922
    # Default to request data version 0
923
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
924

    
925
    if data_version == 0:
926
      raise http.HttpBadRequest("Instance creation request version 0 is no"
927
                                " longer supported")
928
    elif data_version != 1:
929
      raise http.HttpBadRequest("Unsupported request data version %s" %
930
                                data_version)
931

    
932
    data = self.request_body.copy()
933
    # Remove "__version__"
934
    data.pop(_REQ_DATA_VERSION, None)
935

    
936
    return (data, {
937
      "dry_run": self.dryRun(),
938
      })
939

    
940

    
941
class R_2_instances_multi_alloc(baserlib.OpcodeResource):
942
  """/2/instances-multi-alloc resource.
943

944
  """
945
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
946

    
947
  def GetPostOpInput(self):
948
    """Try to allocate multiple instances.
949

950
    @return: A dict with submitted jobs, allocatable instances and failed
951
             allocations
952

953
    """
954
    if "instances" not in self.request_body:
955
      raise http.HttpBadRequest("Request is missing required 'instances' field"
956
                                " in body")
957

    
958
    op_id = {
959
      "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
960
      }
961
    body = objects.FillDict(self.request_body, {
962
      "instances": [objects.FillDict(inst, op_id)
963
                    for inst in self.request_body["instances"]],
964
      })
965

    
966
    return (body, {
967
      "dry_run": self.dryRun(),
968
      })
969

    
970

    
971
class R_2_instances_name(baserlib.OpcodeResource):
972
  """/2/instances/[instance_name] resource.
973

974
  """
975
  GET_OPCODE = opcodes.OpInstanceQuery
976
  DELETE_OPCODE = opcodes.OpInstanceRemove
977

    
978
  def GET(self):
979
    """Send information about an instance.
980

981
    """
982
    client = self.GetClient(query=True)
983
    instance_name = self.items[0]
984

    
985
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
986
                                            names=[instance_name],
987
                                            fields=I_FIELDS,
988
                                            use_locking=self.useLocking())
989

    
990
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
991

    
992
  def GetDeleteOpInput(self):
993
    """Delete an instance.
994

995
    """
996
    assert len(self.items) == 1
997
    return ({}, {
998
      "instance_name": self.items[0],
999
      "ignore_failures": False,
1000
      "dry_run": self.dryRun(),
1001
      })
1002

    
1003

    
1004
class R_2_instances_name_info(baserlib.OpcodeResource):
1005
  """/2/instances/[instance_name]/info resource.
1006

1007
  """
1008
  GET_OPCODE = opcodes.OpInstanceQueryData
1009

    
1010
  def GetGetOpInput(self):
1011
    """Request detailed instance information.
1012

1013
    """
1014
    assert len(self.items) == 1
1015
    return ({}, {
1016
      "instances": [self.items[0]],
1017
      "static": bool(self._checkIntVariable("static", default=0)),
1018
      })
1019

    
1020

    
1021
class R_2_instances_name_reboot(baserlib.OpcodeResource):
1022
  """/2/instances/[instance_name]/reboot resource.
1023

1024
  Implements an instance reboot.
1025

1026
  """
1027
  POST_OPCODE = opcodes.OpInstanceReboot
1028

    
1029
  def GetPostOpInput(self):
1030
    """Reboot an instance.
1031

1032
    The URI takes type=[hard|soft|full] and
1033
    ignore_secondaries=[False|True] parameters.
1034

1035
    """
1036
    return ({}, {
1037
      "instance_name": self.items[0],
1038
      "reboot_type":
1039
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1040
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1041
      "dry_run": self.dryRun(),
1042
      })
1043

    
1044

    
1045
class R_2_instances_name_startup(baserlib.OpcodeResource):
1046
  """/2/instances/[instance_name]/startup resource.
1047

1048
  Implements an instance startup.
1049

1050
  """
1051
  PUT_OPCODE = opcodes.OpInstanceStartup
1052

    
1053
  def GetPutOpInput(self):
1054
    """Startup an instance.
1055

1056
    The URI takes force=[False|True] parameter to start the instance
1057
    if even if secondary disks are failing.
1058

1059
    """
1060
    return ({}, {
1061
      "instance_name": self.items[0],
1062
      "force": self.useForce(),
1063
      "dry_run": self.dryRun(),
1064
      "no_remember": bool(self._checkIntVariable("no_remember")),
1065
      })
1066

    
1067

    
1068
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1069
  """/2/instances/[instance_name]/shutdown resource.
1070

1071
  Implements an instance shutdown.
1072

1073
  """
1074
  PUT_OPCODE = opcodes.OpInstanceShutdown
1075

    
1076
  def GetPutOpInput(self):
1077
    """Shutdown an instance.
1078

1079
    """
1080
    return (self.request_body, {
1081
      "instance_name": self.items[0],
1082
      "no_remember": bool(self._checkIntVariable("no_remember")),
1083
      "dry_run": self.dryRun(),
1084
      })
1085

    
1086

    
1087
def _ParseInstanceReinstallRequest(name, data):
1088
  """Parses a request for reinstalling an instance.
1089

1090
  """
1091
  if not isinstance(data, dict):
1092
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1093

    
1094
  ostype = baserlib.CheckParameter(data, "os", default=None)
1095
  start = baserlib.CheckParameter(data, "start", exptype=bool,
1096
                                  default=True)
1097
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
1098

    
1099
  ops = [
1100
    opcodes.OpInstanceShutdown(instance_name=name),
1101
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1102
                                osparams=osparams),
1103
    ]
1104

    
1105
  if start:
1106
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1107

    
1108
  return ops
1109

    
1110

    
1111
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1112
  """/2/instances/[instance_name]/reinstall resource.
1113

1114
  Implements an instance reinstall.
1115

1116
  """
1117
  POST_OPCODE = opcodes.OpInstanceReinstall
1118

    
1119
  def POST(self):
1120
    """Reinstall an instance.
1121

1122
    The URI takes os=name and nostartup=[0|1] optional
1123
    parameters. By default, the instance will be started
1124
    automatically.
1125

1126
    """
1127
    if self.request_body:
1128
      if self.queryargs:
1129
        raise http.HttpBadRequest("Can't combine query and body parameters")
1130

    
1131
      body = self.request_body
1132
    elif self.queryargs:
1133
      # Legacy interface, do not modify/extend
1134
      body = {
1135
        "os": self._checkStringVariable("os"),
1136
        "start": not self._checkIntVariable("nostartup"),
1137
        }
1138
    else:
1139
      body = {}
1140

    
1141
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1142

    
1143
    return self.SubmitJob(ops)
1144

    
1145

    
1146
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1147
  """/2/instances/[instance_name]/replace-disks resource.
1148

1149
  """
1150
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1151

    
1152
  def GetPostOpInput(self):
1153
    """Replaces disks on an instance.
1154

1155
    """
1156
    static = {
1157
      "instance_name": self.items[0],
1158
      }
1159

    
1160
    if self.request_body:
1161
      data = self.request_body
1162
    elif self.queryargs:
1163
      # Legacy interface, do not modify/extend
1164
      data = {
1165
        "remote_node": self._checkStringVariable("remote_node", default=None),
1166
        "mode": self._checkStringVariable("mode", default=None),
1167
        "disks": self._checkStringVariable("disks", default=None),
1168
        "iallocator": self._checkStringVariable("iallocator", default=None),
1169
        }
1170
    else:
1171
      data = {}
1172

    
1173
    # Parse disks
1174
    try:
1175
      raw_disks = data.pop("disks")
1176
    except KeyError:
1177
      pass
1178
    else:
1179
      if raw_disks:
1180
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1181
          data["disks"] = raw_disks
1182
        else:
1183
          # Backwards compatibility for strings of the format "1, 2, 3"
1184
          try:
1185
            data["disks"] = [int(part) for part in raw_disks.split(",")]
1186
          except (TypeError, ValueError), err:
1187
            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1188

    
1189
    return (data, static)
1190

    
1191

    
1192
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1193
  """/2/instances/[instance_name]/activate-disks resource.
1194

1195
  """
1196
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1197

    
1198
  def GetPutOpInput(self):
1199
    """Activate disks for an instance.
1200

1201
    The URI might contain ignore_size to ignore current recorded size.
1202

1203
    """
1204
    return ({}, {
1205
      "instance_name": self.items[0],
1206
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
1207
      })
1208

    
1209

    
1210
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1211
  """/2/instances/[instance_name]/deactivate-disks resource.
1212

1213
  """
1214
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1215

    
1216
  def GetPutOpInput(self):
1217
    """Deactivate disks for an instance.
1218

1219
    """
1220
    return ({}, {
1221
      "instance_name": self.items[0],
1222
      })
1223

    
1224

    
1225
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1226
  """/2/instances/[instance_name]/recreate-disks resource.
1227

1228
  """
1229
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1230

    
1231
  def GetPostOpInput(self):
1232
    """Recreate disks for an instance.
1233

1234
    """
1235
    return ({}, {
1236
      "instance_name": self.items[0],
1237
      })
1238

    
1239

    
1240
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1241
  """/2/instances/[instance_name]/prepare-export resource.
1242

1243
  """
1244
  PUT_OPCODE = opcodes.OpBackupPrepare
1245

    
1246
  def GetPutOpInput(self):
1247
    """Prepares an export for an instance.
1248

1249
    """
1250
    return ({}, {
1251
      "instance_name": self.items[0],
1252
      "mode": self._checkStringVariable("mode"),
1253
      })
1254

    
1255

    
1256
class R_2_instances_name_export(baserlib.OpcodeResource):
1257
  """/2/instances/[instance_name]/export resource.
1258

1259
  """
1260
  PUT_OPCODE = opcodes.OpBackupExport
1261
  PUT_RENAME = {
1262
    "destination": "target_node",
1263
    }
1264

    
1265
  def GetPutOpInput(self):
1266
    """Exports an instance.
1267

1268
    """
1269
    return (self.request_body, {
1270
      "instance_name": self.items[0],
1271
      })
1272

    
1273

    
1274
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1275
  """/2/instances/[instance_name]/migrate resource.
1276

1277
  """
1278
  PUT_OPCODE = opcodes.OpInstanceMigrate
1279

    
1280
  def GetPutOpInput(self):
1281
    """Migrates an instance.
1282

1283
    """
1284
    return (self.request_body, {
1285
      "instance_name": self.items[0],
1286
      })
1287

    
1288

    
1289
class R_2_instances_name_failover(baserlib.OpcodeResource):
1290
  """/2/instances/[instance_name]/failover resource.
1291

1292
  """
1293
  PUT_OPCODE = opcodes.OpInstanceFailover
1294

    
1295
  def GetPutOpInput(self):
1296
    """Does a failover of an instance.
1297

1298
    """
1299
    return (self.request_body, {
1300
      "instance_name": self.items[0],
1301
      })
1302

    
1303

    
1304
class R_2_instances_name_rename(baserlib.OpcodeResource):
1305
  """/2/instances/[instance_name]/rename resource.
1306

1307
  """
1308
  PUT_OPCODE = opcodes.OpInstanceRename
1309

    
1310
  def GetPutOpInput(self):
1311
    """Changes the name of an instance.
1312

1313
    """
1314
    return (self.request_body, {
1315
      "instance_name": self.items[0],
1316
      })
1317

    
1318

    
1319
class R_2_instances_name_modify(baserlib.OpcodeResource):
1320
  """/2/instances/[instance_name]/modify resource.
1321

1322
  """
1323
  PUT_OPCODE = opcodes.OpInstanceSetParams
1324

    
1325
  def GetPutOpInput(self):
1326
    """Changes parameters of an instance.
1327

1328
    """
1329
    return (self.request_body, {
1330
      "instance_name": self.items[0],
1331
      })
1332

    
1333

    
1334
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1335
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1336

1337
  """
1338
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1339

    
1340
  def GetPostOpInput(self):
1341
    """Increases the size of an instance disk.
1342

1343
    """
1344
    return (self.request_body, {
1345
      "instance_name": self.items[0],
1346
      "disk": int(self.items[1]),
1347
      })
1348

    
1349

    
1350
class R_2_instances_name_console(baserlib.ResourceBase):
1351
  """/2/instances/[instance_name]/console resource.
1352

1353
  """
1354
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1355
  GET_OPCODE = opcodes.OpInstanceConsole
1356

    
1357
  def GET(self):
1358
    """Request information for connecting to instance's console.
1359

1360
    @return: Serialized instance console description, see
1361
             L{objects.InstanceConsole}
1362

1363
    """
1364
    instance_name = self.items[0]
1365
    client = self.GetClient(query=True)
1366

    
1367
    ((console, oper_state), ) = \
1368
        client.QueryInstances([instance_name], ["console", "oper_state"], False)
1369

    
1370
    if not oper_state:
1371
      raise http.HttpServiceUnavailable("Instance console unavailable")
1372

    
1373
    assert isinstance(console, dict)
1374
    return console
1375

    
1376

    
1377
def _GetQueryFields(args):
1378
  """Tries to extract C{fields} query parameter.
1379

1380
  @type args: dictionary
1381
  @rtype: list of string
1382
  @raise http.HttpBadRequest: When parameter can't be found
1383

1384
  """
1385
  try:
1386
    fields = args["fields"]
1387
  except KeyError:
1388
    raise http.HttpBadRequest("Missing 'fields' query argument")
1389

    
1390
  return _SplitQueryFields(fields[0])
1391

    
1392

    
1393
def _SplitQueryFields(fields):
1394
  """Splits fields as given for a query request.
1395

1396
  @type fields: string
1397
  @rtype: list of string
1398

1399
  """
1400
  return [i.strip() for i in fields.split(",")]
1401

    
1402

    
1403
class R_2_query(baserlib.ResourceBase):
1404
  """/2/query/[resource] resource.
1405

1406
  """
1407
  # Results might contain sensitive information
1408
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1409
  PUT_ACCESS = GET_ACCESS
1410
  GET_OPCODE = opcodes.OpQuery
1411
  PUT_OPCODE = opcodes.OpQuery
1412

    
1413
  def _Query(self, fields, qfilter):
1414
    client = self.GetClient()
1415
    return client.Query(self.items[0], fields, qfilter).ToDict()
1416

    
1417
  def GET(self):
1418
    """Returns resource information.
1419

1420
    @return: Query result, see L{objects.QueryResponse}
1421

1422
    """
1423
    return self._Query(_GetQueryFields(self.queryargs), None)
1424

    
1425
  def PUT(self):
1426
    """Submits job querying for resources.
1427

1428
    @return: Query result, see L{objects.QueryResponse}
1429

1430
    """
1431
    body = self.request_body
1432

    
1433
    baserlib.CheckType(body, dict, "Body contents")
1434

    
1435
    try:
1436
      fields = body["fields"]
1437
    except KeyError:
1438
      fields = _GetQueryFields(self.queryargs)
1439

    
1440
    qfilter = body.get("qfilter", None)
1441
    # TODO: remove this after 2.7
1442
    if qfilter is None:
1443
      qfilter = body.get("filter", None)
1444

    
1445
    return self._Query(fields, qfilter)
1446

    
1447

    
1448
class R_2_query_fields(baserlib.ResourceBase):
1449
  """/2/query/[resource]/fields resource.
1450

1451
  """
1452
  GET_OPCODE = opcodes.OpQueryFields
1453

    
1454
  def GET(self):
1455
    """Retrieves list of available fields for a resource.
1456

1457
    @return: List of serialized L{objects.QueryFieldDefinition}
1458

1459
    """
1460
    try:
1461
      raw_fields = self.queryargs["fields"]
1462
    except KeyError:
1463
      fields = None
1464
    else:
1465
      fields = _SplitQueryFields(raw_fields[0])
1466

    
1467
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1468

    
1469

    
1470
class _R_Tags(baserlib.OpcodeResource):
1471
  """Quasiclass for tagging resources.
1472

1473
  Manages tags. When inheriting this class you must define the
1474
  TAG_LEVEL for it.
1475

1476
  """
1477
  TAG_LEVEL = None
1478
  GET_OPCODE = opcodes.OpTagsGet
1479
  PUT_OPCODE = opcodes.OpTagsSet
1480
  DELETE_OPCODE = opcodes.OpTagsDel
1481

    
1482
  def __init__(self, items, queryargs, req, **kwargs):
1483
    """A tag resource constructor.
1484

1485
    We have to override the default to sort out cluster naming case.
1486

1487
    """
1488
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1489

    
1490
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1491
      self.name = None
1492
    else:
1493
      self.name = items[0]
1494

    
1495
  def GET(self):
1496
    """Returns a list of tags.
1497

1498
    Example: ["tag1", "tag2", "tag3"]
1499

1500
    """
1501
    kind = self.TAG_LEVEL
1502

    
1503
    if kind in (constants.TAG_INSTANCE,
1504
                constants.TAG_NODEGROUP,
1505
                constants.TAG_NODE):
1506
      if not self.name:
1507
        raise http.HttpBadRequest("Missing name on tag request")
1508

    
1509
      cl = self.GetClient(query=True)
1510
      tags = list(cl.QueryTags(kind, self.name))
1511

    
1512
    elif kind == constants.TAG_CLUSTER:
1513
      assert not self.name
1514
      # TODO: Use query API?
1515
      ssc = ssconf.SimpleStore()
1516
      tags = ssc.GetClusterTags()
1517

    
1518
    return list(tags)
1519

    
1520
  def GetPutOpInput(self):
1521
    """Add a set of tags.
1522

1523
    The request as a list of strings should be PUT to this URI. And
1524
    you'll have back a job id.
1525

1526
    """
1527
    return ({}, {
1528
      "kind": self.TAG_LEVEL,
1529
      "name": self.name,
1530
      "tags": self.queryargs.get("tag", []),
1531
      "dry_run": self.dryRun(),
1532
      })
1533

    
1534
  def GetDeleteOpInput(self):
1535
    """Delete a tag.
1536

1537
    In order to delete a set of tags, the DELETE
1538
    request should be addressed to URI like:
1539
    /tags?tag=[tag]&tag=[tag]
1540

1541
    """
1542
    # Re-use code
1543
    return self.GetPutOpInput()
1544

    
1545

    
1546
class R_2_instances_name_tags(_R_Tags):
1547
  """ /2/instances/[instance_name]/tags resource.
1548

1549
  Manages per-instance tags.
1550

1551
  """
1552
  TAG_LEVEL = constants.TAG_INSTANCE
1553

    
1554

    
1555
class R_2_nodes_name_tags(_R_Tags):
1556
  """ /2/nodes/[node_name]/tags resource.
1557

1558
  Manages per-node tags.
1559

1560
  """
1561
  TAG_LEVEL = constants.TAG_NODE
1562

    
1563

    
1564
class R_2_groups_name_tags(_R_Tags):
1565
  """ /2/groups/[group_name]/tags resource.
1566

1567
  Manages per-nodegroup tags.
1568

1569
  """
1570
  TAG_LEVEL = constants.TAG_NODEGROUP
1571

    
1572

    
1573
class R_2_networks_name_tags(_R_Tags):
1574
  """ /2/networks/[network_name]/tags resource.
1575

1576
  Manages per-network tags.
1577

1578
  """
1579
  TAG_LEVEL = constants.TAG_NETWORK
1580

    
1581

    
1582
class R_2_tags(_R_Tags):
1583
  """ /2/tags resource.
1584

1585
  Manages cluster tags.
1586

1587
  """
1588
  TAG_LEVEL = constants.TAG_CLUSTER