Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ d9fdd354

History | View | Annotate | Download (40.1 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
def _ConvertUsbDevices(data):
890
  """Convert in place the usb_devices string to the proper format.
891

892
  In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
893
  comma to space because commas cannot be accepted on the command line
894
  (they already act as the separator between different hvparams). RAPI
895
  should be able to accept commas for backwards compatibility, but we want
896
  it to also accept the new space separator. Therefore, we convert
897
  spaces into commas here and keep the old parsing logic elsewhere.
898

899
  """
900
  try:
901
    hvparams = data["hvparams"]
902
    usb_devices = hvparams[constants.HV_USB_DEVICES]
903
    hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
904
    data["hvparams"] = hvparams
905
  except KeyError:
906
    #No usb_devices, no modification required
907
    pass
908

    
909

    
910
class R_2_instances(baserlib.OpcodeResource):
911
  """/2/instances resource.
912

913
  """
914
  GET_OPCODE = opcodes.OpInstanceQuery
915
  POST_OPCODE = opcodes.OpInstanceCreate
916
  POST_RENAME = {
917
    "os": "os_type",
918
    "name": "instance_name",
919
    }
920

    
921
  def GET(self):
922
    """Returns a list of all available instances.
923

924
    """
925
    client = self.GetClient()
926

    
927
    use_locking = self.useLocking()
928
    if self.useBulk():
929
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
930
      return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
931
    else:
932
      instancesdata = client.QueryInstances([], ["name"], use_locking)
933
      instanceslist = [row[0] for row in instancesdata]
934
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
935
                                   uri_fields=("id", "uri"))
936

    
937
  def GetPostOpInput(self):
938
    """Create an instance.
939

940
    @return: a job id
941

942
    """
943
    baserlib.CheckType(self.request_body, dict, "Body contents")
944

    
945
    # Default to request data version 0
946
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
947

    
948
    if data_version == 0:
949
      raise http.HttpBadRequest("Instance creation request version 0 is no"
950
                                " longer supported")
951
    elif data_version != 1:
952
      raise http.HttpBadRequest("Unsupported request data version %s" %
953
                                data_version)
954

    
955
    data = self.request_body.copy()
956
    # Remove "__version__"
957
    data.pop(_REQ_DATA_VERSION, None)
958

    
959
    _ConvertUsbDevices(data)
960

    
961
    return (data, {
962
      "dry_run": self.dryRun(),
963
      })
964

    
965

    
966
class R_2_instances_multi_alloc(baserlib.OpcodeResource):
967
  """/2/instances-multi-alloc resource.
968

969
  """
970
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
971

    
972
  def GetPostOpInput(self):
973
    """Try to allocate multiple instances.
974

975
    @return: A dict with submitted jobs, allocatable instances and failed
976
             allocations
977

978
    """
979
    if "instances" not in self.request_body:
980
      raise http.HttpBadRequest("Request is missing required 'instances' field"
981
                                " in body")
982

    
983
    # Unlike most other RAPI calls, this one is composed of individual opcodes,
984
    # and we have to do the filling ourselves
985
    OPCODE_RENAME = {
986
      "os": "os_type",
987
      "name": "instance_name",
988
    }
989

    
990
    body = objects.FillDict(self.request_body, {
991
      "instances": [
992
        baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {}, OPCODE_RENAME)
993
        for inst in self.request_body["instances"]
994
        ],
995
      })
996

    
997
    return (body, {
998
      "dry_run": self.dryRun(),
999
      })
1000

    
1001

    
1002
class R_2_instances_name(baserlib.OpcodeResource):
1003
  """/2/instances/[instance_name] resource.
1004

1005
  """
1006
  GET_OPCODE = opcodes.OpInstanceQuery
1007
  DELETE_OPCODE = opcodes.OpInstanceRemove
1008

    
1009
  def GET(self):
1010
    """Send information about an instance.
1011

1012
    """
1013
    client = self.GetClient()
1014
    instance_name = self.items[0]
1015

    
1016
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1017
                                            names=[instance_name],
1018
                                            fields=I_FIELDS,
1019
                                            use_locking=self.useLocking())
1020

    
1021
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1022

    
1023
  def GetDeleteOpInput(self):
1024
    """Delete an instance.
1025

1026
    """
1027
    assert len(self.items) == 1
1028
    return ({}, {
1029
      "instance_name": self.items[0],
1030
      "ignore_failures": False,
1031
      "dry_run": self.dryRun(),
1032
      })
1033

    
1034

    
1035
class R_2_instances_name_info(baserlib.OpcodeResource):
1036
  """/2/instances/[instance_name]/info resource.
1037

1038
  """
1039
  GET_OPCODE = opcodes.OpInstanceQueryData
1040

    
1041
  def GetGetOpInput(self):
1042
    """Request detailed instance information.
1043

1044
    """
1045
    assert len(self.items) == 1
1046
    return ({}, {
1047
      "instances": [self.items[0]],
1048
      "static": bool(self._checkIntVariable("static", default=0)),
1049
      })
1050

    
1051

    
1052
class R_2_instances_name_reboot(baserlib.OpcodeResource):
1053
  """/2/instances/[instance_name]/reboot resource.
1054

1055
  Implements an instance reboot.
1056

1057
  """
1058
  POST_OPCODE = opcodes.OpInstanceReboot
1059

    
1060
  def GetPostOpInput(self):
1061
    """Reboot an instance.
1062

1063
    The URI takes type=[hard|soft|full] and
1064
    ignore_secondaries=[False|True] parameters.
1065

1066
    """
1067
    return ({}, {
1068
      "instance_name": self.items[0],
1069
      "reboot_type":
1070
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1071
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1072
      "dry_run": self.dryRun(),
1073
      })
1074

    
1075

    
1076
class R_2_instances_name_startup(baserlib.OpcodeResource):
1077
  """/2/instances/[instance_name]/startup resource.
1078

1079
  Implements an instance startup.
1080

1081
  """
1082
  PUT_OPCODE = opcodes.OpInstanceStartup
1083

    
1084
  def GetPutOpInput(self):
1085
    """Startup an instance.
1086

1087
    The URI takes force=[False|True] parameter to start the instance
1088
    if even if secondary disks are failing.
1089

1090
    """
1091
    return ({}, {
1092
      "instance_name": self.items[0],
1093
      "force": self.useForce(),
1094
      "dry_run": self.dryRun(),
1095
      "no_remember": bool(self._checkIntVariable("no_remember")),
1096
      })
1097

    
1098

    
1099
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1100
  """/2/instances/[instance_name]/shutdown resource.
1101

1102
  Implements an instance shutdown.
1103

1104
  """
1105
  PUT_OPCODE = opcodes.OpInstanceShutdown
1106

    
1107
  def GetPutOpInput(self):
1108
    """Shutdown an instance.
1109

1110
    """
1111
    return (self.request_body, {
1112
      "instance_name": self.items[0],
1113
      "no_remember": bool(self._checkIntVariable("no_remember")),
1114
      "dry_run": self.dryRun(),
1115
      })
1116

    
1117

    
1118
def _ParseInstanceReinstallRequest(name, data):
1119
  """Parses a request for reinstalling an instance.
1120

1121
  """
1122
  if not isinstance(data, dict):
1123
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1124

    
1125
  ostype = baserlib.CheckParameter(data, "os", default=None)
1126
  start = baserlib.CheckParameter(data, "start", exptype=bool,
1127
                                  default=True)
1128
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
1129

    
1130
  ops = [
1131
    opcodes.OpInstanceShutdown(instance_name=name),
1132
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1133
                                osparams=osparams),
1134
    ]
1135

    
1136
  if start:
1137
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1138

    
1139
  return ops
1140

    
1141

    
1142
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1143
  """/2/instances/[instance_name]/reinstall resource.
1144

1145
  Implements an instance reinstall.
1146

1147
  """
1148
  POST_OPCODE = opcodes.OpInstanceReinstall
1149

    
1150
  def POST(self):
1151
    """Reinstall an instance.
1152

1153
    The URI takes os=name and nostartup=[0|1] optional
1154
    parameters. By default, the instance will be started
1155
    automatically.
1156

1157
    """
1158
    if self.request_body:
1159
      if self.queryargs:
1160
        raise http.HttpBadRequest("Can't combine query and body parameters")
1161

    
1162
      body = self.request_body
1163
    elif self.queryargs:
1164
      # Legacy interface, do not modify/extend
1165
      body = {
1166
        "os": self._checkStringVariable("os"),
1167
        "start": not self._checkIntVariable("nostartup"),
1168
        }
1169
    else:
1170
      body = {}
1171

    
1172
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1173

    
1174
    return self.SubmitJob(ops)
1175

    
1176

    
1177
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1178
  """/2/instances/[instance_name]/replace-disks resource.
1179

1180
  """
1181
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1182

    
1183
  def GetPostOpInput(self):
1184
    """Replaces disks on an instance.
1185

1186
    """
1187
    static = {
1188
      "instance_name": self.items[0],
1189
      }
1190

    
1191
    if self.request_body:
1192
      data = self.request_body
1193
    elif self.queryargs:
1194
      # Legacy interface, do not modify/extend
1195
      data = {
1196
        "remote_node": self._checkStringVariable("remote_node", default=None),
1197
        "mode": self._checkStringVariable("mode", default=None),
1198
        "disks": self._checkStringVariable("disks", default=None),
1199
        "iallocator": self._checkStringVariable("iallocator", default=None),
1200
        }
1201
    else:
1202
      data = {}
1203

    
1204
    # Parse disks
1205
    try:
1206
      raw_disks = data.pop("disks")
1207
    except KeyError:
1208
      pass
1209
    else:
1210
      if raw_disks:
1211
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1212
          data["disks"] = raw_disks
1213
        else:
1214
          # Backwards compatibility for strings of the format "1, 2, 3"
1215
          try:
1216
            data["disks"] = [int(part) for part in raw_disks.split(",")]
1217
          except (TypeError, ValueError), err:
1218
            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1219

    
1220
    return (data, static)
1221

    
1222

    
1223
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1224
  """/2/instances/[instance_name]/activate-disks resource.
1225

1226
  """
1227
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1228

    
1229
  def GetPutOpInput(self):
1230
    """Activate disks for an instance.
1231

1232
    The URI might contain ignore_size to ignore current recorded size.
1233

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

    
1240

    
1241
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1242
  """/2/instances/[instance_name]/deactivate-disks resource.
1243

1244
  """
1245
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1246

    
1247
  def GetPutOpInput(self):
1248
    """Deactivate disks for an instance.
1249

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

    
1255

    
1256
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1257
  """/2/instances/[instance_name]/recreate-disks resource.
1258

1259
  """
1260
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1261

    
1262
  def GetPostOpInput(self):
1263
    """Recreate disks for an instance.
1264

1265
    """
1266
    return ({}, {
1267
      "instance_name": self.items[0],
1268
      })
1269

    
1270

    
1271
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1272
  """/2/instances/[instance_name]/prepare-export resource.
1273

1274
  """
1275
  PUT_OPCODE = opcodes.OpBackupPrepare
1276

    
1277
  def GetPutOpInput(self):
1278
    """Prepares an export for an instance.
1279

1280
    """
1281
    return ({}, {
1282
      "instance_name": self.items[0],
1283
      "mode": self._checkStringVariable("mode"),
1284
      })
1285

    
1286

    
1287
class R_2_instances_name_export(baserlib.OpcodeResource):
1288
  """/2/instances/[instance_name]/export resource.
1289

1290
  """
1291
  PUT_OPCODE = opcodes.OpBackupExport
1292
  PUT_RENAME = {
1293
    "destination": "target_node",
1294
    }
1295

    
1296
  def GetPutOpInput(self):
1297
    """Exports an instance.
1298

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

    
1304

    
1305
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1306
  """/2/instances/[instance_name]/migrate resource.
1307

1308
  """
1309
  PUT_OPCODE = opcodes.OpInstanceMigrate
1310

    
1311
  def GetPutOpInput(self):
1312
    """Migrates an instance.
1313

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

    
1319

    
1320
class R_2_instances_name_failover(baserlib.OpcodeResource):
1321
  """/2/instances/[instance_name]/failover resource.
1322

1323
  """
1324
  PUT_OPCODE = opcodes.OpInstanceFailover
1325

    
1326
  def GetPutOpInput(self):
1327
    """Does a failover of an instance.
1328

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

    
1334

    
1335
class R_2_instances_name_rename(baserlib.OpcodeResource):
1336
  """/2/instances/[instance_name]/rename resource.
1337

1338
  """
1339
  PUT_OPCODE = opcodes.OpInstanceRename
1340

    
1341
  def GetPutOpInput(self):
1342
    """Changes the name of an instance.
1343

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

    
1349

    
1350
class R_2_instances_name_modify(baserlib.OpcodeResource):
1351
  """/2/instances/[instance_name]/modify resource.
1352

1353
  """
1354
  PUT_OPCODE = opcodes.OpInstanceSetParams
1355

    
1356
  def GetPutOpInput(self):
1357
    """Changes parameters of an instance.
1358

1359
    """
1360
    data = self.request_body.copy()
1361
    _ConvertUsbDevices(data)
1362

    
1363
    return (data, {
1364
      "instance_name": self.items[0],
1365
      })
1366

    
1367

    
1368
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1369
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1370

1371
  """
1372
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1373

    
1374
  def GetPostOpInput(self):
1375
    """Increases the size of an instance disk.
1376

1377
    """
1378
    return (self.request_body, {
1379
      "instance_name": self.items[0],
1380
      "disk": int(self.items[1]),
1381
      })
1382

    
1383

    
1384
class R_2_instances_name_console(baserlib.ResourceBase):
1385
  """/2/instances/[instance_name]/console resource.
1386

1387
  """
1388
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1389
  GET_OPCODE = opcodes.OpInstanceConsole
1390

    
1391
  def GET(self):
1392
    """Request information for connecting to instance's console.
1393

1394
    @return: Serialized instance console description, see
1395
             L{objects.InstanceConsole}
1396

1397
    """
1398
    client = self.GetClient()
1399

    
1400
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1401

    
1402
    if console is None:
1403
      raise http.HttpServiceUnavailable("Instance console unavailable")
1404

    
1405
    assert isinstance(console, dict)
1406
    return console
1407

    
1408

    
1409
def _GetQueryFields(args):
1410
  """Tries to extract C{fields} query parameter.
1411

1412
  @type args: dictionary
1413
  @rtype: list of string
1414
  @raise http.HttpBadRequest: When parameter can't be found
1415

1416
  """
1417
  try:
1418
    fields = args["fields"]
1419
  except KeyError:
1420
    raise http.HttpBadRequest("Missing 'fields' query argument")
1421

    
1422
  return _SplitQueryFields(fields[0])
1423

    
1424

    
1425
def _SplitQueryFields(fields):
1426
  """Splits fields as given for a query request.
1427

1428
  @type fields: string
1429
  @rtype: list of string
1430

1431
  """
1432
  return [i.strip() for i in fields.split(",")]
1433

    
1434

    
1435
class R_2_query(baserlib.ResourceBase):
1436
  """/2/query/[resource] resource.
1437

1438
  """
1439
  # Results might contain sensitive information
1440
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1441
  PUT_ACCESS = GET_ACCESS
1442
  GET_OPCODE = opcodes.OpQuery
1443
  PUT_OPCODE = opcodes.OpQuery
1444

    
1445
  def _Query(self, fields, qfilter):
1446
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1447

    
1448
  def GET(self):
1449
    """Returns resource information.
1450

1451
    @return: Query result, see L{objects.QueryResponse}
1452

1453
    """
1454
    return self._Query(_GetQueryFields(self.queryargs), None)
1455

    
1456
  def PUT(self):
1457
    """Submits job querying for resources.
1458

1459
    @return: Query result, see L{objects.QueryResponse}
1460

1461
    """
1462
    body = self.request_body
1463

    
1464
    baserlib.CheckType(body, dict, "Body contents")
1465

    
1466
    try:
1467
      fields = body["fields"]
1468
    except KeyError:
1469
      fields = _GetQueryFields(self.queryargs)
1470

    
1471
    qfilter = body.get("qfilter", None)
1472
    # TODO: remove this after 2.7
1473
    if qfilter is None:
1474
      qfilter = body.get("filter", None)
1475

    
1476
    return self._Query(fields, qfilter)
1477

    
1478

    
1479
class R_2_query_fields(baserlib.ResourceBase):
1480
  """/2/query/[resource]/fields resource.
1481

1482
  """
1483
  GET_OPCODE = opcodes.OpQueryFields
1484

    
1485
  def GET(self):
1486
    """Retrieves list of available fields for a resource.
1487

1488
    @return: List of serialized L{objects.QueryFieldDefinition}
1489

1490
    """
1491
    try:
1492
      raw_fields = self.queryargs["fields"]
1493
    except KeyError:
1494
      fields = None
1495
    else:
1496
      fields = _SplitQueryFields(raw_fields[0])
1497

    
1498
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1499

    
1500

    
1501
class _R_Tags(baserlib.OpcodeResource):
1502
  """Quasiclass for tagging resources.
1503

1504
  Manages tags. When inheriting this class you must define the
1505
  TAG_LEVEL for it.
1506

1507
  """
1508
  TAG_LEVEL = None
1509
  GET_OPCODE = opcodes.OpTagsGet
1510
  PUT_OPCODE = opcodes.OpTagsSet
1511
  DELETE_OPCODE = opcodes.OpTagsDel
1512

    
1513
  def __init__(self, items, queryargs, req, **kwargs):
1514
    """A tag resource constructor.
1515

1516
    We have to override the default to sort out cluster naming case.
1517

1518
    """
1519
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1520

    
1521
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1522
      self.name = None
1523
    else:
1524
      self.name = items[0]
1525

    
1526
  def GET(self):
1527
    """Returns a list of tags.
1528

1529
    Example: ["tag1", "tag2", "tag3"]
1530

1531
    """
1532
    kind = self.TAG_LEVEL
1533

    
1534
    if kind in (constants.TAG_INSTANCE,
1535
                constants.TAG_NODEGROUP,
1536
                constants.TAG_NODE,
1537
                constants.TAG_NETWORK):
1538
      if not self.name:
1539
        raise http.HttpBadRequest("Missing name on tag request")
1540

    
1541
      cl = self.GetClient(query=True)
1542
      tags = list(cl.QueryTags(kind, self.name))
1543

    
1544
    elif kind == constants.TAG_CLUSTER:
1545
      assert not self.name
1546
      # TODO: Use query API?
1547
      ssc = ssconf.SimpleStore()
1548
      tags = ssc.GetClusterTags()
1549

    
1550
    else:
1551
      raise http.HttpBadRequest("Unhandled tag type!")
1552

    
1553
    return list(tags)
1554

    
1555
  def GetPutOpInput(self):
1556
    """Add a set of tags.
1557

1558
    The request as a list of strings should be PUT to this URI. And
1559
    you'll have back a job id.
1560

1561
    """
1562
    return ({}, {
1563
      "kind": self.TAG_LEVEL,
1564
      "name": self.name,
1565
      "tags": self.queryargs.get("tag", []),
1566
      "dry_run": self.dryRun(),
1567
      })
1568

    
1569
  def GetDeleteOpInput(self):
1570
    """Delete a tag.
1571

1572
    In order to delete a set of tags, the DELETE
1573
    request should be addressed to URI like:
1574
    /tags?tag=[tag]&tag=[tag]
1575

1576
    """
1577
    # Re-use code
1578
    return self.GetPutOpInput()
1579

    
1580

    
1581
class R_2_instances_name_tags(_R_Tags):
1582
  """ /2/instances/[instance_name]/tags resource.
1583

1584
  Manages per-instance tags.
1585

1586
  """
1587
  TAG_LEVEL = constants.TAG_INSTANCE
1588

    
1589

    
1590
class R_2_nodes_name_tags(_R_Tags):
1591
  """ /2/nodes/[node_name]/tags resource.
1592

1593
  Manages per-node tags.
1594

1595
  """
1596
  TAG_LEVEL = constants.TAG_NODE
1597

    
1598

    
1599
class R_2_groups_name_tags(_R_Tags):
1600
  """ /2/groups/[group_name]/tags resource.
1601

1602
  Manages per-nodegroup tags.
1603

1604
  """
1605
  TAG_LEVEL = constants.TAG_NODEGROUP
1606

    
1607

    
1608
class R_2_networks_name_tags(_R_Tags):
1609
  """ /2/networks/[network_name]/tags resource.
1610

1611
  Manages per-network tags.
1612

1613
  """
1614
  TAG_LEVEL = constants.TAG_NETWORK
1615

    
1616

    
1617
class R_2_tags(_R_Tags):
1618
  """ /2/tags resource.
1619

1620
  Manages cluster tags.
1621

1622
  """
1623
  TAG_LEVEL = constants.TAG_CLUSTER