Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ a52978c7

History | View | Annotate | Download (32.9 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=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_2(R_root):
165
  """/2 resource.
166

167
  """
168

    
169

    
170
class R_version(baserlib.ResourceBase):
171
  """/version resource.
172

173
  This resource should be used to determine the remote API version and
174
  to adapt clients accordingly.
175

176
  """
177
  @staticmethod
178
  def GET():
179
    """Returns the remote API version.
180

181
    """
182
    return constants.RAPI_VERSION
183

    
184

    
185
class R_2_info(baserlib.OpcodeResource):
186
  """/2/info resource.
187

188
  """
189
  GET_OPCODE = opcodes.OpClusterQuery
190

    
191
  def GET(self):
192
    """Returns cluster information.
193

194
    """
195
    client = self.GetClient()
196
    return client.QueryClusterInfo()
197

    
198

    
199
class R_2_features(baserlib.ResourceBase):
200
  """/2/features resource.
201

202
  """
203
  @staticmethod
204
  def GET():
205
    """Returns list of optional RAPI features implemented.
206

207
    """
208
    return list(ALL_FEATURES)
209

    
210

    
211
class R_2_os(baserlib.OpcodeResource):
212
  """/2/os resource.
213

214
  """
215
  GET_OPCODE = opcodes.OpOsDiagnose
216

    
217
  def GET(self):
218
    """Return a list of all OSes.
219

220
    Can return error 500 in case of a problem.
221

222
    Example: ["debian-etch"]
223

224
    """
225
    cl = self.GetClient()
226
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
227
    job_id = self.SubmitJob([op], cl=cl)
228
    # we use custom feedback function, instead of print we log the status
229
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
230
    diagnose_data = result[0]
231

    
232
    if not isinstance(diagnose_data, list):
233
      raise http.HttpBadGateway(message="Can't get OS list")
234

    
235
    os_names = []
236
    for (name, variants) in diagnose_data:
237
      os_names.extend(cli.CalculateOSNames(name, variants))
238

    
239
    return os_names
240

    
241

    
242
class R_2_redist_config(baserlib.OpcodeResource):
243
  """/2/redistribute-config resource.
244

245
  """
246
  PUT_OPCODE = opcodes.OpClusterRedistConf
247

    
248

    
249
class R_2_cluster_modify(baserlib.OpcodeResource):
250
  """/2/modify resource.
251

252
  """
253
  PUT_OPCODE = opcodes.OpClusterSetParams
254

    
255

    
256
class R_2_jobs(baserlib.ResourceBase):
257
  """/2/jobs resource.
258

259
  """
260
  def GET(self):
261
    """Returns a dictionary of jobs.
262

263
    @return: a dictionary with jobs id and uri.
264

265
    """
266
    client = self.GetClient()
267

    
268
    if self.useBulk():
269
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
270
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
271
    else:
272
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
273
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
274
                                   uri_fields=("id", "uri"))
275

    
276

    
277
class R_2_jobs_id(baserlib.ResourceBase):
278
  """/2/jobs/[job_id] resource.
279

280
  """
281
  def GET(self):
282
    """Returns a job status.
283

284
    @return: a dictionary with job parameters.
285
        The result includes:
286
            - id: job ID as a number
287
            - status: current job status as a string
288
            - ops: involved OpCodes as a list of dictionaries for each
289
              opcodes in the job
290
            - opstatus: OpCodes status as a list
291
            - opresult: OpCodes results as a list of lists
292

293
    """
294
    job_id = self.items[0]
295
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
296
    if result is None:
297
      raise http.HttpNotFound()
298
    return baserlib.MapFields(J_FIELDS, result)
299

    
300
  def DELETE(self):
301
    """Cancel not-yet-started job.
302

303
    """
304
    job_id = self.items[0]
305
    result = self.GetClient().CancelJob(job_id)
306
    return result
307

    
308

    
309
class R_2_jobs_id_wait(baserlib.ResourceBase):
310
  """/2/jobs/[job_id]/wait resource.
311

312
  """
313
  # WaitForJobChange provides access to sensitive information and blocks
314
  # machine resources (it's a blocking RAPI call), hence restricting access.
315
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
316

    
317
  def GET(self):
318
    """Waits for job changes.
319

320
    """
321
    job_id = self.items[0]
322

    
323
    fields = self.getBodyParameter("fields")
324
    prev_job_info = self.getBodyParameter("previous_job_info", None)
325
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
326

    
327
    if not isinstance(fields, list):
328
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
329

    
330
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
331
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
332
                                " be a list")
333

    
334
    if not (prev_log_serial is None or
335
            isinstance(prev_log_serial, (int, long))):
336
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
337
                                " be a number")
338

    
339
    client = self.GetClient()
340
    result = client.WaitForJobChangeOnce(job_id, fields,
341
                                         prev_job_info, prev_log_serial,
342
                                         timeout=_WFJC_TIMEOUT)
343
    if not result:
344
      raise http.HttpNotFound()
345

    
346
    if result == constants.JOB_NOTCHANGED:
347
      # No changes
348
      return None
349

    
350
    (job_info, log_entries) = result
351

    
352
    return {
353
      "job_info": job_info,
354
      "log_entries": log_entries,
355
      }
356

    
357

    
358
class R_2_nodes(baserlib.OpcodeResource):
359
  """/2/nodes resource.
360

361
  """
362
  GET_OPCODE = opcodes.OpNodeQuery
363

    
364
  def GET(self):
365
    """Returns a list of all nodes.
366

367
    """
368
    client = self.GetClient()
369

    
370
    if self.useBulk():
371
      bulkdata = client.QueryNodes([], N_FIELDS, False)
372
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
373
    else:
374
      nodesdata = client.QueryNodes([], ["name"], False)
375
      nodeslist = [row[0] for row in nodesdata]
376
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
377
                                   uri_fields=("id", "uri"))
378

    
379

    
380
class R_2_nodes_name(baserlib.OpcodeResource):
381
  """/2/nodes/[node_name] resource.
382

383
  """
384
  GET_OPCODE = opcodes.OpNodeQuery
385

    
386
  def GET(self):
387
    """Send information about a node.
388

389
    """
390
    node_name = self.items[0]
391
    client = self.GetClient()
392

    
393
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
394
                                            names=[node_name], fields=N_FIELDS,
395
                                            use_locking=self.useLocking())
396

    
397
    return baserlib.MapFields(N_FIELDS, result[0])
398

    
399

    
400
class R_2_nodes_name_role(baserlib.OpcodeResource):
401
  """/2/nodes/[node_name]/role resource.
402

403
  """
404
  PUT_OPCODE = opcodes.OpNodeSetParams
405

    
406
  def GET(self):
407
    """Returns the current node role.
408

409
    @return: Node role
410

411
    """
412
    node_name = self.items[0]
413
    client = self.GetClient()
414
    result = client.QueryNodes(names=[node_name], fields=["role"],
415
                               use_locking=self.useLocking())
416

    
417
    return _NR_MAP[result[0][0]]
418

    
419
  def GetPutOpInput(self):
420
    """Sets the node role.
421

422
    """
423
    baserlib.CheckType(self.request_body, basestring, "Body contents")
424

    
425
    role = self.request_body
426

    
427
    if role == _NR_REGULAR:
428
      candidate = False
429
      offline = False
430
      drained = False
431

    
432
    elif role == _NR_MASTER_CANDIDATE:
433
      candidate = True
434
      offline = drained = None
435

    
436
    elif role == _NR_DRAINED:
437
      drained = True
438
      candidate = offline = None
439

    
440
    elif role == _NR_OFFLINE:
441
      offline = True
442
      candidate = drained = None
443

    
444
    else:
445
      raise http.HttpBadRequest("Can't set '%s' role" % role)
446

    
447
    assert len(self.items) == 1
448

    
449
    return ({}, {
450
      "node_name": self.items[0],
451
      "master_candidate": candidate,
452
      "offline": offline,
453
      "drained": drained,
454
      "force": self.useForce(),
455
      })
456

    
457

    
458
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
459
  """/2/nodes/[node_name]/evacuate resource.
460

461
  """
462
  POST_OPCODE = opcodes.OpNodeEvacuate
463

    
464
  def GetPostOpInput(self):
465
    """Evacuate all instances off a node.
466

467
    """
468
    return (self.request_body, {
469
      "node_name": self.items[0],
470
      "dry_run": self.dryRun(),
471
      })
472

    
473

    
474
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
475
  """/2/nodes/[node_name]/migrate resource.
476

477
  """
478
  POST_OPCODE = opcodes.OpNodeMigrate
479

    
480
  def GetPostOpInput(self):
481
    """Migrate all primary instances from a node.
482

483
    """
484
    if self.queryargs:
485
      # Support old-style requests
486
      if "live" in self.queryargs and "mode" in self.queryargs:
487
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
488
                                  " be passed")
489

    
490
      if "live" in self.queryargs:
491
        if self._checkIntVariable("live", default=1):
492
          mode = constants.HT_MIGRATION_LIVE
493
        else:
494
          mode = constants.HT_MIGRATION_NONLIVE
495
      else:
496
        mode = self._checkStringVariable("mode", default=None)
497

    
498
      data = {
499
        "mode": mode,
500
        }
501
    else:
502
      data = self.request_body
503

    
504
    return (data, {
505
      "node_name": self.items[0],
506
      })
507

    
508

    
509
class R_2_nodes_name_storage(baserlib.OpcodeResource):
510
  """/2/nodes/[node_name]/storage resource.
511

512
  """
513
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
514
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
515
  GET_OPCODE = opcodes.OpNodeQueryStorage
516

    
517
  def GetGetOpInput(self):
518
    """List storage available on a node.
519

520
    """
521
    storage_type = self._checkStringVariable("storage_type", None)
522
    output_fields = self._checkStringVariable("output_fields", None)
523

    
524
    if not output_fields:
525
      raise http.HttpBadRequest("Missing the required 'output_fields'"
526
                                " parameter")
527

    
528
    return ({}, {
529
      "nodes": [self.items[0]],
530
      "storage_type": storage_type,
531
      "output_fields": output_fields.split(","),
532
      })
533

    
534

    
535
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
536
  """/2/nodes/[node_name]/storage/modify resource.
537

538
  """
539
  PUT_OPCODE = opcodes.OpNodeModifyStorage
540

    
541
  def GetPutOpInput(self):
542
    """Modifies a storage volume on a node.
543

544
    """
545
    storage_type = self._checkStringVariable("storage_type", None)
546
    name = self._checkStringVariable("name", None)
547

    
548
    if not name:
549
      raise http.HttpBadRequest("Missing the required 'name'"
550
                                " parameter")
551

    
552
    changes = {}
553

    
554
    if "allocatable" in self.queryargs:
555
      changes[constants.SF_ALLOCATABLE] = \
556
        bool(self._checkIntVariable("allocatable", default=1))
557

    
558
    return ({}, {
559
      "node_name": self.items[0],
560
      "storage_type": storage_type,
561
      "name": name,
562
      "changes": changes,
563
      })
564

    
565

    
566
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
567
  """/2/nodes/[node_name]/storage/repair resource.
568

569
  """
570
  PUT_OPCODE = opcodes.OpRepairNodeStorage
571

    
572
  def GetPutOpInput(self):
573
    """Repairs a storage volume on a node.
574

575
    """
576
    storage_type = self._checkStringVariable("storage_type", None)
577
    name = self._checkStringVariable("name", None)
578
    if not name:
579
      raise http.HttpBadRequest("Missing the required 'name'"
580
                                " parameter")
581

    
582
    return ({}, {
583
      "node_name": self.items[0],
584
      "storage_type": storage_type,
585
      "name": name,
586
      })
587

    
588

    
589
class R_2_groups(baserlib.OpcodeResource):
590
  """/2/groups resource.
591

592
  """
593
  GET_OPCODE = opcodes.OpGroupQuery
594
  POST_OPCODE = opcodes.OpGroupAdd
595
  POST_RENAME = {
596
    "name": "group_name",
597
    }
598

    
599
  def GetPostOpInput(self):
600
    """Create a node group.
601

602
    """
603
    assert not self.items
604
    return (self.request_body, {
605
      "dry_run": self.dryRun(),
606
      })
607

    
608
  def GET(self):
609
    """Returns a list of all node groups.
610

611
    """
612
    client = self.GetClient()
613

    
614
    if self.useBulk():
615
      bulkdata = client.QueryGroups([], G_FIELDS, False)
616
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
617
    else:
618
      data = client.QueryGroups([], ["name"], False)
619
      groupnames = [row[0] for row in data]
620
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
621
                                   uri_fields=("name", "uri"))
622

    
623

    
624
class R_2_groups_name(baserlib.OpcodeResource):
625
  """/2/groups/[group_name] resource.
626

627
  """
628
  DELETE_OPCODE = opcodes.OpGroupRemove
629

    
630
  def GET(self):
631
    """Send information about a node group.
632

633
    """
634
    group_name = self.items[0]
635
    client = self.GetClient()
636

    
637
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
638
                                            names=[group_name], fields=G_FIELDS,
639
                                            use_locking=self.useLocking())
640

    
641
    return baserlib.MapFields(G_FIELDS, result[0])
642

    
643
  def GetDeleteOpInput(self):
644
    """Delete a node group.
645

646
    """
647
    assert len(self.items) == 1
648
    return ({}, {
649
      "group_name": self.items[0],
650
      "dry_run": self.dryRun(),
651
      })
652

    
653

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

657
  """
658
  PUT_OPCODE = opcodes.OpGroupSetParams
659

    
660
  def GetPutOpInput(self):
661
    """Changes some parameters of node group.
662

663
    """
664
    assert self.items
665
    return (self.request_body, {
666
      "group_name": self.items[0],
667
      })
668

    
669

    
670
class R_2_groups_name_rename(baserlib.OpcodeResource):
671
  """/2/groups/[group_name]/rename resource.
672

673
  """
674
  PUT_OPCODE = opcodes.OpGroupRename
675

    
676
  def GetPutOpInput(self):
677
    """Changes the name of a node group.
678

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

    
686

    
687
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
688
  """/2/groups/[group_name]/assign-nodes resource.
689

690
  """
691
  PUT_OPCODE = opcodes.OpGroupAssignNodes
692

    
693
  def GetPutOpInput(self):
694
    """Assigns nodes to a group.
695

696
    """
697
    assert len(self.items) == 1
698
    return (self.request_body, {
699
      "group_name": self.items[0],
700
      "dry_run": self.dryRun(),
701
      "force": self.useForce(),
702
      })
703

    
704

    
705
class R_2_instances(baserlib.OpcodeResource):
706
  """/2/instances resource.
707

708
  """
709
  GET_OPCODE = opcodes.OpInstanceQuery
710
  POST_OPCODE = opcodes.OpInstanceCreate
711
  POST_RENAME = {
712
    "os": "os_type",
713
    "name": "instance_name",
714
    }
715

    
716
  def GET(self):
717
    """Returns a list of all available instances.
718

719
    """
720
    client = self.GetClient()
721

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

    
732
  def GetPostOpInput(self):
733
    """Create an instance.
734

735
    @return: a job id
736

737
    """
738
    baserlib.CheckType(self.request_body, dict, "Body contents")
739

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

    
743
    if data_version == 0:
744
      raise http.HttpBadRequest("Instance creation request version 0 is no"
745
                                " longer supported")
746
    elif data_version != 1:
747
      raise http.HttpBadRequest("Unsupported request data version %s" %
748
                                data_version)
749

    
750
    data = self.request_body.copy()
751
    # Remove "__version__"
752
    data.pop(_REQ_DATA_VERSION, None)
753

    
754
    return (data, {
755
      "dry_run": self.dryRun(),
756
      })
757

    
758

    
759
class R_2_instances_name(baserlib.OpcodeResource):
760
  """/2/instances/[instance_name] resource.
761

762
  """
763
  GET_OPCODE = opcodes.OpInstanceQuery
764
  DELETE_OPCODE = opcodes.OpInstanceRemove
765

    
766
  def GET(self):
767
    """Send information about an instance.
768

769
    """
770
    client = self.GetClient()
771
    instance_name = self.items[0]
772

    
773
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
774
                                            names=[instance_name],
775
                                            fields=I_FIELDS,
776
                                            use_locking=self.useLocking())
777

    
778
    return baserlib.MapFields(I_FIELDS, result[0])
779

    
780
  def GetDeleteOpInput(self):
781
    """Delete an instance.
782

783
    """
784
    assert len(self.items) == 1
785
    return ({}, {
786
      "instance_name": self.items[0],
787
      "ignore_failures": False,
788
      "dry_run": self.dryRun(),
789
      })
790

    
791

    
792
class R_2_instances_name_info(baserlib.OpcodeResource):
793
  """/2/instances/[instance_name]/info resource.
794

795
  """
796
  GET_OPCODE = opcodes.OpInstanceQueryData
797

    
798
  def GetGetOpInput(self):
799
    """Request detailed instance information.
800

801
    """
802
    assert len(self.items) == 1
803
    return ({}, {
804
      "instances": [self.items[0]],
805
      "static": bool(self._checkIntVariable("static", default=0)),
806
      })
807

    
808

    
809
class R_2_instances_name_reboot(baserlib.OpcodeResource):
810
  """/2/instances/[instance_name]/reboot resource.
811

812
  Implements an instance reboot.
813

814
  """
815
  POST_OPCODE = opcodes.OpInstanceReboot
816

    
817
  def GetPostOpInput(self):
818
    """Reboot an instance.
819

820
    The URI takes type=[hard|soft|full] and
821
    ignore_secondaries=[False|True] parameters.
822

823
    """
824
    return ({}, {
825
      "instance_name": self.items[0],
826
      "reboot_type":
827
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
828
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
829
      "dry_run": self.dryRun(),
830
      })
831

    
832

    
833
class R_2_instances_name_startup(baserlib.OpcodeResource):
834
  """/2/instances/[instance_name]/startup resource.
835

836
  Implements an instance startup.
837

838
  """
839
  PUT_OPCODE = opcodes.OpInstanceStartup
840

    
841
  def GetPutOpInput(self):
842
    """Startup an instance.
843

844
    The URI takes force=[False|True] parameter to start the instance
845
    if even if secondary disks are failing.
846

847
    """
848
    return ({}, {
849
      "instance_name": self.items[0],
850
      "force": self.useForce(),
851
      "dry_run": self.dryRun(),
852
      "no_remember": bool(self._checkIntVariable("no_remember")),
853
      })
854

    
855

    
856
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
857
  """/2/instances/[instance_name]/shutdown resource.
858

859
  Implements an instance shutdown.
860

861
  """
862
  PUT_OPCODE = opcodes.OpInstanceShutdown
863

    
864
  def GetPutOpInput(self):
865
    """Shutdown an instance.
866

867
    """
868
    return (self.request_body, {
869
      "instance_name": self.items[0],
870
      "no_remember": bool(self._checkIntVariable("no_remember")),
871
      "dry_run": self.dryRun(),
872
      })
873

    
874

    
875
def _ParseInstanceReinstallRequest(name, data):
876
  """Parses a request for reinstalling an instance.
877

878
  """
879
  if not isinstance(data, dict):
880
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
881

    
882
  ostype = baserlib.CheckParameter(data, "os", default=None)
883
  start = baserlib.CheckParameter(data, "start", exptype=bool,
884
                                  default=True)
885
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
886

    
887
  ops = [
888
    opcodes.OpInstanceShutdown(instance_name=name),
889
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
890
                                osparams=osparams),
891
    ]
892

    
893
  if start:
894
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
895

    
896
  return ops
897

    
898

    
899
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
900
  """/2/instances/[instance_name]/reinstall resource.
901

902
  Implements an instance reinstall.
903

904
  """
905
  POST_OPCODE = opcodes.OpInstanceReinstall
906

    
907
  def POST(self):
908
    """Reinstall an instance.
909

910
    The URI takes os=name and nostartup=[0|1] optional
911
    parameters. By default, the instance will be started
912
    automatically.
913

914
    """
915
    if self.request_body:
916
      if self.queryargs:
917
        raise http.HttpBadRequest("Can't combine query and body parameters")
918

    
919
      body = self.request_body
920
    elif self.queryargs:
921
      # Legacy interface, do not modify/extend
922
      body = {
923
        "os": self._checkStringVariable("os"),
924
        "start": not self._checkIntVariable("nostartup"),
925
        }
926
    else:
927
      body = {}
928

    
929
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
930

    
931
    return self.SubmitJob(ops)
932

    
933

    
934
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
935
  """/2/instances/[instance_name]/replace-disks resource.
936

937
  """
938
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
939

    
940
  def GetPostOpInput(self):
941
    """Replaces disks on an instance.
942

943
    """
944
    data = self.request_body.copy()
945
    static = {
946
      "instance_name": self.items[0],
947
      }
948

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

    
962
    return (data, static)
963

    
964

    
965
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
966
  """/2/instances/[instance_name]/activate-disks resource.
967

968
  """
969
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
970

    
971
  def GetPutOpInput(self):
972
    """Activate disks for an instance.
973

974
    The URI might contain ignore_size to ignore current recorded size.
975

976
    """
977
    return ({}, {
978
      "instance_name": self.items[0],
979
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
980
      })
981

    
982

    
983
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
984
  """/2/instances/[instance_name]/deactivate-disks resource.
985

986
  """
987
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
988

    
989
  def GetPutOpInput(self):
990
    """Deactivate disks for an instance.
991

992
    """
993
    return ({}, {
994
      "instance_name": self.items[0],
995
      })
996

    
997

    
998
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
999
  """/2/instances/[instance_name]/recreate-disks resource.
1000

1001
  """
1002
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1003

    
1004
  def GetPostOpInput(self):
1005
    """Recreate disks for an instance.
1006

1007
    """
1008
    return ({}, {
1009
      "instance_name": self.items[0],
1010
      })
1011

    
1012

    
1013
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1014
  """/2/instances/[instance_name]/prepare-export resource.
1015

1016
  """
1017
  PUT_OPCODE = opcodes.OpBackupPrepare
1018

    
1019
  def GetPutOpInput(self):
1020
    """Prepares an export for an instance.
1021

1022
    """
1023
    return ({}, {
1024
      "instance_name": self.items[0],
1025
      "mode": self._checkStringVariable("mode"),
1026
      })
1027

    
1028

    
1029
class R_2_instances_name_export(baserlib.OpcodeResource):
1030
  """/2/instances/[instance_name]/export resource.
1031

1032
  """
1033
  PUT_OPCODE = opcodes.OpBackupExport
1034
  PUT_RENAME = {
1035
    "destination": "target_node",
1036
    }
1037

    
1038
  def GetPutOpInput(self):
1039
    """Exports an instance.
1040

1041
    """
1042
    return (self.request_body, {
1043
      "instance_name": self.items[0],
1044
      })
1045

    
1046

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

1050
  """
1051
  PUT_OPCODE = opcodes.OpInstanceMigrate
1052

    
1053
  def GetPutOpInput(self):
1054
    """Migrates an instance.
1055

1056
    """
1057
    return (self.request_body, {
1058
      "instance_name": self.items[0],
1059
      })
1060

    
1061

    
1062
class R_2_instances_name_failover(baserlib.OpcodeResource):
1063
  """/2/instances/[instance_name]/failover resource.
1064

1065
  """
1066
  PUT_OPCODE = opcodes.OpInstanceFailover
1067

    
1068
  def GetPutOpInput(self):
1069
    """Does a failover of an instance.
1070

1071
    """
1072
    return (self.request_body, {
1073
      "instance_name": self.items[0],
1074
      })
1075

    
1076

    
1077
class R_2_instances_name_rename(baserlib.OpcodeResource):
1078
  """/2/instances/[instance_name]/rename resource.
1079

1080
  """
1081
  PUT_OPCODE = opcodes.OpInstanceRename
1082

    
1083
  def GetPutOpInput(self):
1084
    """Changes the name of an instance.
1085

1086
    """
1087
    return (self.request_body, {
1088
      "instance_name": self.items[0],
1089
      })
1090

    
1091

    
1092
class R_2_instances_name_modify(baserlib.OpcodeResource):
1093
  """/2/instances/[instance_name]/modify resource.
1094

1095
  """
1096
  PUT_OPCODE = opcodes.OpInstanceSetParams
1097

    
1098
  def GetPutOpInput(self):
1099
    """Changes parameters of an instance.
1100

1101
    """
1102
    return (self.request_body, {
1103
      "instance_name": self.items[0],
1104
      })
1105

    
1106

    
1107
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1108
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1109

1110
  """
1111
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1112

    
1113
  def GetPostOpInput(self):
1114
    """Increases the size of an instance disk.
1115

1116
    """
1117
    return (self.request_body, {
1118
      "instance_name": self.items[0],
1119
      "disk": int(self.items[1]),
1120
      })
1121

    
1122

    
1123
class R_2_instances_name_console(baserlib.ResourceBase):
1124
  """/2/instances/[instance_name]/console resource.
1125

1126
  """
1127
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1128
  GET_OPCODE = opcodes.OpInstanceConsole
1129

    
1130
  def GET(self):
1131
    """Request information for connecting to instance's console.
1132

1133
    @return: Serialized instance console description, see
1134
             L{objects.InstanceConsole}
1135

1136
    """
1137
    client = self.GetClient()
1138

    
1139
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1140

    
1141
    if console is None:
1142
      raise http.HttpServiceUnavailable("Instance console unavailable")
1143

    
1144
    assert isinstance(console, dict)
1145
    return console
1146

    
1147

    
1148
def _GetQueryFields(args):
1149
  """
1150

1151
  """
1152
  try:
1153
    fields = args["fields"]
1154
  except KeyError:
1155
    raise http.HttpBadRequest("Missing 'fields' query argument")
1156

    
1157
  return _SplitQueryFields(fields[0])
1158

    
1159

    
1160
def _SplitQueryFields(fields):
1161
  """
1162

1163
  """
1164
  return [i.strip() for i in fields.split(",")]
1165

    
1166

    
1167
class R_2_query(baserlib.ResourceBase):
1168
  """/2/query/[resource] resource.
1169

1170
  """
1171
  # Results might contain sensitive information
1172
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1173
  GET_OPCODE = opcodes.OpQuery
1174
  PUT_OPCODE = opcodes.OpQuery
1175

    
1176
  def _Query(self, fields, filter_):
1177
    return self.GetClient().Query(self.items[0], fields, filter_).ToDict()
1178

    
1179
  def GET(self):
1180
    """Returns resource information.
1181

1182
    @return: Query result, see L{objects.QueryResponse}
1183

1184
    """
1185
    return self._Query(_GetQueryFields(self.queryargs), None)
1186

    
1187
  def PUT(self):
1188
    """Submits job querying for resources.
1189

1190
    @return: Query result, see L{objects.QueryResponse}
1191

1192
    """
1193
    body = self.request_body
1194

    
1195
    baserlib.CheckType(body, dict, "Body contents")
1196

    
1197
    try:
1198
      fields = body["fields"]
1199
    except KeyError:
1200
      fields = _GetQueryFields(self.queryargs)
1201

    
1202
    return self._Query(fields, self.request_body.get("filter", None))
1203

    
1204

    
1205
class R_2_query_fields(baserlib.ResourceBase):
1206
  """/2/query/[resource]/fields resource.
1207

1208
  """
1209
  GET_OPCODE = opcodes.OpQueryFields
1210

    
1211
  def GET(self):
1212
    """Retrieves list of available fields for a resource.
1213

1214
    @return: List of serialized L{objects.QueryFieldDefinition}
1215

1216
    """
1217
    try:
1218
      raw_fields = self.queryargs["fields"]
1219
    except KeyError:
1220
      fields = None
1221
    else:
1222
      fields = _SplitQueryFields(raw_fields[0])
1223

    
1224
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1225

    
1226

    
1227
class _R_Tags(baserlib.OpcodeResource):
1228
  """ Quasiclass for tagging resources
1229

1230
  Manages tags. When inheriting this class you must define the
1231
  TAG_LEVEL for it.
1232

1233
  """
1234
  TAG_LEVEL = None
1235
  GET_OPCODE = opcodes.OpTagsGet
1236
  PUT_OPCODE = opcodes.OpTagsSet
1237
  DELETE_OPCODE = opcodes.OpTagsDel
1238

    
1239
  def __init__(self, items, queryargs, req, **kwargs):
1240
    """A tag resource constructor.
1241

1242
    We have to override the default to sort out cluster naming case.
1243

1244
    """
1245
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1246

    
1247
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1248
      self.name = None
1249
    else:
1250
      self.name = items[0]
1251

    
1252
  def GET(self):
1253
    """Returns a list of tags.
1254

1255
    Example: ["tag1", "tag2", "tag3"]
1256

1257
    """
1258
    kind = self.TAG_LEVEL
1259

    
1260
    if kind in (constants.TAG_INSTANCE,
1261
                constants.TAG_NODEGROUP,
1262
                constants.TAG_NODE):
1263
      if not self.name:
1264
        raise http.HttpBadRequest("Missing name on tag request")
1265

    
1266
      cl = self.GetClient()
1267
      if kind == constants.TAG_INSTANCE:
1268
        fn = cl.QueryInstances
1269
      elif kind == constants.TAG_NODEGROUP:
1270
        fn = cl.QueryGroups
1271
      else:
1272
        fn = cl.QueryNodes
1273
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1274
      if not result or not result[0]:
1275
        raise http.HttpBadGateway("Invalid response from tag query")
1276
      tags = result[0][0]
1277

    
1278
    elif kind == constants.TAG_CLUSTER:
1279
      assert not self.name
1280
      # TODO: Use query API?
1281
      ssc = ssconf.SimpleStore()
1282
      tags = ssc.GetClusterTags()
1283

    
1284
    return list(tags)
1285

    
1286
  def GetPutOpInput(self):
1287
    """Add a set of tags.
1288

1289
    The request as a list of strings should be PUT to this URI. And
1290
    you'll have back a job id.
1291

1292
    """
1293
    return ({}, {
1294
      "kind": self.TAG_LEVEL,
1295
      "name": self.name,
1296
      "tags": self.queryargs.get("tag", []),
1297
      "dry_run": self.dryRun(),
1298
      })
1299

    
1300
  def GetDeleteOpInput(self):
1301
    """Delete a tag.
1302

1303
    In order to delete a set of tags, the DELETE
1304
    request should be addressed to URI like:
1305
    /tags?tag=[tag]&tag=[tag]
1306

1307
    """
1308
    # Re-use code
1309
    return self.GetPutOpInput()
1310

    
1311

    
1312
class R_2_instances_name_tags(_R_Tags):
1313
  """ /2/instances/[instance_name]/tags resource.
1314

1315
  Manages per-instance tags.
1316

1317
  """
1318
  TAG_LEVEL = constants.TAG_INSTANCE
1319

    
1320

    
1321
class R_2_nodes_name_tags(_R_Tags):
1322
  """ /2/nodes/[node_name]/tags resource.
1323

1324
  Manages per-node tags.
1325

1326
  """
1327
  TAG_LEVEL = constants.TAG_NODE
1328

    
1329

    
1330
class R_2_groups_name_tags(_R_Tags):
1331
  """ /2/groups/[group_name]/tags resource.
1332

1333
  Manages per-nodegroup tags.
1334

1335
  """
1336
  TAG_LEVEL = constants.TAG_NODEGROUP
1337

    
1338

    
1339
class R_2_tags(_R_Tags):
1340
  """ /2/tags resource.
1341

1342
  Manages cluster tags.
1343

1344
  """
1345
  TAG_LEVEL = constants.TAG_CLUSTER