Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ eb08e09d

History | View | Annotate | Download (33.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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-msg=C0103
55

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

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

    
68

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

    
81
N_FIELDS = ["name", "offline", "master_candidate", "drained",
82
            "dtotal", "dfree",
83
            "mtotal", "mnode", "mfree",
84
            "pinst_cnt", "sinst_cnt",
85
            "ctotal", "cnodes", "csockets",
86
            "pip", "sip", "role",
87
            "pinst_list", "sinst_list",
88
            "master_capable", "vm_capable",
89
            "group.uuid",
90
            ] + _COMMON_FIELDS
91

    
92
G_FIELDS = [
93
  "alloc_policy",
94
  "name",
95
  "node_cnt",
96
  "node_list",
97
  ] + _COMMON_FIELDS
98

    
99
J_FIELDS_BULK = [
100
  "id", "ops", "status", "summary",
101
  "opstatus",
102
  "received_ts", "start_ts", "end_ts",
103
  ]
104

    
105
J_FIELDS = J_FIELDS_BULK + [
106
  "oplog",
107
  "opresult",
108
  ]
109

    
110
_NR_DRAINED = "drained"
111
_NR_MASTER_CANDIDATE = "master-candidate"
112
_NR_MASTER = "master"
113
_NR_OFFLINE = "offline"
114
_NR_REGULAR = "regular"
115

    
116
_NR_MAP = {
117
  constants.NR_MASTER: _NR_MASTER,
118
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
119
  constants.NR_DRAINED: _NR_DRAINED,
120
  constants.NR_OFFLINE: _NR_OFFLINE,
121
  constants.NR_REGULAR: _NR_REGULAR,
122
  }
123

    
124
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
125

    
126
# Request data version field
127
_REQ_DATA_VERSION = "__version__"
128

    
129
# Feature string for instance creation request data version 1
130
_INST_CREATE_REQV1 = "instance-create-reqv1"
131

    
132
# Feature string for instance reinstall request version 1
133
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
134

    
135
# Feature string for node migration version 1
136
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
137

    
138
# Feature string for node evacuation with LU-generated jobs
139
_NODE_EVAC_RES1 = "node-evac-res1"
140

    
141
ALL_FEATURES = frozenset([
142
  _INST_CREATE_REQV1,
143
  _INST_REINSTALL_REQV1,
144
  _NODE_MIGRATE_REQV1,
145
  _NODE_EVAC_RES1,
146
  ])
147

    
148
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
149
_WFJC_TIMEOUT = 10
150

    
151

    
152
class R_root(baserlib.ResourceBase):
153
  """/ resource.
154

155
  """
156
  @staticmethod
157
  def GET():
158
    """Supported for legacy reasons.
159

160
    """
161
    return None
162

    
163

    
164
class R_version(baserlib.ResourceBase):
165
  """/version resource.
166

167
  This resource should be used to determine the remote API version and
168
  to adapt clients accordingly.
169

170
  """
171
  @staticmethod
172
  def GET():
173
    """Returns the remote API version.
174

175
    """
176
    return constants.RAPI_VERSION
177

    
178

    
179
class R_2_info(baserlib.ResourceBase):
180
  """/2/info resource.
181

182
  """
183
  def GET(self):
184
    """Returns cluster information.
185

186
    """
187
    client = self.GetClient()
188
    return client.QueryClusterInfo()
189

    
190

    
191
class R_2_features(baserlib.ResourceBase):
192
  """/2/features resource.
193

194
  """
195
  @staticmethod
196
  def GET():
197
    """Returns list of optional RAPI features implemented.
198

199
    """
200
    return list(ALL_FEATURES)
201

    
202

    
203
class R_2_os(baserlib.ResourceBase):
204
  """/2/os resource.
205

206
  """
207
  def GET(self):
208
    """Return a list of all OSes.
209

210
    Can return error 500 in case of a problem.
211

212
    Example: ["debian-etch"]
213

214
    """
215
    cl = self.GetClient()
216
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
217
    job_id = self.SubmitJob([op], cl=cl)
218
    # we use custom feedback function, instead of print we log the status
219
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
220
    diagnose_data = result[0]
221

    
222
    if not isinstance(diagnose_data, list):
223
      raise http.HttpBadGateway(message="Can't get OS list")
224

    
225
    os_names = []
226
    for (name, variants) in diagnose_data:
227
      os_names.extend(cli.CalculateOSNames(name, variants))
228

    
229
    return os_names
230

    
231

    
232
class R_2_redist_config(baserlib.OpcodeResource):
233
  """/2/redistribute-config resource.
234

235
  """
236
  PUT_OPCODE = opcodes.OpClusterRedistConf
237

    
238

    
239
class R_2_cluster_modify(baserlib.OpcodeResource):
240
  """/2/modify resource.
241

242
  """
243
  PUT_OPCODE = opcodes.OpClusterSetParams
244

    
245

    
246
class R_2_jobs(baserlib.ResourceBase):
247
  """/2/jobs resource.
248

249
  """
250
  def GET(self):
251
    """Returns a dictionary of jobs.
252

253
    @return: a dictionary with jobs id and uri.
254

255
    """
256
    client = self.GetClient()
257

    
258
    if self.useBulk():
259
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
260
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
261
    else:
262
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
263
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
264
                                   uri_fields=("id", "uri"))
265

    
266

    
267
class R_2_jobs_id(baserlib.ResourceBase):
268
  """/2/jobs/[job_id] resource.
269

270
  """
271
  def GET(self):
272
    """Returns a job status.
273

274
    @return: a dictionary with job parameters.
275
        The result includes:
276
            - id: job ID as a number
277
            - status: current job status as a string
278
            - ops: involved OpCodes as a list of dictionaries for each
279
              opcodes in the job
280
            - opstatus: OpCodes status as a list
281
            - opresult: OpCodes results as a list of lists
282

283
    """
284
    job_id = self.items[0]
285
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
286
    if result is None:
287
      raise http.HttpNotFound()
288
    return baserlib.MapFields(J_FIELDS, result)
289

    
290
  def DELETE(self):
291
    """Cancel not-yet-started job.
292

293
    """
294
    job_id = self.items[0]
295
    result = self.GetClient().CancelJob(job_id)
296
    return result
297

    
298

    
299
class R_2_jobs_id_wait(baserlib.ResourceBase):
300
  """/2/jobs/[job_id]/wait resource.
301

302
  """
303
  # WaitForJobChange provides access to sensitive information and blocks
304
  # machine resources (it's a blocking RAPI call), hence restricting access.
305
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
306

    
307
  def GET(self):
308
    """Waits for job changes.
309

310
    """
311
    job_id = self.items[0]
312

    
313
    fields = self.getBodyParameter("fields")
314
    prev_job_info = self.getBodyParameter("previous_job_info", None)
315
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
316

    
317
    if not isinstance(fields, list):
318
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
319

    
320
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
321
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
322
                                " be a list")
323

    
324
    if not (prev_log_serial is None or
325
            isinstance(prev_log_serial, (int, long))):
326
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
327
                                " be a number")
328

    
329
    client = self.GetClient()
330
    result = client.WaitForJobChangeOnce(job_id, fields,
331
                                         prev_job_info, prev_log_serial,
332
                                         timeout=_WFJC_TIMEOUT)
333
    if not result:
334
      raise http.HttpNotFound()
335

    
336
    if result == constants.JOB_NOTCHANGED:
337
      # No changes
338
      return None
339

    
340
    (job_info, log_entries) = result
341

    
342
    return {
343
      "job_info": job_info,
344
      "log_entries": log_entries,
345
      }
346

    
347

    
348
class R_2_nodes(baserlib.ResourceBase):
349
  """/2/nodes resource.
350

351
  """
352
  def GET(self):
353
    """Returns a list of all nodes.
354

355
    """
356
    client = self.GetClient()
357

    
358
    if self.useBulk():
359
      bulkdata = client.QueryNodes([], N_FIELDS, False)
360
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
361
    else:
362
      nodesdata = client.QueryNodes([], ["name"], False)
363
      nodeslist = [row[0] for row in nodesdata]
364
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
365
                                   uri_fields=("id", "uri"))
366

    
367

    
368
class R_2_nodes_name(baserlib.ResourceBase):
369
  """/2/nodes/[node_name] resource.
370

371
  """
372
  def GET(self):
373
    """Send information about a node.
374

375
    """
376
    node_name = self.items[0]
377
    client = self.GetClient()
378

    
379
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
380
                                            names=[node_name], fields=N_FIELDS,
381
                                            use_locking=self.useLocking())
382

    
383
    return baserlib.MapFields(N_FIELDS, result[0])
384

    
385

    
386
class R_2_nodes_name_role(baserlib.OpcodeResource):
387
  """/2/nodes/[node_name]/role resource.
388

389
  """
390
  PUT_OPCODE = opcodes.OpNodeSetParams
391

    
392
  def GET(self):
393
    """Returns the current node role.
394

395
    @return: Node role
396

397
    """
398
    node_name = self.items[0]
399
    client = self.GetClient()
400
    result = client.QueryNodes(names=[node_name], fields=["role"],
401
                               use_locking=self.useLocking())
402

    
403
    return _NR_MAP[result[0][0]]
404

    
405
  def GetPutOpInput(self):
406
    """Sets the node role.
407

408
    """
409
    baserlib.CheckType(self.request_body, basestring, "Body contents")
410

    
411
    role = self.request_body
412

    
413
    if role == _NR_REGULAR:
414
      candidate = False
415
      offline = False
416
      drained = False
417

    
418
    elif role == _NR_MASTER_CANDIDATE:
419
      candidate = True
420
      offline = drained = None
421

    
422
    elif role == _NR_DRAINED:
423
      drained = True
424
      candidate = offline = None
425

    
426
    elif role == _NR_OFFLINE:
427
      offline = True
428
      candidate = drained = None
429

    
430
    else:
431
      raise http.HttpBadRequest("Can't set '%s' role" % role)
432

    
433
    assert len(self.items) == 1
434

    
435
    return ({}, {
436
      "node_name": self.items[0],
437
      "master_candidate": candidate,
438
      "offline": offline,
439
      "drained": drained,
440
      "force": self.useForce(),
441
      })
442

    
443

    
444
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
445
  """/2/nodes/[node_name]/evacuate resource.
446

447
  """
448
  POST_OPCODE = opcodes.OpNodeEvacuate
449

    
450
  def GetPostOpInput(self):
451
    """Evacuate all instances off a node.
452

453
    """
454
    return (self.request_body, {
455
      "node_name": self.items[0],
456
      "dry_run": self.dryRun(),
457
      })
458

    
459

    
460
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
461
  """/2/nodes/[node_name]/migrate resource.
462

463
  """
464
  POST_OPCODE = opcodes.OpNodeMigrate
465

    
466
  def GetPostOpInput(self):
467
    """Migrate all primary instances from a node.
468

469
    """
470
    if self.queryargs:
471
      # Support old-style requests
472
      if "live" in self.queryargs and "mode" in self.queryargs:
473
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
474
                                  " be passed")
475

    
476
      if "live" in self.queryargs:
477
        if self._checkIntVariable("live", default=1):
478
          mode = constants.HT_MIGRATION_LIVE
479
        else:
480
          mode = constants.HT_MIGRATION_NONLIVE
481
      else:
482
        mode = self._checkStringVariable("mode", default=None)
483

    
484
      data = {
485
        "mode": mode,
486
        }
487
    else:
488
      data = self.request_body
489

    
490
    return (data, {
491
      "node_name": self.items[0],
492
      })
493

    
494

    
495
class R_2_nodes_name_storage(baserlib.OpcodeResource):
496
  """/2/nodes/[node_name]/storage resource.
497

498
  """
499
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
500
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
501
  GET_OPCODE = opcodes.OpNodeQueryStorage
502

    
503
  def GetGetOpInput(self):
504
    """List storage available on a node.
505

506
    """
507
    storage_type = self._checkStringVariable("storage_type", None)
508
    output_fields = self._checkStringVariable("output_fields", None)
509

    
510
    if not output_fields:
511
      raise http.HttpBadRequest("Missing the required 'output_fields'"
512
                                " parameter")
513

    
514
    return ({}, {
515
      "nodes": [self.items[0]],
516
      "storage_type": storage_type,
517
      "output_fields": output_fields.split(","),
518
      })
519

    
520

    
521
class R_2_nodes_name_storage_modify(baserlib.ResourceBase):
522
  """/2/nodes/[node_name]/storage/modify resource.
523

524
  """
525
  def PUT(self):
526
    node_name = self.items[0]
527

    
528
    storage_type = self._checkStringVariable("storage_type", None)
529
    if not storage_type:
530
      raise http.HttpBadRequest("Missing the required 'storage_type'"
531
                                " parameter")
532

    
533
    name = self._checkStringVariable("name", None)
534
    if not name:
535
      raise http.HttpBadRequest("Missing the required 'name'"
536
                                " parameter")
537

    
538
    changes = {}
539

    
540
    if "allocatable" in self.queryargs:
541
      changes[constants.SF_ALLOCATABLE] = \
542
        bool(self._checkIntVariable("allocatable", default=1))
543

    
544
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
545
                                     storage_type=storage_type,
546
                                     name=name,
547
                                     changes=changes)
548
    return self.SubmitJob([op])
549

    
550

    
551
class R_2_nodes_name_storage_repair(baserlib.ResourceBase):
552
  """/2/nodes/[node_name]/storage/repair resource.
553

554
  """
555
  def PUT(self):
556
    node_name = self.items[0]
557

    
558
    storage_type = self._checkStringVariable("storage_type", None)
559
    if not storage_type:
560
      raise http.HttpBadRequest("Missing the required 'storage_type'"
561
                                " parameter")
562

    
563
    name = self._checkStringVariable("name", None)
564
    if not name:
565
      raise http.HttpBadRequest("Missing the required 'name'"
566
                                " parameter")
567

    
568
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
569
                                     storage_type=storage_type,
570
                                     name=name)
571
    return self.SubmitJob([op])
572

    
573

    
574
class R_2_groups(baserlib.OpcodeResource):
575
  """/2/groups resource.
576

577
  """
578
  POST_OPCODE = opcodes.OpGroupAdd
579
  POST_RENAME = {
580
    "name": "group_name",
581
    }
582

    
583
  def GetPostOpInput(self):
584
    """Create a node group.
585

586
    """
587
    assert not self.items
588
    return (self.request_body, {
589
      "dry_run": self.dryRun(),
590
      })
591

    
592
  def GET(self):
593
    """Returns a list of all node groups.
594

595
    """
596
    client = self.GetClient()
597

    
598
    if self.useBulk():
599
      bulkdata = client.QueryGroups([], G_FIELDS, False)
600
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
601
    else:
602
      data = client.QueryGroups([], ["name"], False)
603
      groupnames = [row[0] for row in data]
604
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
605
                                   uri_fields=("name", "uri"))
606

    
607

    
608
class R_2_groups_name(baserlib.OpcodeResource):
609
  """/2/groups/[group_name] resource.
610

611
  """
612
  DELETE_OPCODE = opcodes.OpGroupRemove
613

    
614
  def GET(self):
615
    """Send information about a node group.
616

617
    """
618
    group_name = self.items[0]
619
    client = self.GetClient()
620

    
621
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
622
                                            names=[group_name], fields=G_FIELDS,
623
                                            use_locking=self.useLocking())
624

    
625
    return baserlib.MapFields(G_FIELDS, result[0])
626

    
627
  def GetDeleteOpInput(self):
628
    """Delete a node group.
629

630
    """
631
    assert len(self.items) == 1
632
    return ({}, {
633
      "group_name": self.items[0],
634
      "dry_run": self.dryRun(),
635
      })
636

    
637

    
638
class R_2_groups_name_modify(baserlib.OpcodeResource):
639
  """/2/groups/[group_name]/modify resource.
640

641
  """
642
  PUT_OPCODE = opcodes.OpGroupSetParams
643

    
644
  def GetPutOpInput(self):
645
    """Changes some parameters of node group.
646

647
    """
648
    assert self.items
649
    return (self.request_body, {
650
      "group_name": self.items[0],
651
      })
652

    
653

    
654
class R_2_groups_name_rename(baserlib.OpcodeResource):
655
  """/2/groups/[group_name]/rename resource.
656

657
  """
658
  PUT_OPCODE = opcodes.OpGroupRename
659

    
660
  def GetPutOpInput(self):
661
    """Changes the name of a node group.
662

663
    """
664
    assert len(self.items) == 1
665
    return (self.request_body, {
666
      "group_name": self.items[0],
667
      "dry_run": self.dryRun(),
668
      })
669

    
670

    
671
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
672
  """/2/groups/[group_name]/assign-nodes resource.
673

674
  """
675
  PUT_OPCODE = opcodes.OpGroupAssignNodes
676

    
677
  def GetPutOpInput(self):
678
    """Assigns nodes to a group.
679

680
    """
681
    assert len(self.items) == 1
682
    return (self.request_body, {
683
      "group_name": self.items[0],
684
      "dry_run": self.dryRun(),
685
      "force": self.useForce(),
686
      })
687

    
688

    
689
def _ParseInstanceCreateRequestVersion1(data, dry_run):
690
  """Parses an instance creation request version 1.
691

692
  @rtype: L{opcodes.OpInstanceCreate}
693
  @return: Instance creation opcode
694

695
  """
696
  override = {
697
    "dry_run": dry_run,
698
    }
699

    
700
  rename = {
701
    "os": "os_type",
702
    "name": "instance_name",
703
    }
704

    
705
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
706
                             rename=rename)
707

    
708

    
709
class R_2_instances(baserlib.ResourceBase):
710
  """/2/instances resource.
711

712
  """
713
  def GET(self):
714
    """Returns a list of all available instances.
715

716
    """
717
    client = self.GetClient()
718

    
719
    use_locking = self.useLocking()
720
    if self.useBulk():
721
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
722
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
723
    else:
724
      instancesdata = client.QueryInstances([], ["name"], use_locking)
725
      instanceslist = [row[0] for row in instancesdata]
726
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
727
                                   uri_fields=("id", "uri"))
728

    
729
  def POST(self):
730
    """Create an instance.
731

732
    @return: a job id
733

734
    """
735
    if not isinstance(self.request_body, dict):
736
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
737

    
738
    # Default to request data version 0
739
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
740

    
741
    if data_version == 0:
742
      raise http.HttpBadRequest("Instance creation request version 0 is no"
743
                                " longer supported")
744
    elif data_version == 1:
745
      data = self.request_body.copy()
746
      # Remove "__version__"
747
      data.pop(_REQ_DATA_VERSION, None)
748
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
749
    else:
750
      raise http.HttpBadRequest("Unsupported request data version %s" %
751
                                data_version)
752

    
753
    return self.SubmitJob([op])
754

    
755

    
756
class R_2_instances_name(baserlib.OpcodeResource):
757
  """/2/instances/[instance_name] resource.
758

759
  """
760
  DELETE_OPCODE = opcodes.OpInstanceRemove
761

    
762
  def GET(self):
763
    """Send information about an instance.
764

765
    """
766
    client = self.GetClient()
767
    instance_name = self.items[0]
768

    
769
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
770
                                            names=[instance_name],
771
                                            fields=I_FIELDS,
772
                                            use_locking=self.useLocking())
773

    
774
    return baserlib.MapFields(I_FIELDS, result[0])
775

    
776
  def GetDeleteOpInput(self):
777
    """Delete an instance.
778

779
    """
780
    assert len(self.items) == 1
781
    return ({}, {
782
      "instance_name": self.items[0],
783
      "ignore_failures": False,
784
      "dry_run": self.dryRun(),
785
      })
786

    
787

    
788
class R_2_instances_name_info(baserlib.OpcodeResource):
789
  """/2/instances/[instance_name]/info resource.
790

791
  """
792
  GET_OPCODE = opcodes.OpInstanceQueryData
793

    
794
  def GetGetOpInput(self):
795
    """Request detailed instance information.
796

797
    """
798
    assert len(self.items) == 1
799
    return ({}, {
800
      "instances": [self.items[0]],
801
      "static": bool(self._checkIntVariable("static", default=0)),
802
      })
803

    
804

    
805
class R_2_instances_name_reboot(baserlib.OpcodeResource):
806
  """/2/instances/[instance_name]/reboot resource.
807

808
  Implements an instance reboot.
809

810
  """
811
  POST_OPCODE = opcodes.OpInstanceReboot
812

    
813
  def GetPostOpInput(self):
814
    """Reboot an instance.
815

816
    The URI takes type=[hard|soft|full] and
817
    ignore_secondaries=[False|True] parameters.
818

819
    """
820
    return ({}, {
821
      "instance_name": self.items[0],
822
      "reboot_type":
823
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
824
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
825
      "dry_run": self.dryRun(),
826
      })
827

    
828

    
829
class R_2_instances_name_startup(baserlib.OpcodeResource):
830
  """/2/instances/[instance_name]/startup resource.
831

832
  Implements an instance startup.
833

834
  """
835
  PUT_OPCODE = opcodes.OpInstanceStartup
836

    
837
  def GetPutOpInput(self):
838
    """Startup an instance.
839

840
    The URI takes force=[False|True] parameter to start the instance
841
    if even if secondary disks are failing.
842

843
    """
844
    return ({}, {
845
      "instance_name": self.items[0],
846
      "force": self.useForce(),
847
      "dry_run": self.dryRun(),
848
      "no_remember": bool(self._checkIntVariable("no_remember")),
849
      })
850

    
851

    
852
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
853
  """/2/instances/[instance_name]/shutdown resource.
854

855
  Implements an instance shutdown.
856

857
  """
858
  PUT_OPCODE = opcodes.OpInstanceShutdown
859

    
860
  def GetPutOpInput(self):
861
    """Shutdown an instance.
862

863
    """
864
    return (self.request_body, {
865
      "instance_name": self.items[0],
866
      "no_remember": bool(self._checkIntVariable("no_remember")),
867
      "dry_run": self.dryRun(),
868
      })
869

    
870

    
871
def _ParseInstanceReinstallRequest(name, data):
872
  """Parses a request for reinstalling an instance.
873

874
  """
875
  if not isinstance(data, dict):
876
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
877

    
878
  ostype = baserlib.CheckParameter(data, "os", default=None)
879
  start = baserlib.CheckParameter(data, "start", exptype=bool,
880
                                  default=True)
881
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
882

    
883
  ops = [
884
    opcodes.OpInstanceShutdown(instance_name=name),
885
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
886
                                osparams=osparams),
887
    ]
888

    
889
  if start:
890
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
891

    
892
  return ops
893

    
894

    
895
class R_2_instances_name_reinstall(baserlib.ResourceBase):
896
  """/2/instances/[instance_name]/reinstall resource.
897

898
  Implements an instance reinstall.
899

900
  """
901
  def POST(self):
902
    """Reinstall an instance.
903

904
    The URI takes os=name and nostartup=[0|1] optional
905
    parameters. By default, the instance will be started
906
    automatically.
907

908
    """
909
    if self.request_body:
910
      if self.queryargs:
911
        raise http.HttpBadRequest("Can't combine query and body parameters")
912

    
913
      body = self.request_body
914
    elif self.queryargs:
915
      # Legacy interface, do not modify/extend
916
      body = {
917
        "os": self._checkStringVariable("os"),
918
        "start": not self._checkIntVariable("nostartup"),
919
        }
920
    else:
921
      body = {}
922

    
923
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
924

    
925
    return self.SubmitJob(ops)
926

    
927

    
928
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
929
  """/2/instances/[instance_name]/replace-disks resource.
930

931
  """
932
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
933

    
934
  def GetPostOpInput(self):
935
    """Replaces disks on an instance.
936

937
    """
938
    data = self.request_body.copy()
939
    static = {
940
      "instance_name": self.items[0],
941
      }
942

    
943
    # Parse disks
944
    try:
945
      raw_disks = data["disks"]
946
    except KeyError:
947
      pass
948
    else:
949
      if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
950
        # Backwards compatibility for strings of the format "1, 2, 3"
951
        try:
952
          data["disks"] = [int(part) for part in raw_disks.split(",")]
953
        except (TypeError, ValueError), err:
954
          raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
955

    
956
    return (data, static)
957

    
958

    
959
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
960
  """/2/instances/[instance_name]/activate-disks resource.
961

962
  """
963
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
964

    
965
  def GetPutOpInput(self):
966
    """Activate disks for an instance.
967

968
    The URI might contain ignore_size to ignore current recorded size.
969

970
    """
971
    return ({}, {
972
      "instance_name": self.items[0],
973
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
974
      })
975

    
976

    
977
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
978
  """/2/instances/[instance_name]/deactivate-disks resource.
979

980
  """
981
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
982

    
983
  def GetPutOpInput(self):
984
    """Deactivate disks for an instance.
985

986
    """
987
    return ({}, {
988
      "instance_name": self.items[0],
989
      })
990

    
991

    
992
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
993
  """/2/instances/[instance_name]/prepare-export resource.
994

995
  """
996
  PUT_OPCODE = opcodes.OpBackupPrepare
997

    
998
  def GetPutOpInput(self):
999
    """Prepares an export for an instance.
1000

1001
    """
1002
    return ({}, {
1003
      "instance_name": self.items[0],
1004
      "mode": self._checkStringVariable("mode"),
1005
      })
1006

    
1007

    
1008
class R_2_instances_name_export(baserlib.OpcodeResource):
1009
  """/2/instances/[instance_name]/export resource.
1010

1011
  """
1012
  PUT_OPCODE = opcodes.OpBackupExport
1013
  PUT_RENAME = {
1014
    "destination": "target_node",
1015
    }
1016

    
1017
  def GetPutOpInput(self):
1018
    """Exports an instance.
1019

1020
    """
1021
    return (self.request_body, {
1022
      "instance_name": self.items[0],
1023
      })
1024

    
1025

    
1026
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1027
  """/2/instances/[instance_name]/migrate resource.
1028

1029
  """
1030
  PUT_OPCODE = opcodes.OpInstanceMigrate
1031

    
1032
  def GetPutOpInput(self):
1033
    """Migrates an instance.
1034

1035
    """
1036
    return (self.request_body, {
1037
      "instance_name": self.items[0],
1038
      })
1039

    
1040

    
1041
class R_2_instances_name_failover(baserlib.OpcodeResource):
1042
  """/2/instances/[instance_name]/failover resource.
1043

1044
  """
1045
  PUT_OPCODE = opcodes.OpInstanceFailover
1046

    
1047
  def GetPutOpInput(self):
1048
    """Does a failover of an instance.
1049

1050
    """
1051
    return (self.request_body, {
1052
      "instance_name": self.items[0],
1053
      })
1054

    
1055

    
1056
class R_2_instances_name_rename(baserlib.OpcodeResource):
1057
  """/2/instances/[instance_name]/rename resource.
1058

1059
  """
1060
  PUT_OPCODE = opcodes.OpInstanceRename
1061

    
1062
  def GetPutOpInput(self):
1063
    """Changes the name of an instance.
1064

1065
    """
1066
    return (self.request_body, {
1067
      "instance_name": self.items[0],
1068
      })
1069

    
1070

    
1071
class R_2_instances_name_modify(baserlib.OpcodeResource):
1072
  """/2/instances/[instance_name]/modify resource.
1073

1074
  """
1075
  PUT_OPCODE = opcodes.OpInstanceSetParams
1076

    
1077
  def GetPutOpInput(self):
1078
    """Changes parameters of an instance.
1079

1080
    """
1081
    return (self.request_body, {
1082
      "instance_name": self.items[0],
1083
      })
1084

    
1085

    
1086
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1087
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1088

1089
  """
1090
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1091

    
1092
  def GetPostOpInput(self):
1093
    """Increases the size of an instance disk.
1094

1095
    """
1096
    return (self.request_body, {
1097
      "instance_name": self.items[0],
1098
      "disk": int(self.items[1]),
1099
      })
1100

    
1101

    
1102
class R_2_instances_name_console(baserlib.ResourceBase):
1103
  """/2/instances/[instance_name]/console resource.
1104

1105
  """
1106
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1107

    
1108
  def GET(self):
1109
    """Request information for connecting to instance's console.
1110

1111
    @return: Serialized instance console description, see
1112
             L{objects.InstanceConsole}
1113

1114
    """
1115
    client = self.GetClient()
1116

    
1117
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1118

    
1119
    if console is None:
1120
      raise http.HttpServiceUnavailable("Instance console unavailable")
1121

    
1122
    assert isinstance(console, dict)
1123
    return console
1124

    
1125

    
1126
def _GetQueryFields(args):
1127
  """
1128

1129
  """
1130
  try:
1131
    fields = args["fields"]
1132
  except KeyError:
1133
    raise http.HttpBadRequest("Missing 'fields' query argument")
1134

    
1135
  return _SplitQueryFields(fields[0])
1136

    
1137

    
1138
def _SplitQueryFields(fields):
1139
  """
1140

1141
  """
1142
  return [i.strip() for i in fields.split(",")]
1143

    
1144

    
1145
class R_2_query(baserlib.ResourceBase):
1146
  """/2/query/[resource] resource.
1147

1148
  """
1149
  # Results might contain sensitive information
1150
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1151

    
1152
  def _Query(self, fields, filter_):
1153
    return self.GetClient().Query(self.items[0], fields, filter_).ToDict()
1154

    
1155
  def GET(self):
1156
    """Returns resource information.
1157

1158
    @return: Query result, see L{objects.QueryResponse}
1159

1160
    """
1161
    return self._Query(_GetQueryFields(self.queryargs), None)
1162

    
1163
  def PUT(self):
1164
    """Submits job querying for resources.
1165

1166
    @return: Query result, see L{objects.QueryResponse}
1167

1168
    """
1169
    body = self.request_body
1170

    
1171
    baserlib.CheckType(body, dict, "Body contents")
1172

    
1173
    try:
1174
      fields = body["fields"]
1175
    except KeyError:
1176
      fields = _GetQueryFields(self.queryargs)
1177

    
1178
    return self._Query(fields, self.request_body.get("filter", None))
1179

    
1180

    
1181
class R_2_query_fields(baserlib.ResourceBase):
1182
  """/2/query/[resource]/fields resource.
1183

1184
  """
1185
  def GET(self):
1186
    """Retrieves list of available fields for a resource.
1187

1188
    @return: List of serialized L{objects.QueryFieldDefinition}
1189

1190
    """
1191
    try:
1192
      raw_fields = self.queryargs["fields"]
1193
    except KeyError:
1194
      fields = None
1195
    else:
1196
      fields = _SplitQueryFields(raw_fields[0])
1197

    
1198
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1199

    
1200

    
1201
class _R_Tags(baserlib.ResourceBase):
1202
  """ Quasiclass for tagging resources
1203

1204
  Manages tags. When inheriting this class you must define the
1205
  TAG_LEVEL for it.
1206

1207
  """
1208
  TAG_LEVEL = None
1209

    
1210
  def __init__(self, items, queryargs, req):
1211
    """A tag resource constructor.
1212

1213
    We have to override the default to sort out cluster naming case.
1214

1215
    """
1216
    baserlib.ResourceBase.__init__(self, items, queryargs, req)
1217

    
1218
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1219
      self.name = None
1220
    else:
1221
      self.name = items[0]
1222

    
1223
  def GET(self):
1224
    """Returns a list of tags.
1225

1226
    Example: ["tag1", "tag2", "tag3"]
1227

1228
    """
1229
    kind = self.TAG_LEVEL
1230

    
1231
    if kind in (constants.TAG_INSTANCE,
1232
                constants.TAG_NODEGROUP,
1233
                constants.TAG_NODE):
1234
      if not self.name:
1235
        raise http.HttpBadRequest("Missing name on tag request")
1236

    
1237
      cl = self.GetClient()
1238
      if kind == constants.TAG_INSTANCE:
1239
        fn = cl.QueryInstances
1240
      elif kind == constants.TAG_NODEGROUP:
1241
        fn = cl.QueryGroups
1242
      else:
1243
        fn = cl.QueryNodes
1244
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1245
      if not result or not result[0]:
1246
        raise http.HttpBadGateway("Invalid response from tag query")
1247
      tags = result[0][0]
1248

    
1249
    elif kind == constants.TAG_CLUSTER:
1250
      assert not self.name
1251
      # TODO: Use query API?
1252
      ssc = ssconf.SimpleStore()
1253
      tags = ssc.GetClusterTags()
1254

    
1255
    return list(tags)
1256

    
1257
  def PUT(self):
1258
    """Add a set of tags.
1259

1260
    The request as a list of strings should be PUT to this URI. And
1261
    you'll have back a job id.
1262

1263
    """
1264
    # pylint: disable-msg=W0212
1265
    if "tag" not in self.queryargs:
1266
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1267
                                " the 'tag' parameter")
1268
    op = opcodes.OpTagsSet(kind=self.TAG_LEVEL, name=self.name,
1269
                           tags=self.queryargs["tag"], dry_run=self.dryRun())
1270
    return self.SubmitJob([op])
1271

    
1272
  def DELETE(self):
1273
    """Delete a tag.
1274

1275
    In order to delete a set of tags, the DELETE
1276
    request should be addressed to URI like:
1277
    /tags?tag=[tag]&tag=[tag]
1278

1279
    """
1280
    # pylint: disable-msg=W0212
1281
    if "tag" not in self.queryargs:
1282
      # no we not gonna delete all tags
1283
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1284
                                " tag(s) using the 'tag' parameter")
1285
    op = opcodes.OpTagsDel(kind=self.TAG_LEVEL, name=self.name,
1286
                           tags=self.queryargs["tag"], dry_run=self.dryRun())
1287
    return self.SubmitJob([op])
1288

    
1289

    
1290
class R_2_instances_name_tags(_R_Tags):
1291
  """ /2/instances/[instance_name]/tags resource.
1292

1293
  Manages per-instance tags.
1294

1295
  """
1296
  TAG_LEVEL = constants.TAG_INSTANCE
1297

    
1298

    
1299
class R_2_nodes_name_tags(_R_Tags):
1300
  """ /2/nodes/[node_name]/tags resource.
1301

1302
  Manages per-node tags.
1303

1304
  """
1305
  TAG_LEVEL = constants.TAG_NODE
1306

    
1307

    
1308
class R_2_groups_name_tags(_R_Tags):
1309
  """ /2/groups/[group_name]/tags resource.
1310

1311
  Manages per-nodegroup tags.
1312

1313
  """
1314
  TAG_LEVEL = constants.TAG_NODEGROUP
1315

    
1316

    
1317
class R_2_tags(_R_Tags):
1318
  """ /2/tags resource.
1319

1320
  Manages cluster tags.
1321

1322
  """
1323
  TAG_LEVEL = constants.TAG_CLUSTER