Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ bb579a7d

History | View | Annotate | Download (21.8 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008 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 version 2 baserlib.library.
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
  To be in context of this module for instance creation POST on
32
  /2/instances is legitim while PUT would be not, due to it does create a
33
  new entity and not just replace /2/instances with it.
34

35
  So when adding new methods, if they are operating on the URI entity itself,
36
  PUT should be prefered over POST.
37

38
"""
39

    
40
# pylint: disable-msg=C0103
41

    
42
# C0103: Invalid name, since the R_* names are not conforming
43

    
44
from ganeti import opcodes
45
from ganeti import http
46
from ganeti import constants
47
from ganeti import cli
48
from ganeti import rapi
49
from ganeti.rapi import baserlib
50

    
51

    
52
_COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
53
I_FIELDS = ["name", "admin_state", "os",
54
            "pnode", "snodes",
55
            "disk_template",
56
            "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
57
            "network_port",
58
            "disk.sizes", "disk_usage",
59
            "beparams", "hvparams",
60
            "oper_state", "oper_ram", "status",
61
            ] + _COMMON_FIELDS
62

    
63
N_FIELDS = ["name", "offline", "master_candidate", "drained",
64
            "dtotal", "dfree",
65
            "mtotal", "mnode", "mfree",
66
            "pinst_cnt", "sinst_cnt",
67
            "ctotal", "cnodes", "csockets",
68
            "pip", "sip", "role",
69
            "pinst_list", "sinst_list",
70
            ] + _COMMON_FIELDS
71

    
72
_NR_DRAINED = "drained"
73
_NR_MASTER_CANDIATE = "master-candidate"
74
_NR_MASTER = "master"
75
_NR_OFFLINE = "offline"
76
_NR_REGULAR = "regular"
77

    
78
_NR_MAP = {
79
  "M": _NR_MASTER,
80
  "C": _NR_MASTER_CANDIATE,
81
  "D": _NR_DRAINED,
82
  "O": _NR_OFFLINE,
83
  "R": _NR_REGULAR,
84
  }
85

    
86

    
87
class R_version(baserlib.R_Generic):
88
  """/version resource.
89

90
  This resource should be used to determine the remote API version and
91
  to adapt clients accordingly.
92

93
  """
94
  @staticmethod
95
  def GET():
96
    """Returns the remote API version.
97

98
    """
99
    return constants.RAPI_VERSION
100

    
101

    
102
class R_2_info(baserlib.R_Generic):
103
  """Cluster info.
104

105
  """
106
  @staticmethod
107
  def GET():
108
    """Returns cluster information.
109

110
    """
111
    client = baserlib.GetClient()
112
    return client.QueryClusterInfo()
113

    
114

    
115
class R_2_os(baserlib.R_Generic):
116
  """/2/os resource.
117

118
  """
119
  @staticmethod
120
  def GET():
121
    """Return a list of all OSes.
122

123
    Can return error 500 in case of a problem.
124

125
    Example: ["debian-etch"]
126

127
    """
128
    cl = baserlib.GetClient()
129
    op = opcodes.OpDiagnoseOS(output_fields=["name", "valid", "variants"],
130
                              names=[])
131
    job_id = baserlib.SubmitJob([op], cl)
132
    # we use custom feedback function, instead of print we log the status
133
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
134
    diagnose_data = result[0]
135

    
136
    if not isinstance(diagnose_data, list):
137
      raise http.HttpBadGateway(message="Can't get OS list")
138

    
139
    os_names = []
140
    for (name, valid, variants) in diagnose_data:
141
      if valid:
142
        os_names.extend(cli.CalculateOSNames(name, variants))
143

    
144
    return os_names
145

    
146

    
147
class R_2_redist_config(baserlib.R_Generic):
148
  """/2/redistribute-config resource.
149

150
  """
151
  @staticmethod
152
  def PUT():
153
    """Redistribute configuration to all nodes.
154

155
    """
156
    return baserlib.SubmitJob([opcodes.OpRedistributeConfig()])
157

    
158

    
159
class R_2_jobs(baserlib.R_Generic):
160
  """/2/jobs resource.
161

162
  """
163
  @staticmethod
164
  def GET():
165
    """Returns a dictionary of jobs.
166

167
    @return: a dictionary with jobs id and uri.
168

169
    """
170
    fields = ["id"]
171
    cl = baserlib.GetClient()
172
    # Convert the list of lists to the list of ids
173
    result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
174
    return baserlib.BuildUriList(result, "/2/jobs/%s",
175
                                 uri_fields=("id", "uri"))
176

    
177

    
178
class R_2_jobs_id(baserlib.R_Generic):
179
  """/2/jobs/[job_id] resource.
180

181
  """
182
  def GET(self):
183
    """Returns a job status.
184

185
    @return: a dictionary with job parameters.
186
        The result includes:
187
            - id: job ID as a number
188
            - status: current job status as a string
189
            - ops: involved OpCodes as a list of dictionaries for each
190
              opcodes in the job
191
            - opstatus: OpCodes status as a list
192
            - opresult: OpCodes results as a list of lists
193

194
    """
195
    fields = ["id", "ops", "status", "summary",
196
              "opstatus", "opresult", "oplog",
197
              "received_ts", "start_ts", "end_ts",
198
              ]
199
    job_id = self.items[0]
200
    result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
201
    if result is None:
202
      raise http.HttpNotFound()
203
    return baserlib.MapFields(fields, result)
204

    
205
  def DELETE(self):
206
    """Cancel not-yet-started job.
207

208
    """
209
    job_id = self.items[0]
210
    result = baserlib.GetClient().CancelJob(job_id)
211
    return result
212

    
213

    
214
class R_2_nodes(baserlib.R_Generic):
215
  """/2/nodes resource.
216

217
  """
218
  def GET(self):
219
    """Returns a list of all nodes.
220

221
    """
222
    client = baserlib.GetClient()
223

    
224
    if self.useBulk():
225
      bulkdata = client.QueryNodes([], N_FIELDS, False)
226
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
227
    else:
228
      nodesdata = client.QueryNodes([], ["name"], False)
229
      nodeslist = [row[0] for row in nodesdata]
230
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
231
                                   uri_fields=("id", "uri"))
232

    
233

    
234
class R_2_nodes_name(baserlib.R_Generic):
235
  """/2/nodes/[node_name] resources.
236

237
  """
238
  def GET(self):
239
    """Send information about a node.
240

241
    """
242
    node_name = self.items[0]
243
    client = baserlib.GetClient()
244
    result = client.QueryNodes(names=[node_name], fields=N_FIELDS,
245
                               use_locking=self.useLocking())
246

    
247
    return baserlib.MapFields(N_FIELDS, result[0])
248

    
249

    
250
class R_2_nodes_name_role(baserlib.R_Generic):
251
  """ /2/nodes/[node_name]/role resource.
252

253
  """
254
  def GET(self):
255
    """Returns the current node role.
256

257
    @return: Node role
258

259
    """
260
    node_name = self.items[0]
261
    client = baserlib.GetClient()
262
    result = client.QueryNodes(names=[node_name], fields=["role"],
263
                               use_locking=self.useLocking())
264

    
265
    return _NR_MAP[result[0][0]]
266

    
267
  def PUT(self):
268
    """Sets the node role.
269

270
    @return: a job id
271

272
    """
273
    if not isinstance(self.req.request_body, basestring):
274
      raise http.HttpBadRequest("Invalid body contents, not a string")
275

    
276
    node_name = self.items[0]
277
    role = self.req.request_body
278

    
279
    if role == _NR_REGULAR:
280
      candidate = False
281
      offline = False
282
      drained = False
283

    
284
    elif role == _NR_MASTER_CANDIATE:
285
      candidate = True
286
      offline = drained = None
287

    
288
    elif role == _NR_DRAINED:
289
      drained = True
290
      candidate = offline = None
291

    
292
    elif role == _NR_OFFLINE:
293
      offline = True
294
      candidate = drained = None
295

    
296
    else:
297
      raise http.HttpBadRequest("Can't set '%s' role" % role)
298

    
299
    op = opcodes.OpSetNodeParams(node_name=node_name,
300
                                 master_candidate=candidate,
301
                                 offline=offline,
302
                                 drained=drained,
303
                                 force=bool(self.useForce()))
304

    
305
    return baserlib.SubmitJob([op])
306

    
307

    
308
class R_2_nodes_name_evacuate(baserlib.R_Generic):
309
  """/2/nodes/[node_name]/evacuate resource.
310

311
  """
312
  def POST(self):
313
    """Evacuate all secondary instances off a node.
314

315
    """
316
    node_name = self.items[0]
317
    remote_node = self._checkStringVariable("remote_node", default=None)
318
    iallocator = self._checkStringVariable("iallocator", default=None)
319

    
320
    op = opcodes.OpEvacuateNode(node_name=node_name,
321
                                remote_node=remote_node,
322
                                iallocator=iallocator)
323

    
324
    return baserlib.SubmitJob([op])
325

    
326

    
327
class R_2_nodes_name_migrate(baserlib.R_Generic):
328
  """/2/nodes/[node_name]/migrate resource.
329

330
  """
331
  def POST(self):
332
    """Migrate all primary instances from a node.
333

334
    """
335
    node_name = self.items[0]
336
    live = bool(self._checkIntVariable("live", default=1))
337

    
338
    op = opcodes.OpMigrateNode(node_name=node_name, live=live)
339

    
340
    return baserlib.SubmitJob([op])
341

    
342

    
343
class R_2_nodes_name_storage(baserlib.R_Generic):
344
  """/2/nodes/[node_name]/storage ressource.
345

346
  """
347
  # LUQueryNodeStorage acquires locks, hence restricting access to GET
348
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
349

    
350
  def GET(self):
351
    node_name = self.items[0]
352

    
353
    storage_type = self._checkStringVariable("storage_type", None)
354
    if not storage_type:
355
      raise http.HttpBadRequest("Missing the required 'storage_type'"
356
                                " parameter")
357

    
358
    output_fields = self._checkStringVariable("output_fields", None)
359
    if not output_fields:
360
      raise http.HttpBadRequest("Missing the required 'output_fields'"
361
                                " parameter")
362

    
363
    op = opcodes.OpQueryNodeStorage(nodes=[node_name],
364
                                    storage_type=storage_type,
365
                                    output_fields=output_fields.split(","))
366
    return baserlib.SubmitJob([op])
367

    
368

    
369
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
370
  """/2/nodes/[node_name]/storage/modify ressource.
371

372
  """
373
  def PUT(self):
374
    node_name = self.items[0]
375

    
376
    storage_type = self._checkStringVariable("storage_type", None)
377
    if not storage_type:
378
      raise http.HttpBadRequest("Missing the required 'storage_type'"
379
                                " parameter")
380

    
381
    name = self._checkStringVariable("name", None)
382
    if not name:
383
      raise http.HttpBadRequest("Missing the required 'name'"
384
                                " parameter")
385

    
386
    changes = {}
387

    
388
    if "allocatable" in self.queryargs:
389
      changes[constants.SF_ALLOCATABLE] = \
390
        bool(self._checkIntVariable("allocatable", default=1))
391

    
392
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
393
                                     storage_type=storage_type,
394
                                     name=name,
395
                                     changes=changes)
396
    return baserlib.SubmitJob([op])
397

    
398

    
399
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
400
  """/2/nodes/[node_name]/storage/repair ressource.
401

402
  """
403
  def PUT(self):
404
    node_name = self.items[0]
405

    
406
    storage_type = self._checkStringVariable("storage_type", None)
407
    if not storage_type:
408
      raise http.HttpBadRequest("Missing the required 'storage_type'"
409
                                " parameter")
410

    
411
    name = self._checkStringVariable("name", None)
412
    if not name:
413
      raise http.HttpBadRequest("Missing the required 'name'"
414
                                " parameter")
415

    
416
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
417
                                     storage_type=storage_type,
418
                                     name=name)
419
    return baserlib.SubmitJob([op])
420

    
421

    
422
class R_2_instances(baserlib.R_Generic):
423
  """/2/instances resource.
424

425
  """
426
  def GET(self):
427
    """Returns a list of all available instances.
428

429
    """
430
    client = baserlib.GetClient()
431

    
432
    use_locking = self.useLocking()
433
    if self.useBulk():
434
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
435
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
436
    else:
437
      instancesdata = client.QueryInstances([], ["name"], use_locking)
438
      instanceslist = [row[0] for row in instancesdata]
439
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
440
                                   uri_fields=("id", "uri"))
441

    
442
  def POST(self):
443
    """Create an instance.
444

445
    @return: a job id
446

447
    """
448
    if not isinstance(self.req.request_body, dict):
449
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
450

    
451
    beparams = baserlib.MakeParamsDict(self.req.request_body,
452
                                       constants.BES_PARAMETERS)
453
    hvparams = baserlib.MakeParamsDict(self.req.request_body,
454
                                       constants.HVS_PARAMETERS)
455
    fn = self.getBodyParameter
456

    
457
    # disk processing
458
    disk_data = fn('disks')
459
    if not isinstance(disk_data, list):
460
      raise http.HttpBadRequest("The 'disks' parameter should be a list")
461
    disks = []
462
    for idx, d in enumerate(disk_data):
463
      if not isinstance(d, int):
464
        raise http.HttpBadRequest("Disk %d specification wrong: should"
465
                                  " be an integer" % idx)
466
      disks.append({"size": d})
467
    # nic processing (one nic only)
468
    nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
469
    if fn("ip", None) is not None:
470
      nics[0]["ip"] = fn("ip")
471
    if fn("mode", None) is not None:
472
      nics[0]["mode"] = fn("mode")
473
    if fn("link", None) is not None:
474
      nics[0]["link"] = fn("link")
475
    if fn("bridge", None) is not None:
476
      nics[0]["bridge"] = fn("bridge")
477

    
478
    op = opcodes.OpCreateInstance(
479
      mode=constants.INSTANCE_CREATE,
480
      instance_name=fn('name'),
481
      disks=disks,
482
      disk_template=fn('disk_template'),
483
      os_type=fn('os'),
484
      pnode=fn('pnode', None),
485
      snode=fn('snode', None),
486
      iallocator=fn('iallocator', None),
487
      nics=nics,
488
      start=fn('start', True),
489
      ip_check=fn('ip_check', True),
490
      name_check=fn('name_check', True),
491
      wait_for_sync=True,
492
      hypervisor=fn('hypervisor', None),
493
      hvparams=hvparams,
494
      beparams=beparams,
495
      file_storage_dir=fn('file_storage_dir', None),
496
      file_driver=fn('file_driver', 'loop'),
497
      dry_run=bool(self.dryRun()),
498
      )
499

    
500
    return baserlib.SubmitJob([op])
501

    
502

    
503
class R_2_instances_name(baserlib.R_Generic):
504
  """/2/instances/[instance_name] resources.
505

506
  """
507
  def GET(self):
508
    """Send information about an instance.
509

510
    """
511
    client = baserlib.GetClient()
512
    instance_name = self.items[0]
513
    result = client.QueryInstances(names=[instance_name], fields=I_FIELDS,
514
                                   use_locking=self.useLocking())
515

    
516
    return baserlib.MapFields(I_FIELDS, result[0])
517

    
518
  def DELETE(self):
519
    """Delete an instance.
520

521
    """
522
    op = opcodes.OpRemoveInstance(instance_name=self.items[0],
523
                                  ignore_failures=False,
524
                                  dry_run=bool(self.dryRun()))
525
    return baserlib.SubmitJob([op])
526

    
527

    
528
class R_2_instances_name_info(baserlib.R_Generic):
529
  """/2/instances/[instance_name]/info resource.
530

531
  """
532
  def GET(self):
533
    """Request detailed instance information.
534

535
    """
536
    instance_name = self.items[0]
537
    static = bool(self._checkIntVariable("static", default=0))
538

    
539
    op = opcodes.OpQueryInstanceData(instances=[instance_name],
540
                                     static=static)
541
    return baserlib.SubmitJob([op])
542

    
543

    
544
class R_2_instances_name_reboot(baserlib.R_Generic):
545
  """/2/instances/[instance_name]/reboot resource.
546

547
  Implements an instance reboot.
548

549
  """
550
  def POST(self):
551
    """Reboot an instance.
552

553
    The URI takes type=[hard|soft|full] and
554
    ignore_secondaries=[False|True] parameters.
555

556
    """
557
    instance_name = self.items[0]
558
    reboot_type = self.queryargs.get('type',
559
                                     [constants.INSTANCE_REBOOT_HARD])[0]
560
    ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
561
    op = opcodes.OpRebootInstance(instance_name=instance_name,
562
                                  reboot_type=reboot_type,
563
                                  ignore_secondaries=ignore_secondaries,
564
                                  dry_run=bool(self.dryRun()))
565

    
566
    return baserlib.SubmitJob([op])
567

    
568

    
569
class R_2_instances_name_startup(baserlib.R_Generic):
570
  """/2/instances/[instance_name]/startup resource.
571

572
  Implements an instance startup.
573

574
  """
575
  def PUT(self):
576
    """Startup an instance.
577

578
    The URI takes force=[False|True] parameter to start the instance
579
    if even if secondary disks are failing.
580

581
    """
582
    instance_name = self.items[0]
583
    force_startup = bool(self._checkIntVariable('force'))
584
    op = opcodes.OpStartupInstance(instance_name=instance_name,
585
                                   force=force_startup,
586
                                   dry_run=bool(self.dryRun()))
587

    
588
    return baserlib.SubmitJob([op])
589

    
590

    
591
class R_2_instances_name_shutdown(baserlib.R_Generic):
592
  """/2/instances/[instance_name]/shutdown resource.
593

594
  Implements an instance shutdown.
595

596
  """
597
  def PUT(self):
598
    """Shutdown an instance.
599

600
    """
601
    instance_name = self.items[0]
602
    op = opcodes.OpShutdownInstance(instance_name=instance_name,
603
                                    dry_run=bool(self.dryRun()))
604

    
605
    return baserlib.SubmitJob([op])
606

    
607

    
608
class R_2_instances_name_reinstall(baserlib.R_Generic):
609
  """/2/instances/[instance_name]/reinstall resource.
610

611
  Implements an instance reinstall.
612

613
  """
614
  def POST(self):
615
    """Reinstall an instance.
616

617
    The URI takes os=name and nostartup=[0|1] optional
618
    parameters. By default, the instance will be started
619
    automatically.
620

621
    """
622
    instance_name = self.items[0]
623
    ostype = self._checkStringVariable('os')
624
    nostartup = self._checkIntVariable('nostartup')
625
    ops = [
626
      opcodes.OpShutdownInstance(instance_name=instance_name),
627
      opcodes.OpReinstallInstance(instance_name=instance_name, os_type=ostype),
628
      ]
629
    if not nostartup:
630
      ops.append(opcodes.OpStartupInstance(instance_name=instance_name,
631
                                           force=False))
632
    return baserlib.SubmitJob(ops)
633

    
634

    
635
class R_2_instances_name_replace_disks(baserlib.R_Generic):
636
  """/2/instances/[instance_name]/replace-disks resource.
637

638
  """
639
  def POST(self):
640
    """Replaces disks on an instance.
641

642
    """
643
    instance_name = self.items[0]
644
    remote_node = self._checkStringVariable("remote_node", default=None)
645
    mode = self._checkStringVariable("mode", default=None)
646
    raw_disks = self._checkStringVariable("disks", default=None)
647
    iallocator = self._checkStringVariable("iallocator", default=None)
648

    
649
    if raw_disks:
650
      try:
651
        disks = [int(part) for part in raw_disks.split(",")]
652
      except ValueError, err:
653
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
654
    else:
655
      disks = []
656

    
657
    op = opcodes.OpReplaceDisks(instance_name=instance_name,
658
                                remote_node=remote_node,
659
                                mode=mode,
660
                                disks=disks,
661
                                iallocator=iallocator)
662

    
663
    return baserlib.SubmitJob([op])
664

    
665

    
666
class R_2_instances_name_activate_disks(baserlib.R_Generic):
667
  """/2/instances/[instance_name]/activate-disks resource.
668

669
  """
670
  def PUT(self):
671
    """Activate disks for an instance.
672

673
    The URI might contain ignore_size to ignore current recorded size.
674

675
    """
676
    instance_name = self.items[0]
677
    ignore_size = bool(self._checkIntVariable('ignore_size'))
678

    
679
    op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
680
                                         ignore_size=ignore_size)
681

    
682
    return baserlib.SubmitJob([op])
683

    
684

    
685
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
686
  """/2/instances/[instance_name]/deactivate-disks resource.
687

688
  """
689
  def PUT(self):
690
    """Deactivate disks for an instance.
691

692
    """
693
    instance_name = self.items[0]
694

    
695
    op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
696

    
697
    return baserlib.SubmitJob([op])
698

    
699

    
700
class _R_Tags(baserlib.R_Generic):
701
  """ Quasiclass for tagging resources
702

703
  Manages tags. When inheriting this class you must define the
704
  TAG_LEVEL for it.
705

706
  """
707
  TAG_LEVEL = None
708

    
709
  def __init__(self, items, queryargs, req):
710
    """A tag resource constructor.
711

712
    We have to override the default to sort out cluster naming case.
713

714
    """
715
    baserlib.R_Generic.__init__(self, items, queryargs, req)
716

    
717
    if self.TAG_LEVEL != constants.TAG_CLUSTER:
718
      self.name = items[0]
719
    else:
720
      self.name = ""
721

    
722
  def GET(self):
723
    """Returns a list of tags.
724

725
    Example: ["tag1", "tag2", "tag3"]
726

727
    """
728
    # pylint: disable-msg=W0212
729
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
730

    
731
  def PUT(self):
732
    """Add a set of tags.
733

734
    The request as a list of strings should be PUT to this URI. And
735
    you'll have back a job id.
736

737
    """
738
    # pylint: disable-msg=W0212
739
    if 'tag' not in self.queryargs:
740
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
741
                                " the 'tag' parameter")
742
    return baserlib._Tags_PUT(self.TAG_LEVEL,
743
                              self.queryargs['tag'], name=self.name,
744
                              dry_run=bool(self.dryRun()))
745

    
746
  def DELETE(self):
747
    """Delete a tag.
748

749
    In order to delete a set of tags, the DELETE
750
    request should be addressed to URI like:
751
    /tags?tag=[tag]&tag=[tag]
752

753
    """
754
    # pylint: disable-msg=W0212
755
    if 'tag' not in self.queryargs:
756
      # no we not gonna delete all tags
757
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
758
                                " tag(s) using the 'tag' parameter")
759
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
760
                                 self.queryargs['tag'],
761
                                 name=self.name,
762
                                 dry_run=bool(self.dryRun()))
763

    
764

    
765
class R_2_instances_name_tags(_R_Tags):
766
  """ /2/instances/[instance_name]/tags resource.
767

768
  Manages per-instance tags.
769

770
  """
771
  TAG_LEVEL = constants.TAG_INSTANCE
772

    
773

    
774
class R_2_nodes_name_tags(_R_Tags):
775
  """ /2/nodes/[node_name]/tags resource.
776

777
  Manages per-node tags.
778

779
  """
780
  TAG_LEVEL = constants.TAG_NODE
781

    
782

    
783
class R_2_tags(_R_Tags):
784
  """ /2/instances/tags resource.
785

786
  Manages cluster tags.
787

788
  """
789
  TAG_LEVEL = constants.TAG_CLUSTER