Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ d0b60d3a

History | View | Annotate | Download (40.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
  GET_ALIASES = {
224
    "volume_group_name": "vg_name",
225
    "drbd_usermode_helper": "drbd_helper",
226
    }
227

    
228
  def GET(self):
229
    """Returns cluster information.
230

231
    """
232
    client = self.GetClient(query=True)
233
    return client.QueryClusterInfo()
234

    
235

    
236
class R_2_features(baserlib.ResourceBase):
237
  """/2/features resource.
238

239
  """
240
  @staticmethod
241
  def GET():
242
    """Returns list of optional RAPI features implemented.
243

244
    """
245
    return list(ALL_FEATURES)
246

    
247

    
248
class R_2_os(baserlib.OpcodeResource):
249
  """/2/os resource.
250

251
  """
252
  GET_OPCODE = opcodes.OpOsDiagnose
253

    
254
  def GET(self):
255
    """Return a list of all OSes.
256

257
    Can return error 500 in case of a problem.
258

259
    Example: ["debian-etch"]
260

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

    
269
    if not isinstance(diagnose_data, list):
270
      raise http.HttpBadGateway(message="Can't get OS list")
271

    
272
    os_names = []
273
    for (name, variants) in diagnose_data:
274
      os_names.extend(cli.CalculateOSNames(name, variants))
275

    
276
    return os_names
277

    
278

    
279
class R_2_redist_config(baserlib.OpcodeResource):
280
  """/2/redistribute-config resource.
281

282
  """
283
  PUT_OPCODE = opcodes.OpClusterRedistConf
284

    
285

    
286
class R_2_cluster_modify(baserlib.OpcodeResource):
287
  """/2/modify resource.
288

289
  """
290
  PUT_OPCODE = opcodes.OpClusterSetParams
291

    
292

    
293
class R_2_jobs(baserlib.ResourceBase):
294
  """/2/jobs resource.
295

296
  """
297
  def GET(self):
298
    """Returns a dictionary of jobs.
299

300
    @return: a dictionary with jobs id and uri.
301

302
    """
303
    client = self.GetClient(query=True)
304

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

    
313

    
314
class R_2_jobs_id(baserlib.ResourceBase):
315
  """/2/jobs/[job_id] resource.
316

317
  """
318
  def GET(self):
319
    """Returns a job status.
320

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

330
    """
331
    job_id = self.items[0]
332
    result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
333
    if result is None:
334
      raise http.HttpNotFound()
335
    return baserlib.MapFields(J_FIELDS, result)
336

    
337
  def DELETE(self):
338
    """Cancel not-yet-started job.
339

340
    """
341
    job_id = self.items[0]
342
    result = self.GetClient().CancelJob(job_id)
343
    return result
344

    
345

    
346
class R_2_jobs_id_wait(baserlib.ResourceBase):
347
  """/2/jobs/[job_id]/wait resource.
348

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

    
354
  def GET(self):
355
    """Waits for job changes.
356

357
    """
358
    job_id = self.items[0]
359

    
360
    fields = self.getBodyParameter("fields")
361
    prev_job_info = self.getBodyParameter("previous_job_info", None)
362
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
363

    
364
    if not isinstance(fields, list):
365
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
366

    
367
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
368
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
369
                                " be a list")
370

    
371
    if not (prev_log_serial is None or
372
            isinstance(prev_log_serial, (int, long))):
373
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
374
                                " be a number")
375

    
376
    client = self.GetClient()
377
    result = client.WaitForJobChangeOnce(job_id, fields,
378
                                         prev_job_info, prev_log_serial,
379
                                         timeout=_WFJC_TIMEOUT)
380
    if not result:
381
      raise http.HttpNotFound()
382

    
383
    if result == constants.JOB_NOTCHANGED:
384
      # No changes
385
      return None
386

    
387
    (job_info, log_entries) = result
388

    
389
    return {
390
      "job_info": job_info,
391
      "log_entries": log_entries,
392
      }
393

    
394

    
395
class R_2_nodes(baserlib.OpcodeResource):
396
  """/2/nodes resource.
397

398
  """
399
  GET_OPCODE = opcodes.OpNodeQuery
400

    
401
  def GET(self):
402
    """Returns a list of all nodes.
403

404
    """
405
    client = self.GetClient(query=True)
406

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

    
416

    
417
class R_2_nodes_name(baserlib.OpcodeResource):
418
  """/2/nodes/[node_name] resource.
419

420
  """
421
  GET_OPCODE = opcodes.OpNodeQuery
422
  GET_ALIASES = {
423
    "sip": "secondary_ip",
424
    }
425

    
426
  def GET(self):
427
    """Send information about a node.
428

429
    """
430
    node_name = self.items[0]
431
    client = self.GetClient(query=True)
432

    
433
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
434
                                            names=[node_name], fields=N_FIELDS,
435
                                            use_locking=self.useLocking())
436

    
437
    return baserlib.MapFields(N_FIELDS, result[0])
438

    
439

    
440
class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
441
  """/2/nodes/[node_name]/powercycle resource.
442

443
  """
444
  POST_OPCODE = opcodes.OpNodePowercycle
445

    
446
  def GetPostOpInput(self):
447
    """Tries to powercycle a node.
448

449
    """
450
    return (self.request_body, {
451
      "node_name": self.items[0],
452
      "force": self.useForce(),
453
      })
454

    
455

    
456
class R_2_nodes_name_role(baserlib.OpcodeResource):
457
  """/2/nodes/[node_name]/role resource.
458

459
  """
460
  PUT_OPCODE = opcodes.OpNodeSetParams
461

    
462
  def GET(self):
463
    """Returns the current node role.
464

465
    @return: Node role
466

467
    """
468
    node_name = self.items[0]
469
    client = self.GetClient(query=True)
470
    result = client.QueryNodes(names=[node_name], fields=["role"],
471
                               use_locking=self.useLocking())
472

    
473
    return _NR_MAP[result[0][0]]
474

    
475
  def GetPutOpInput(self):
476
    """Sets the node role.
477

478
    """
479
    baserlib.CheckType(self.request_body, basestring, "Body contents")
480

    
481
    role = self.request_body
482

    
483
    if role == _NR_REGULAR:
484
      candidate = False
485
      offline = False
486
      drained = False
487

    
488
    elif role == _NR_MASTER_CANDIDATE:
489
      candidate = True
490
      offline = drained = None
491

    
492
    elif role == _NR_DRAINED:
493
      drained = True
494
      candidate = offline = None
495

    
496
    elif role == _NR_OFFLINE:
497
      offline = True
498
      candidate = drained = None
499

    
500
    else:
501
      raise http.HttpBadRequest("Can't set '%s' role" % role)
502

    
503
    assert len(self.items) == 1
504

    
505
    return ({}, {
506
      "node_name": self.items[0],
507
      "master_candidate": candidate,
508
      "offline": offline,
509
      "drained": drained,
510
      "force": self.useForce(),
511
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
512
      })
513

    
514

    
515
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
516
  """/2/nodes/[node_name]/evacuate resource.
517

518
  """
519
  POST_OPCODE = opcodes.OpNodeEvacuate
520

    
521
  def GetPostOpInput(self):
522
    """Evacuate all instances off a node.
523

524
    """
525
    return (self.request_body, {
526
      "node_name": self.items[0],
527
      "dry_run": self.dryRun(),
528
      })
529

    
530

    
531
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
532
  """/2/nodes/[node_name]/migrate resource.
533

534
  """
535
  POST_OPCODE = opcodes.OpNodeMigrate
536

    
537
  def GetPostOpInput(self):
538
    """Migrate all primary instances from a node.
539

540
    """
541
    if self.queryargs:
542
      # Support old-style requests
543
      if "live" in self.queryargs and "mode" in self.queryargs:
544
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
545
                                  " be passed")
546

    
547
      if "live" in self.queryargs:
548
        if self._checkIntVariable("live", default=1):
549
          mode = constants.HT_MIGRATION_LIVE
550
        else:
551
          mode = constants.HT_MIGRATION_NONLIVE
552
      else:
553
        mode = self._checkStringVariable("mode", default=None)
554

    
555
      data = {
556
        "mode": mode,
557
        }
558
    else:
559
      data = self.request_body
560

    
561
    return (data, {
562
      "node_name": self.items[0],
563
      })
564

    
565

    
566
class R_2_nodes_name_modify(baserlib.OpcodeResource):
567
  """/2/nodes/[node_name]/modify resource.
568

569
  """
570
  POST_OPCODE = opcodes.OpNodeSetParams
571

    
572
  def GetPostOpInput(self):
573
    """Changes parameters of a node.
574

575
    """
576
    assert len(self.items) == 1
577

    
578
    return (self.request_body, {
579
      "node_name": self.items[0],
580
      })
581

    
582

    
583
class R_2_nodes_name_storage(baserlib.OpcodeResource):
584
  """/2/nodes/[node_name]/storage resource.
585

586
  """
587
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
588
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
589
  GET_OPCODE = opcodes.OpNodeQueryStorage
590

    
591
  def GetGetOpInput(self):
592
    """List storage available on a node.
593

594
    """
595
    storage_type = self._checkStringVariable("storage_type", None)
596
    output_fields = self._checkStringVariable("output_fields", None)
597

    
598
    if not output_fields:
599
      raise http.HttpBadRequest("Missing the required 'output_fields'"
600
                                " parameter")
601

    
602
    return ({}, {
603
      "nodes": [self.items[0]],
604
      "storage_type": storage_type,
605
      "output_fields": output_fields.split(","),
606
      })
607

    
608

    
609
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
610
  """/2/nodes/[node_name]/storage/modify resource.
611

612
  """
613
  PUT_OPCODE = opcodes.OpNodeModifyStorage
614

    
615
  def GetPutOpInput(self):
616
    """Modifies a storage volume on a node.
617

618
    """
619
    storage_type = self._checkStringVariable("storage_type", None)
620
    name = self._checkStringVariable("name", None)
621

    
622
    if not name:
623
      raise http.HttpBadRequest("Missing the required 'name'"
624
                                " parameter")
625

    
626
    changes = {}
627

    
628
    if "allocatable" in self.queryargs:
629
      changes[constants.SF_ALLOCATABLE] = \
630
        bool(self._checkIntVariable("allocatable", default=1))
631

    
632
    return ({}, {
633
      "node_name": self.items[0],
634
      "storage_type": storage_type,
635
      "name": name,
636
      "changes": changes,
637
      })
638

    
639

    
640
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
641
  """/2/nodes/[node_name]/storage/repair resource.
642

643
  """
644
  PUT_OPCODE = opcodes.OpRepairNodeStorage
645

    
646
  def GetPutOpInput(self):
647
    """Repairs a storage volume on a node.
648

649
    """
650
    storage_type = self._checkStringVariable("storage_type", None)
651
    name = self._checkStringVariable("name", None)
652
    if not name:
653
      raise http.HttpBadRequest("Missing the required 'name'"
654
                                " parameter")
655

    
656
    return ({}, {
657
      "node_name": self.items[0],
658
      "storage_type": storage_type,
659
      "name": name,
660
      })
661

    
662

    
663
class R_2_networks(baserlib.OpcodeResource):
664
  """/2/networks resource.
665

666
  """
667
  GET_OPCODE = opcodes.OpNetworkQuery
668
  POST_OPCODE = opcodes.OpNetworkAdd
669
  POST_RENAME = {
670
    "name": "network_name",
671
    }
672

    
673
  def GetPostOpInput(self):
674
    """Create a network.
675

676
    """
677
    assert not self.items
678
    return (self.request_body, {
679
      "dry_run": self.dryRun(),
680
      })
681

    
682
  def GET(self):
683
    """Returns a list of all networks.
684

685
    """
686
    client = self.GetClient(query=True)
687

    
688
    if self.useBulk():
689
      bulkdata = client.QueryNetworks([], NET_FIELDS, False)
690
      return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
691
    else:
692
      data = client.QueryNetworks([], ["name"], False)
693
      networknames = [row[0] for row in data]
694
      return baserlib.BuildUriList(networknames, "/2/networks/%s",
695
                                   uri_fields=("name", "uri"))
696

    
697

    
698
class R_2_networks_name(baserlib.OpcodeResource):
699
  """/2/networks/[network_name] resource.
700

701
  """
702
  DELETE_OPCODE = opcodes.OpNetworkRemove
703

    
704
  def GET(self):
705
    """Send information about a network.
706

707
    """
708
    network_name = self.items[0]
709
    client = self.GetClient(query=True)
710

    
711
    result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
712
                                            names=[network_name],
713
                                            fields=NET_FIELDS,
714
                                            use_locking=self.useLocking())
715

    
716
    return baserlib.MapFields(NET_FIELDS, result[0])
717

    
718
  def GetDeleteOpInput(self):
719
    """Delete a network.
720

721
    """
722
    assert len(self.items) == 1
723
    return (self.request_body, {
724
      "network_name": self.items[0],
725
      "dry_run": self.dryRun(),
726
      })
727

    
728

    
729
class R_2_networks_name_connect(baserlib.OpcodeResource):
730
  """/2/networks/[network_name]/connect resource.
731

732
  """
733
  PUT_OPCODE = opcodes.OpNetworkConnect
734

    
735
  def GetPutOpInput(self):
736
    """Changes some parameters of node group.
737

738
    """
739
    assert self.items
740
    return (self.request_body, {
741
      "network_name": self.items[0],
742
      "dry_run": self.dryRun(),
743
      })
744

    
745

    
746
class R_2_networks_name_disconnect(baserlib.OpcodeResource):
747
  """/2/networks/[network_name]/disconnect resource.
748

749
  """
750
  PUT_OPCODE = opcodes.OpNetworkDisconnect
751

    
752
  def GetPutOpInput(self):
753
    """Changes some parameters of node group.
754

755
    """
756
    assert self.items
757
    return (self.request_body, {
758
      "network_name": self.items[0],
759
      "dry_run": self.dryRun(),
760
      })
761

    
762

    
763
class R_2_networks_name_modify(baserlib.OpcodeResource):
764
  """/2/networks/[network_name]/modify resource.
765

766
  """
767
  PUT_OPCODE = opcodes.OpNetworkSetParams
768

    
769
  def GetPutOpInput(self):
770
    """Changes some parameters of network.
771

772
    """
773
    assert self.items
774
    return (self.request_body, {
775
      "network_name": self.items[0],
776
      })
777

    
778

    
779
class R_2_groups(baserlib.OpcodeResource):
780
  """/2/groups resource.
781

782
  """
783
  GET_OPCODE = opcodes.OpGroupQuery
784
  POST_OPCODE = opcodes.OpGroupAdd
785
  POST_RENAME = {
786
    "name": "group_name",
787
    }
788

    
789
  def GetPostOpInput(self):
790
    """Create a node group.
791

792

793
    """
794
    assert not self.items
795
    return (self.request_body, {
796
      "dry_run": self.dryRun(),
797
      })
798

    
799
  def GET(self):
800
    """Returns a list of all node groups.
801

802
    """
803
    client = self.GetClient(query=True)
804

    
805
    if self.useBulk():
806
      bulkdata = client.QueryGroups([], G_FIELDS, False)
807
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
808
    else:
809
      data = client.QueryGroups([], ["name"], False)
810
      groupnames = [row[0] for row in data]
811
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
812
                                   uri_fields=("name", "uri"))
813

    
814

    
815
class R_2_groups_name(baserlib.OpcodeResource):
816
  """/2/groups/[group_name] resource.
817

818
  """
819
  DELETE_OPCODE = opcodes.OpGroupRemove
820

    
821
  def GET(self):
822
    """Send information about a node group.
823

824
    """
825
    group_name = self.items[0]
826
    client = self.GetClient(query=True)
827

    
828
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
829
                                            names=[group_name], fields=G_FIELDS,
830
                                            use_locking=self.useLocking())
831

    
832
    return baserlib.MapFields(G_FIELDS, result[0])
833

    
834
  def GetDeleteOpInput(self):
835
    """Delete a node group.
836

837
    """
838
    assert len(self.items) == 1
839
    return ({}, {
840
      "group_name": self.items[0],
841
      "dry_run": self.dryRun(),
842
      })
843

    
844

    
845
class R_2_groups_name_modify(baserlib.OpcodeResource):
846
  """/2/groups/[group_name]/modify resource.
847

848
  """
849
  PUT_OPCODE = opcodes.OpGroupSetParams
850
  PUT_RENAME = {
851
    "custom_ndparams": "ndparams",
852
    "custom_ipolicy": "ipolicy",
853
    "custom_diskparams": "diskparams",
854
    }
855

    
856
  def GetPutOpInput(self):
857
    """Changes some parameters of node group.
858

859
    """
860
    assert self.items
861
    return (self.request_body, {
862
      "group_name": self.items[0],
863
      })
864

    
865

    
866
class R_2_groups_name_rename(baserlib.OpcodeResource):
867
  """/2/groups/[group_name]/rename resource.
868

869
  """
870
  PUT_OPCODE = opcodes.OpGroupRename
871

    
872
  def GetPutOpInput(self):
873
    """Changes the name of a node group.
874

875
    """
876
    assert len(self.items) == 1
877
    return (self.request_body, {
878
      "group_name": self.items[0],
879
      "dry_run": self.dryRun(),
880
      })
881

    
882

    
883
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
884
  """/2/groups/[group_name]/assign-nodes resource.
885

886
  """
887
  PUT_OPCODE = opcodes.OpGroupAssignNodes
888

    
889
  def GetPutOpInput(self):
890
    """Assigns nodes to a group.
891

892
    """
893
    assert len(self.items) == 1
894
    return (self.request_body, {
895
      "group_name": self.items[0],
896
      "dry_run": self.dryRun(),
897
      "force": self.useForce(),
898
      })
899

    
900

    
901
def _ConvertUsbDevices(data):
902
  """Convert in place the usb_devices string to the proper format.
903

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

911
  """
912
  try:
913
    hvparams = data["hvparams"]
914
    usb_devices = hvparams[constants.HV_USB_DEVICES]
915
    hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
916
    data["hvparams"] = hvparams
917
  except KeyError:
918
    #No usb_devices, no modification required
919
    pass
920

    
921

    
922
class R_2_instances(baserlib.OpcodeResource):
923
  """/2/instances resource.
924

925
  """
926
  GET_OPCODE = opcodes.OpInstanceQuery
927
  POST_OPCODE = opcodes.OpInstanceCreate
928
  POST_RENAME = {
929
    "os": "os_type",
930
    "name": "instance_name",
931
    }
932

    
933
  def GET(self):
934
    """Returns a list of all available instances.
935

936
    """
937
    client = self.GetClient()
938

    
939
    use_locking = self.useLocking()
940
    if self.useBulk():
941
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
942
      return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
943
    else:
944
      instancesdata = client.QueryInstances([], ["name"], use_locking)
945
      instanceslist = [row[0] for row in instancesdata]
946
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
947
                                   uri_fields=("id", "uri"))
948

    
949
  def GetPostOpInput(self):
950
    """Create an instance.
951

952
    @return: a job id
953

954
    """
955
    baserlib.CheckType(self.request_body, dict, "Body contents")
956

    
957
    # Default to request data version 0
958
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
959

    
960
    if data_version == 0:
961
      raise http.HttpBadRequest("Instance creation request version 0 is no"
962
                                " longer supported")
963
    elif data_version != 1:
964
      raise http.HttpBadRequest("Unsupported request data version %s" %
965
                                data_version)
966

    
967
    data = self.request_body.copy()
968
    # Remove "__version__"
969
    data.pop(_REQ_DATA_VERSION, None)
970

    
971
    _ConvertUsbDevices(data)
972

    
973
    return (data, {
974
      "dry_run": self.dryRun(),
975
      })
976

    
977

    
978
class R_2_instances_multi_alloc(baserlib.OpcodeResource):
979
  """/2/instances-multi-alloc resource.
980

981
  """
982
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
983

    
984
  def GetPostOpInput(self):
985
    """Try to allocate multiple instances.
986

987
    @return: A dict with submitted jobs, allocatable instances and failed
988
             allocations
989

990
    """
991
    if "instances" not in self.request_body:
992
      raise http.HttpBadRequest("Request is missing required 'instances' field"
993
                                " in body")
994

    
995
    # Unlike most other RAPI calls, this one is composed of individual opcodes,
996
    # and we have to do the filling ourselves
997
    OPCODE_RENAME = {
998
      "os": "os_type",
999
      "name": "instance_name",
1000
    }
1001

    
1002
    body = objects.FillDict(self.request_body, {
1003
      "instances": [
1004
        baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {},
1005
                            rename=OPCODE_RENAME)
1006
        for inst in self.request_body["instances"]
1007
        ],
1008
      })
1009

    
1010
    return (body, {
1011
      "dry_run": self.dryRun(),
1012
      })
1013

    
1014

    
1015
class R_2_instances_name(baserlib.OpcodeResource):
1016
  """/2/instances/[instance_name] resource.
1017

1018
  """
1019
  GET_OPCODE = opcodes.OpInstanceQuery
1020
  DELETE_OPCODE = opcodes.OpInstanceRemove
1021

    
1022
  def GET(self):
1023
    """Send information about an instance.
1024

1025
    """
1026
    client = self.GetClient()
1027
    instance_name = self.items[0]
1028

    
1029
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1030
                                            names=[instance_name],
1031
                                            fields=I_FIELDS,
1032
                                            use_locking=self.useLocking())
1033

    
1034
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1035

    
1036
  def GetDeleteOpInput(self):
1037
    """Delete an instance.
1038

1039
    """
1040
    assert len(self.items) == 1
1041
    return (self.request_body, {
1042
      "instance_name": self.items[0],
1043
      "ignore_failures": False,
1044
      "dry_run": self.dryRun(),
1045
      })
1046

    
1047

    
1048
class R_2_instances_name_info(baserlib.OpcodeResource):
1049
  """/2/instances/[instance_name]/info resource.
1050

1051
  """
1052
  GET_OPCODE = opcodes.OpInstanceQueryData
1053

    
1054
  def GetGetOpInput(self):
1055
    """Request detailed instance information.
1056

1057
    """
1058
    assert len(self.items) == 1
1059
    return ({}, {
1060
      "instances": [self.items[0]],
1061
      "static": bool(self._checkIntVariable("static", default=0)),
1062
      })
1063

    
1064

    
1065
class R_2_instances_name_reboot(baserlib.OpcodeResource):
1066
  """/2/instances/[instance_name]/reboot resource.
1067

1068
  Implements an instance reboot.
1069

1070
  """
1071
  POST_OPCODE = opcodes.OpInstanceReboot
1072

    
1073
  def GetPostOpInput(self):
1074
    """Reboot an instance.
1075

1076
    The URI takes type=[hard|soft|full] and
1077
    ignore_secondaries=[False|True] parameters.
1078

1079
    """
1080
    return (self.request_body, {
1081
      "instance_name": self.items[0],
1082
      "reboot_type":
1083
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1084
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1085
      "dry_run": self.dryRun(),
1086
      })
1087

    
1088

    
1089
class R_2_instances_name_startup(baserlib.OpcodeResource):
1090
  """/2/instances/[instance_name]/startup resource.
1091

1092
  Implements an instance startup.
1093

1094
  """
1095
  PUT_OPCODE = opcodes.OpInstanceStartup
1096

    
1097
  def GetPutOpInput(self):
1098
    """Startup an instance.
1099

1100
    The URI takes force=[False|True] parameter to start the instance
1101
    if even if secondary disks are failing.
1102

1103
    """
1104
    return ({}, {
1105
      "instance_name": self.items[0],
1106
      "force": self.useForce(),
1107
      "dry_run": self.dryRun(),
1108
      "no_remember": bool(self._checkIntVariable("no_remember")),
1109
      })
1110

    
1111

    
1112
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1113
  """/2/instances/[instance_name]/shutdown resource.
1114

1115
  Implements an instance shutdown.
1116

1117
  """
1118
  PUT_OPCODE = opcodes.OpInstanceShutdown
1119

    
1120
  def GetPutOpInput(self):
1121
    """Shutdown an instance.
1122

1123
    """
1124
    return (self.request_body, {
1125
      "instance_name": self.items[0],
1126
      "no_remember": bool(self._checkIntVariable("no_remember")),
1127
      "dry_run": self.dryRun(),
1128
      })
1129

    
1130

    
1131
def _ParseInstanceReinstallRequest(name, data):
1132
  """Parses a request for reinstalling an instance.
1133

1134
  """
1135
  if not isinstance(data, dict):
1136
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1137

    
1138
  ostype = baserlib.CheckParameter(data, "os", default=None)
1139
  start = baserlib.CheckParameter(data, "start", exptype=bool,
1140
                                  default=True)
1141
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
1142

    
1143
  ops = [
1144
    opcodes.OpInstanceShutdown(instance_name=name),
1145
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1146
                                osparams=osparams),
1147
    ]
1148

    
1149
  if start:
1150
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1151

    
1152
  return ops
1153

    
1154

    
1155
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1156
  """/2/instances/[instance_name]/reinstall resource.
1157

1158
  Implements an instance reinstall.
1159

1160
  """
1161
  POST_OPCODE = opcodes.OpInstanceReinstall
1162

    
1163
  def POST(self):
1164
    """Reinstall an instance.
1165

1166
    The URI takes os=name and nostartup=[0|1] optional
1167
    parameters. By default, the instance will be started
1168
    automatically.
1169

1170
    """
1171
    if self.request_body:
1172
      if self.queryargs:
1173
        raise http.HttpBadRequest("Can't combine query and body parameters")
1174

    
1175
      body = self.request_body
1176
    elif self.queryargs:
1177
      # Legacy interface, do not modify/extend
1178
      body = {
1179
        "os": self._checkStringVariable("os"),
1180
        "start": not self._checkIntVariable("nostartup"),
1181
        }
1182
    else:
1183
      body = {}
1184

    
1185
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1186

    
1187
    return self.SubmitJob(ops)
1188

    
1189

    
1190
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1191
  """/2/instances/[instance_name]/replace-disks resource.
1192

1193
  """
1194
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1195

    
1196
  def GetPostOpInput(self):
1197
    """Replaces disks on an instance.
1198

1199
    """
1200
    static = {
1201
      "instance_name": self.items[0],
1202
      }
1203

    
1204
    if self.request_body:
1205
      data = self.request_body
1206
    elif self.queryargs:
1207
      # Legacy interface, do not modify/extend
1208
      data = {
1209
        "remote_node": self._checkStringVariable("remote_node", default=None),
1210
        "mode": self._checkStringVariable("mode", default=None),
1211
        "disks": self._checkStringVariable("disks", default=None),
1212
        "iallocator": self._checkStringVariable("iallocator", default=None),
1213
        }
1214
    else:
1215
      data = {}
1216

    
1217
    # Parse disks
1218
    try:
1219
      raw_disks = data.pop("disks")
1220
    except KeyError:
1221
      pass
1222
    else:
1223
      if raw_disks:
1224
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1225
          data["disks"] = raw_disks
1226
        else:
1227
          # Backwards compatibility for strings of the format "1, 2, 3"
1228
          try:
1229
            data["disks"] = [int(part) for part in raw_disks.split(",")]
1230
          except (TypeError, ValueError), err:
1231
            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1232

    
1233
    return (data, static)
1234

    
1235

    
1236
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1237
  """/2/instances/[instance_name]/activate-disks resource.
1238

1239
  """
1240
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1241

    
1242
  def GetPutOpInput(self):
1243
    """Activate disks for an instance.
1244

1245
    The URI might contain ignore_size to ignore current recorded size.
1246

1247
    """
1248
    return ({}, {
1249
      "instance_name": self.items[0],
1250
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
1251
      })
1252

    
1253

    
1254
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1255
  """/2/instances/[instance_name]/deactivate-disks resource.
1256

1257
  """
1258
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1259

    
1260
  def GetPutOpInput(self):
1261
    """Deactivate disks for an instance.
1262

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

    
1268

    
1269
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1270
  """/2/instances/[instance_name]/recreate-disks resource.
1271

1272
  """
1273
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1274

    
1275
  def GetPostOpInput(self):
1276
    """Recreate disks for an instance.
1277

1278
    """
1279
    return ({}, {
1280
      "instance_name": self.items[0],
1281
      })
1282

    
1283

    
1284
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1285
  """/2/instances/[instance_name]/prepare-export resource.
1286

1287
  """
1288
  PUT_OPCODE = opcodes.OpBackupPrepare
1289

    
1290
  def GetPutOpInput(self):
1291
    """Prepares an export for an instance.
1292

1293
    """
1294
    return ({}, {
1295
      "instance_name": self.items[0],
1296
      "mode": self._checkStringVariable("mode"),
1297
      })
1298

    
1299

    
1300
class R_2_instances_name_export(baserlib.OpcodeResource):
1301
  """/2/instances/[instance_name]/export resource.
1302

1303
  """
1304
  PUT_OPCODE = opcodes.OpBackupExport
1305
  PUT_RENAME = {
1306
    "destination": "target_node",
1307
    }
1308

    
1309
  def GetPutOpInput(self):
1310
    """Exports an instance.
1311

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

    
1317

    
1318
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1319
  """/2/instances/[instance_name]/migrate resource.
1320

1321
  """
1322
  PUT_OPCODE = opcodes.OpInstanceMigrate
1323

    
1324
  def GetPutOpInput(self):
1325
    """Migrates an instance.
1326

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

    
1332

    
1333
class R_2_instances_name_failover(baserlib.OpcodeResource):
1334
  """/2/instances/[instance_name]/failover resource.
1335

1336
  """
1337
  PUT_OPCODE = opcodes.OpInstanceFailover
1338

    
1339
  def GetPutOpInput(self):
1340
    """Does a failover of an instance.
1341

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

    
1347

    
1348
class R_2_instances_name_rename(baserlib.OpcodeResource):
1349
  """/2/instances/[instance_name]/rename resource.
1350

1351
  """
1352
  PUT_OPCODE = opcodes.OpInstanceRename
1353

    
1354
  def GetPutOpInput(self):
1355
    """Changes the name of an instance.
1356

1357
    """
1358
    return (self.request_body, {
1359
      "instance_name": self.items[0],
1360
      })
1361

    
1362

    
1363
class R_2_instances_name_modify(baserlib.OpcodeResource):
1364
  """/2/instances/[instance_name]/modify resource.
1365

1366
  """
1367
  PUT_OPCODE = opcodes.OpInstanceSetParams
1368
  PUT_RENAME = {
1369
    "custom_beparams": "beparams",
1370
    "custom_hvparams": "hvparams",
1371
    }
1372

    
1373
  def GetPutOpInput(self):
1374
    """Changes parameters of an instance.
1375

1376
    """
1377
    data = self.request_body.copy()
1378
    _ConvertUsbDevices(data)
1379

    
1380
    return (data, {
1381
      "instance_name": self.items[0],
1382
      })
1383

    
1384

    
1385
class R_2_instances_name_snapshot(baserlib.OpcodeResource):
1386
  """/2/instances/[instance_name]/snapshot resource.
1387

1388
  Implements an instance snapshot.
1389

1390
  """
1391
  PUT_OPCODE = opcodes.OpInstanceSnapshot
1392

    
1393
  def GetPutOpInput(self):
1394
    """Snapshot disks of an instance.
1395

1396
    """
1397
    return (self.request_body, {
1398
        "instance_name": self.items[0],
1399
        "dry_run": self.dryRun(),
1400
      })
1401

    
1402

    
1403
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1404
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1405

1406
  """
1407
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1408

    
1409
  def GetPostOpInput(self):
1410
    """Increases the size of an instance disk.
1411

1412
    """
1413
    return (self.request_body, {
1414
      "instance_name": self.items[0],
1415
      "disk": int(self.items[1]),
1416
      })
1417

    
1418

    
1419
class R_2_instances_name_console(baserlib.ResourceBase):
1420
  """/2/instances/[instance_name]/console resource.
1421

1422
  """
1423
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1424
  GET_OPCODE = opcodes.OpInstanceConsole
1425

    
1426
  def GET(self):
1427
    """Request information for connecting to instance's console.
1428

1429
    @return: Serialized instance console description, see
1430
             L{objects.InstanceConsole}
1431

1432
    """
1433
    client = self.GetClient()
1434

    
1435
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1436

    
1437
    if console is None:
1438
      raise http.HttpServiceUnavailable("Instance console unavailable")
1439

    
1440
    assert isinstance(console, dict)
1441
    return console
1442

    
1443

    
1444
def _GetQueryFields(args):
1445
  """Tries to extract C{fields} query parameter.
1446

1447
  @type args: dictionary
1448
  @rtype: list of string
1449
  @raise http.HttpBadRequest: When parameter can't be found
1450

1451
  """
1452
  try:
1453
    fields = args["fields"]
1454
  except KeyError:
1455
    raise http.HttpBadRequest("Missing 'fields' query argument")
1456

    
1457
  return _SplitQueryFields(fields[0])
1458

    
1459

    
1460
def _SplitQueryFields(fields):
1461
  """Splits fields as given for a query request.
1462

1463
  @type fields: string
1464
  @rtype: list of string
1465

1466
  """
1467
  return [i.strip() for i in fields.split(",")]
1468

    
1469

    
1470
class R_2_query(baserlib.ResourceBase):
1471
  """/2/query/[resource] resource.
1472

1473
  """
1474
  # Results might contain sensitive information
1475
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1476
  PUT_ACCESS = GET_ACCESS
1477
  GET_OPCODE = opcodes.OpQuery
1478
  PUT_OPCODE = opcodes.OpQuery
1479

    
1480
  def _Query(self, fields, qfilter):
1481
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1482

    
1483
  def GET(self):
1484
    """Returns resource information.
1485

1486
    @return: Query result, see L{objects.QueryResponse}
1487

1488
    """
1489
    return self._Query(_GetQueryFields(self.queryargs), None)
1490

    
1491
  def PUT(self):
1492
    """Submits job querying for resources.
1493

1494
    @return: Query result, see L{objects.QueryResponse}
1495

1496
    """
1497
    body = self.request_body
1498

    
1499
    baserlib.CheckType(body, dict, "Body contents")
1500

    
1501
    try:
1502
      fields = body["fields"]
1503
    except KeyError:
1504
      fields = _GetQueryFields(self.queryargs)
1505

    
1506
    qfilter = body.get("qfilter", None)
1507
    # TODO: remove this after 2.7
1508
    if qfilter is None:
1509
      qfilter = body.get("filter", None)
1510

    
1511
    return self._Query(fields, qfilter)
1512

    
1513

    
1514
class R_2_query_fields(baserlib.ResourceBase):
1515
  """/2/query/[resource]/fields resource.
1516

1517
  """
1518
  GET_OPCODE = opcodes.OpQueryFields
1519

    
1520
  def GET(self):
1521
    """Retrieves list of available fields for a resource.
1522

1523
    @return: List of serialized L{objects.QueryFieldDefinition}
1524

1525
    """
1526
    try:
1527
      raw_fields = self.queryargs["fields"]
1528
    except KeyError:
1529
      fields = None
1530
    else:
1531
      fields = _SplitQueryFields(raw_fields[0])
1532

    
1533
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1534

    
1535

    
1536
class _R_Tags(baserlib.OpcodeResource):
1537
  """Quasiclass for tagging resources.
1538

1539
  Manages tags. When inheriting this class you must define the
1540
  TAG_LEVEL for it.
1541

1542
  """
1543
  TAG_LEVEL = None
1544
  GET_OPCODE = opcodes.OpTagsGet
1545
  PUT_OPCODE = opcodes.OpTagsSet
1546
  DELETE_OPCODE = opcodes.OpTagsDel
1547

    
1548
  def __init__(self, items, queryargs, req, **kwargs):
1549
    """A tag resource constructor.
1550

1551
    We have to override the default to sort out cluster naming case.
1552

1553
    """
1554
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1555

    
1556
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1557
      self.name = None
1558
    else:
1559
      self.name = items[0]
1560

    
1561
  def GET(self):
1562
    """Returns a list of tags.
1563

1564
    Example: ["tag1", "tag2", "tag3"]
1565

1566
    """
1567
    kind = self.TAG_LEVEL
1568

    
1569
    if kind in (constants.TAG_INSTANCE,
1570
                constants.TAG_NODEGROUP,
1571
                constants.TAG_NODE,
1572
                constants.TAG_NETWORK):
1573
      if not self.name:
1574
        raise http.HttpBadRequest("Missing name on tag request")
1575

    
1576
      cl = self.GetClient(query=True)
1577
      tags = list(cl.QueryTags(kind, self.name))
1578

    
1579
    elif kind == constants.TAG_CLUSTER:
1580
      assert not self.name
1581
      # TODO: Use query API?
1582
      ssc = ssconf.SimpleStore()
1583
      tags = ssc.GetClusterTags()
1584

    
1585
    else:
1586
      raise http.HttpBadRequest("Unhandled tag type!")
1587

    
1588
    return list(tags)
1589

    
1590
  def GetPutOpInput(self):
1591
    """Add a set of tags.
1592

1593
    The request as a list of strings should be PUT to this URI. And
1594
    you'll have back a job id.
1595

1596
    """
1597
    return ({}, {
1598
      "kind": self.TAG_LEVEL,
1599
      "name": self.name,
1600
      "tags": self.queryargs.get("tag", []),
1601
      "dry_run": self.dryRun(),
1602
      })
1603

    
1604
  def GetDeleteOpInput(self):
1605
    """Delete a tag.
1606

1607
    In order to delete a set of tags, the DELETE
1608
    request should be addressed to URI like:
1609
    /tags?tag=[tag]&tag=[tag]
1610

1611
    """
1612
    # Re-use code
1613
    return self.GetPutOpInput()
1614

    
1615

    
1616
class R_2_instances_name_tags(_R_Tags):
1617
  """ /2/instances/[instance_name]/tags resource.
1618

1619
  Manages per-instance tags.
1620

1621
  """
1622
  TAG_LEVEL = constants.TAG_INSTANCE
1623

    
1624

    
1625
class R_2_nodes_name_tags(_R_Tags):
1626
  """ /2/nodes/[node_name]/tags resource.
1627

1628
  Manages per-node tags.
1629

1630
  """
1631
  TAG_LEVEL = constants.TAG_NODE
1632

    
1633

    
1634
class R_2_groups_name_tags(_R_Tags):
1635
  """ /2/groups/[group_name]/tags resource.
1636

1637
  Manages per-nodegroup tags.
1638

1639
  """
1640
  TAG_LEVEL = constants.TAG_NODEGROUP
1641

    
1642

    
1643
class R_2_networks_name_tags(_R_Tags):
1644
  """ /2/networks/[network_name]/tags resource.
1645

1646
  Manages per-network tags.
1647

1648
  """
1649
  TAG_LEVEL = constants.TAG_NETWORK
1650

    
1651

    
1652
class R_2_tags(_R_Tags):
1653
  """ /2/tags resource.
1654

1655
  Manages cluster tags.
1656

1657
  """
1658
  TAG_LEVEL = constants.TAG_CLUSTER