Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ a1578ccf

History | View | Annotate | Download (38.9 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
  GET_OPCODE = opcodes.OpNodeQuery
396

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

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

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

    
412

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

416
  """
417
  GET_OPCODE = opcodes.OpNodeQuery
418

    
419
  def GET(self):
420
    """Send information about a node.
421

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

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

    
430
    return baserlib.MapFields(N_FIELDS, result[0])
431

    
432

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

436
  """
437
  POST_OPCODE = opcodes.OpNodePowercycle
438

    
439
  def GetPostOpInput(self):
440
    """Tries to powercycle a node.
441

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

    
448

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

452
  """
453
  PUT_OPCODE = opcodes.OpNodeSetParams
454

    
455
  def GET(self):
456
    """Returns the current node role.
457

458
    @return: Node role
459

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

    
466
    return _NR_MAP[result[0][0]]
467

    
468
  def GetPutOpInput(self):
469
    """Sets the node role.
470

471
    """
472
    baserlib.CheckType(self.request_body, basestring, "Body contents")
473

    
474
    role = self.request_body
475

    
476
    if role == _NR_REGULAR:
477
      candidate = False
478
      offline = False
479
      drained = False
480

    
481
    elif role == _NR_MASTER_CANDIDATE:
482
      candidate = True
483
      offline = drained = None
484

    
485
    elif role == _NR_DRAINED:
486
      drained = True
487
      candidate = offline = None
488

    
489
    elif role == _NR_OFFLINE:
490
      offline = True
491
      candidate = drained = None
492

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

    
496
    assert len(self.items) == 1
497

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

    
507

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

511
  """
512
  POST_OPCODE = opcodes.OpNodeEvacuate
513

    
514
  def GetPostOpInput(self):
515
    """Evacuate all instances off a node.
516

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

    
523

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

527
  """
528
  POST_OPCODE = opcodes.OpNodeMigrate
529

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

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

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

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

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

    
558

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

562
  """
563
  POST_OPCODE = opcodes.OpNodeSetParams
564

    
565
  def GetPostOpInput(self):
566
    """Changes parameters of a node.
567

568
    """
569
    assert len(self.items) == 1
570

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

    
575

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

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

    
584
  def GetGetOpInput(self):
585
    """List storage available on a node.
586

587
    """
588
    storage_type = self._checkStringVariable("storage_type", None)
589
    output_fields = self._checkStringVariable("output_fields", None)
590

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

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

    
601

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

605
  """
606
  PUT_OPCODE = opcodes.OpNodeModifyStorage
607

    
608
  def GetPutOpInput(self):
609
    """Modifies a storage volume on a node.
610

611
    """
612
    storage_type = self._checkStringVariable("storage_type", None)
613
    name = self._checkStringVariable("name", None)
614

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

    
619
    changes = {}
620

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

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

    
632

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

636
  """
637
  PUT_OPCODE = opcodes.OpRepairNodeStorage
638

    
639
  def GetPutOpInput(self):
640
    """Repairs a storage volume on a node.
641

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

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

    
655

    
656
class R_2_networks(baserlib.OpcodeResource):
657
  """/2/networks resource.
658

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

    
666
  def GetPostOpInput(self):
667
    """Create a network.
668

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

    
675
  def GET(self):
676
    """Returns a list of all networks.
677

678
    """
679
    client = self.GetClient(query=True)
680

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

    
690

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

694
  """
695
  DELETE_OPCODE = opcodes.OpNetworkRemove
696

    
697
  def GET(self):
698
    """Send information about a network.
699

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

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

    
709
    return baserlib.MapFields(NET_FIELDS, result[0])
710

    
711
  def GetDeleteOpInput(self):
712
    """Delete a network.
713

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

    
721

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

725
  """
726
  PUT_OPCODE = opcodes.OpNetworkConnect
727

    
728
  def GetPutOpInput(self):
729
    """Changes some parameters of node group.
730

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

    
738

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

742
  """
743
  PUT_OPCODE = opcodes.OpNetworkDisconnect
744

    
745
  def GetPutOpInput(self):
746
    """Changes some parameters of node group.
747

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

    
755

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

759
  """
760
  PUT_OPCODE = opcodes.OpNetworkSetParams
761

    
762
  def GetPutOpInput(self):
763
    """Changes some parameters of network.
764

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

    
771

    
772
class R_2_groups(baserlib.OpcodeResource):
773
  """/2/groups resource.
774

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

    
782
  def GetPostOpInput(self):
783
    """Create a node group.
784

785

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

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

795
    """
796
    client = self.GetClient(query=True)
797

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

    
807

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

811
  """
812
  DELETE_OPCODE = opcodes.OpGroupRemove
813

    
814
  def GET(self):
815
    """Send information about a node group.
816

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

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

    
825
    return baserlib.MapFields(G_FIELDS, result[0])
826

    
827
  def GetDeleteOpInput(self):
828
    """Delete a node group.
829

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

    
837

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

841
  """
842
  PUT_OPCODE = opcodes.OpGroupSetParams
843

    
844
  def GetPutOpInput(self):
845
    """Changes some parameters of node group.
846

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

    
853

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

857
  """
858
  PUT_OPCODE = opcodes.OpGroupRename
859

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

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

    
870

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

874
  """
875
  PUT_OPCODE = opcodes.OpGroupAssignNodes
876

    
877
  def GetPutOpInput(self):
878
    """Assigns nodes to a group.
879

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

    
888

    
889
class R_2_instances(baserlib.OpcodeResource):
890
  """/2/instances resource.
891

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

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

903
    """
904
    client = self.GetClient()
905

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

    
916
  def GetPostOpInput(self):
917
    """Create an instance.
918

919
    @return: a job id
920

921
    """
922
    baserlib.CheckType(self.request_body, dict, "Body contents")
923

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

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

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

    
938
    return (data, {
939
      "dry_run": self.dryRun(),
940
      })
941

    
942

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

946
  """
947
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
948

    
949
  def GetPostOpInput(self):
950
    """Try to allocate multiple instances.
951

952
    @return: A dict with submitted jobs, allocatable instances and failed
953
             allocations
954

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

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

    
968
    return (body, {
969
      "dry_run": self.dryRun(),
970
      })
971

    
972

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

976
  """
977
  GET_OPCODE = opcodes.OpInstanceQuery
978
  DELETE_OPCODE = opcodes.OpInstanceRemove
979

    
980
  def GET(self):
981
    """Send information about an instance.
982

983
    """
984
    client = self.GetClient()
985
    instance_name = self.items[0]
986

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

    
992
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
993

    
994
  def GetDeleteOpInput(self):
995
    """Delete an instance.
996

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

    
1005

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

1009
  """
1010
  GET_OPCODE = opcodes.OpInstanceQueryData
1011

    
1012
  def GetGetOpInput(self):
1013
    """Request detailed instance information.
1014

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

    
1022

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

1026
  Implements an instance reboot.
1027

1028
  """
1029
  POST_OPCODE = opcodes.OpInstanceReboot
1030

    
1031
  def GetPostOpInput(self):
1032
    """Reboot an instance.
1033

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

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

    
1046

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

1050
  Implements an instance startup.
1051

1052
  """
1053
  PUT_OPCODE = opcodes.OpInstanceStartup
1054

    
1055
  def GetPutOpInput(self):
1056
    """Startup an instance.
1057

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

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

    
1069

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

1073
  Implements an instance shutdown.
1074

1075
  """
1076
  PUT_OPCODE = opcodes.OpInstanceShutdown
1077

    
1078
  def GetPutOpInput(self):
1079
    """Shutdown an instance.
1080

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

    
1088

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

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

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

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

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

    
1110
  return ops
1111

    
1112

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

1116
  Implements an instance reinstall.
1117

1118
  """
1119
  POST_OPCODE = opcodes.OpInstanceReinstall
1120

    
1121
  def POST(self):
1122
    """Reinstall an instance.
1123

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

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

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

    
1143
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1144

    
1145
    return self.SubmitJob(ops)
1146

    
1147

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

1151
  """
1152
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1153

    
1154
  def GetPostOpInput(self):
1155
    """Replaces disks on an instance.
1156

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

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

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

    
1191
    return (data, static)
1192

    
1193

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

1197
  """
1198
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1199

    
1200
  def GetPutOpInput(self):
1201
    """Activate disks for an instance.
1202

1203
    The URI might contain ignore_size to ignore current recorded size.
1204

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

    
1211

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

1215
  """
1216
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1217

    
1218
  def GetPutOpInput(self):
1219
    """Deactivate disks for an instance.
1220

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

    
1226

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

1230
  """
1231
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1232

    
1233
  def GetPostOpInput(self):
1234
    """Recreate disks for an instance.
1235

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

    
1241

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

1245
  """
1246
  PUT_OPCODE = opcodes.OpBackupPrepare
1247

    
1248
  def GetPutOpInput(self):
1249
    """Prepares an export for an instance.
1250

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

    
1257

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

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

    
1267
  def GetPutOpInput(self):
1268
    """Exports an instance.
1269

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

    
1275

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

1279
  """
1280
  PUT_OPCODE = opcodes.OpInstanceMigrate
1281

    
1282
  def GetPutOpInput(self):
1283
    """Migrates an instance.
1284

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

    
1290

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

1294
  """
1295
  PUT_OPCODE = opcodes.OpInstanceFailover
1296

    
1297
  def GetPutOpInput(self):
1298
    """Does a failover of an instance.
1299

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

    
1305

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

1309
  """
1310
  PUT_OPCODE = opcodes.OpInstanceRename
1311

    
1312
  def GetPutOpInput(self):
1313
    """Changes the name of an instance.
1314

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

    
1320

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

1324
  """
1325
  PUT_OPCODE = opcodes.OpInstanceSetParams
1326

    
1327
  def GetPutOpInput(self):
1328
    """Changes parameters of an instance.
1329

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

    
1335

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

1339
  """
1340
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1341

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

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

    
1351

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

1355
  """
1356
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1357
  GET_OPCODE = opcodes.OpInstanceConsole
1358

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

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

1365
    """
1366
    client = self.GetClient()
1367

    
1368
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1369

    
1370
    if console is None:
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
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1415

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

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

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

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

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

1429
    """
1430
    body = self.request_body
1431

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

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

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

    
1444
    return self._Query(fields, qfilter)
1445

    
1446

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

1450
  """
1451
  GET_OPCODE = opcodes.OpQueryFields
1452

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

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

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

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

    
1468

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

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

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

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

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

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

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

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

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

1499
    """
1500
    kind = self.TAG_LEVEL
1501

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

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

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

    
1517
    return list(tags)
1518

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

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

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

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

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

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

    
1544

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

1548
  Manages per-instance tags.
1549

1550
  """
1551
  TAG_LEVEL = constants.TAG_INSTANCE
1552

    
1553

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

1557
  Manages per-node tags.
1558

1559
  """
1560
  TAG_LEVEL = constants.TAG_NODE
1561

    
1562

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

1566
  Manages per-nodegroup tags.
1567

1568
  """
1569
  TAG_LEVEL = constants.TAG_NODEGROUP
1570

    
1571

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

1575
  Manages per-network tags.
1576

1577
  """
1578
  TAG_LEVEL = constants.TAG_NETWORK
1579

    
1580

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

1584
  Manages cluster tags.
1585

1586
  """
1587
  TAG_LEVEL = constants.TAG_CLUSTER