Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ d5211458

History | View | Annotate | Download (36.8 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_CANDIATE = "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_CANDIATE,
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.ResourceBase):
387
  """ /2/nodes/[node_name]/role resource.
388

389
  """
390
  def GET(self):
391
    """Returns the current node role.
392

393
    @return: Node role
394

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

    
401
    return _NR_MAP[result[0][0]]
402

    
403
  def PUT(self):
404
    """Sets the node role.
405

406
    @return: a job id
407

408
    """
409
    if not isinstance(self.request_body, basestring):
410
      raise http.HttpBadRequest("Invalid body contents, not a string")
411

    
412
    node_name = self.items[0]
413
    role = self.request_body
414

    
415
    if role == _NR_REGULAR:
416
      candidate = False
417
      offline = False
418
      drained = False
419

    
420
    elif role == _NR_MASTER_CANDIATE:
421
      candidate = True
422
      offline = drained = None
423

    
424
    elif role == _NR_DRAINED:
425
      drained = True
426
      candidate = offline = None
427

    
428
    elif role == _NR_OFFLINE:
429
      offline = True
430
      candidate = drained = None
431

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

    
435
    op = opcodes.OpNodeSetParams(node_name=node_name,
436
                                 master_candidate=candidate,
437
                                 offline=offline,
438
                                 drained=drained,
439
                                 force=bool(self.useForce()))
440

    
441
    return self.SubmitJob([op])
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.ResourceBase):
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

    
502
  def GET(self):
503
    node_name = self.items[0]
504

    
505
    storage_type = self._checkStringVariable("storage_type", None)
506
    if not storage_type:
507
      raise http.HttpBadRequest("Missing the required 'storage_type'"
508
                                " parameter")
509

    
510
    output_fields = self._checkStringVariable("output_fields", None)
511
    if not output_fields:
512
      raise http.HttpBadRequest("Missing the required 'output_fields'"
513
                                " parameter")
514

    
515
    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
516
                                    storage_type=storage_type,
517
                                    output_fields=output_fields.split(","))
518
    return self.SubmitJob([op])
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.ResourceBase):
609
  """/2/groups/[group_name] resource.
610

611
  """
612
  def GET(self):
613
    """Send information about a node group.
614

615
    """
616
    group_name = self.items[0]
617
    client = self.GetClient()
618

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

    
623
    return baserlib.MapFields(G_FIELDS, result[0])
624

    
625
  def DELETE(self):
626
    """Delete a node group.
627

628
    """
629
    op = opcodes.OpGroupRemove(group_name=self.items[0],
630
                               dry_run=bool(self.dryRun()))
631

    
632
    return self.SubmitJob([op])
633

    
634

    
635
class R_2_groups_name_modify(baserlib.OpcodeResource):
636
  """/2/groups/[group_name]/modify resource.
637

638
  """
639
  PUT_OPCODE = opcodes.OpGroupSetParams
640

    
641
  def GetPutOpInput(self):
642
    """Changes some parameters of node group.
643

644
    """
645
    assert self.items
646
    return (self.request_body, {
647
      "group_name": self.items[0],
648
      })
649

    
650

    
651
def _ParseRenameGroupRequest(name, data, dry_run):
652
  """Parses a request for renaming a node group.
653

654
  @type name: string
655
  @param name: name of the node group to rename
656
  @type data: dict
657
  @param data: the body received by the rename request
658
  @type dry_run: bool
659
  @param dry_run: whether to perform a dry run
660

661
  @rtype: L{opcodes.OpGroupRename}
662
  @return: Node group rename opcode
663

664
  """
665
  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
666
    "group_name": name,
667
    "dry_run": dry_run,
668
    })
669

    
670

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

674
  """
675
  def PUT(self):
676
    """Changes the name of a node group.
677

678
    @return: a job id
679

680
    """
681
    baserlib.CheckType(self.request_body, dict, "Body contents")
682
    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
683
                                  self.dryRun())
684
    return self.SubmitJob([op])
685

    
686

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

690
  """
691
  def PUT(self):
692
    """Assigns nodes to a group.
693

694
    @return: a job id
695

696
    """
697
    op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
698
      "group_name": self.items[0],
699
      "dry_run": self.dryRun(),
700
      "force": self.useForce(),
701
      })
702

    
703
    return self.SubmitJob([op])
704

    
705

    
706
def _ParseInstanceCreateRequestVersion1(data, dry_run):
707
  """Parses an instance creation request version 1.
708

709
  @rtype: L{opcodes.OpInstanceCreate}
710
  @return: Instance creation opcode
711

712
  """
713
  override = {
714
    "dry_run": dry_run,
715
    }
716

    
717
  rename = {
718
    "os": "os_type",
719
    "name": "instance_name",
720
    }
721

    
722
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
723
                             rename=rename)
724

    
725

    
726
class R_2_instances(baserlib.ResourceBase):
727
  """/2/instances resource.
728

729
  """
730
  def GET(self):
731
    """Returns a list of all available instances.
732

733
    """
734
    client = self.GetClient()
735

    
736
    use_locking = self.useLocking()
737
    if self.useBulk():
738
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
739
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
740
    else:
741
      instancesdata = client.QueryInstances([], ["name"], use_locking)
742
      instanceslist = [row[0] for row in instancesdata]
743
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
744
                                   uri_fields=("id", "uri"))
745

    
746
  def POST(self):
747
    """Create an instance.
748

749
    @return: a job id
750

751
    """
752
    if not isinstance(self.request_body, dict):
753
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
754

    
755
    # Default to request data version 0
756
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
757

    
758
    if data_version == 0:
759
      raise http.HttpBadRequest("Instance creation request version 0 is no"
760
                                " longer supported")
761
    elif data_version == 1:
762
      data = self.request_body.copy()
763
      # Remove "__version__"
764
      data.pop(_REQ_DATA_VERSION, None)
765
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
766
    else:
767
      raise http.HttpBadRequest("Unsupported request data version %s" %
768
                                data_version)
769

    
770
    return self.SubmitJob([op])
771

    
772

    
773
class R_2_instances_name(baserlib.ResourceBase):
774
  """/2/instances/[instance_name] resource.
775

776
  """
777
  def GET(self):
778
    """Send information about an instance.
779

780
    """
781
    client = self.GetClient()
782
    instance_name = self.items[0]
783

    
784
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
785
                                            names=[instance_name],
786
                                            fields=I_FIELDS,
787
                                            use_locking=self.useLocking())
788

    
789
    return baserlib.MapFields(I_FIELDS, result[0])
790

    
791
  def DELETE(self):
792
    """Delete an instance.
793

794
    """
795
    op = opcodes.OpInstanceRemove(instance_name=self.items[0],
796
                                  ignore_failures=False,
797
                                  dry_run=bool(self.dryRun()))
798
    return self.SubmitJob([op])
799

    
800

    
801
class R_2_instances_name_info(baserlib.ResourceBase):
802
  """/2/instances/[instance_name]/info resource.
803

804
  """
805
  def GET(self):
806
    """Request detailed instance information.
807

808
    """
809
    instance_name = self.items[0]
810
    static = bool(self._checkIntVariable("static", default=0))
811

    
812
    op = opcodes.OpInstanceQueryData(instances=[instance_name],
813
                                     static=static)
814
    return self.SubmitJob([op])
815

    
816

    
817
class R_2_instances_name_reboot(baserlib.ResourceBase):
818
  """/2/instances/[instance_name]/reboot resource.
819

820
  Implements an instance reboot.
821

822
  """
823
  def POST(self):
824
    """Reboot an instance.
825

826
    The URI takes type=[hard|soft|full] and
827
    ignore_secondaries=[False|True] parameters.
828

829
    """
830
    instance_name = self.items[0]
831
    reboot_type = self.queryargs.get("type",
832
                                     [constants.INSTANCE_REBOOT_HARD])[0]
833
    ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
834
    op = opcodes.OpInstanceReboot(instance_name=instance_name,
835
                                  reboot_type=reboot_type,
836
                                  ignore_secondaries=ignore_secondaries,
837
                                  dry_run=bool(self.dryRun()))
838

    
839
    return self.SubmitJob([op])
840

    
841

    
842
class R_2_instances_name_startup(baserlib.ResourceBase):
843
  """/2/instances/[instance_name]/startup resource.
844

845
  Implements an instance startup.
846

847
  """
848
  def PUT(self):
849
    """Startup an instance.
850

851
    The URI takes force=[False|True] parameter to start the instance
852
    if even if secondary disks are failing.
853

854
    """
855
    instance_name = self.items[0]
856
    force_startup = bool(self._checkIntVariable("force"))
857
    no_remember = bool(self._checkIntVariable("no_remember"))
858
    op = opcodes.OpInstanceStartup(instance_name=instance_name,
859
                                   force=force_startup,
860
                                   dry_run=bool(self.dryRun()),
861
                                   no_remember=no_remember)
862

    
863
    return self.SubmitJob([op])
864

    
865

    
866
def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
867
  """Parses a request for an instance shutdown.
868

869
  @rtype: L{opcodes.OpInstanceShutdown}
870
  @return: Instance shutdown opcode
871

872
  """
873
  return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
874
    "instance_name": name,
875
    "dry_run": dry_run,
876
    "no_remember": no_remember,
877
    })
878

    
879

    
880
class R_2_instances_name_shutdown(baserlib.ResourceBase):
881
  """/2/instances/[instance_name]/shutdown resource.
882

883
  Implements an instance shutdown.
884

885
  """
886
  def PUT(self):
887
    """Shutdown an instance.
888

889
    @return: a job id
890

891
    """
892
    baserlib.CheckType(self.request_body, dict, "Body contents")
893

    
894
    no_remember = bool(self._checkIntVariable("no_remember"))
895
    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
896
                                       bool(self.dryRun()), no_remember)
897

    
898
    return self.SubmitJob([op])
899

    
900

    
901
def _ParseInstanceReinstallRequest(name, data):
902
  """Parses a request for reinstalling an instance.
903

904
  """
905
  if not isinstance(data, dict):
906
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
907

    
908
  ostype = baserlib.CheckParameter(data, "os", default=None)
909
  start = baserlib.CheckParameter(data, "start", exptype=bool,
910
                                  default=True)
911
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
912

    
913
  ops = [
914
    opcodes.OpInstanceShutdown(instance_name=name),
915
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
916
                                osparams=osparams),
917
    ]
918

    
919
  if start:
920
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
921

    
922
  return ops
923

    
924

    
925
class R_2_instances_name_reinstall(baserlib.ResourceBase):
926
  """/2/instances/[instance_name]/reinstall resource.
927

928
  Implements an instance reinstall.
929

930
  """
931
  def POST(self):
932
    """Reinstall an instance.
933

934
    The URI takes os=name and nostartup=[0|1] optional
935
    parameters. By default, the instance will be started
936
    automatically.
937

938
    """
939
    if self.request_body:
940
      if self.queryargs:
941
        raise http.HttpBadRequest("Can't combine query and body parameters")
942

    
943
      body = self.request_body
944
    elif self.queryargs:
945
      # Legacy interface, do not modify/extend
946
      body = {
947
        "os": self._checkStringVariable("os"),
948
        "start": not self._checkIntVariable("nostartup"),
949
        }
950
    else:
951
      body = {}
952

    
953
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
954

    
955
    return self.SubmitJob(ops)
956

    
957

    
958
def _ParseInstanceReplaceDisksRequest(name, data):
959
  """Parses a request for an instance export.
960

961
  @rtype: L{opcodes.OpInstanceReplaceDisks}
962
  @return: Instance export opcode
963

964
  """
965
  override = {
966
    "instance_name": name,
967
    }
968

    
969
  # Parse disks
970
  try:
971
    raw_disks = data["disks"]
972
  except KeyError:
973
    pass
974
  else:
975
    if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
976
      # Backwards compatibility for strings of the format "1, 2, 3"
977
      try:
978
        data["disks"] = [int(part) for part in raw_disks.split(",")]
979
      except (TypeError, ValueError), err:
980
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
981

    
982
  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
983

    
984

    
985
class R_2_instances_name_replace_disks(baserlib.ResourceBase):
986
  """/2/instances/[instance_name]/replace-disks resource.
987

988
  """
989
  def POST(self):
990
    """Replaces disks on an instance.
991

992
    """
993
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
994

    
995
    return self.SubmitJob([op])
996

    
997

    
998
class R_2_instances_name_activate_disks(baserlib.ResourceBase):
999
  """/2/instances/[instance_name]/activate-disks resource.
1000

1001
  """
1002
  def PUT(self):
1003
    """Activate disks for an instance.
1004

1005
    The URI might contain ignore_size to ignore current recorded size.
1006

1007
    """
1008
    instance_name = self.items[0]
1009
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1010

    
1011
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1012
                                         ignore_size=ignore_size)
1013

    
1014
    return self.SubmitJob([op])
1015

    
1016

    
1017
class R_2_instances_name_deactivate_disks(baserlib.ResourceBase):
1018
  """/2/instances/[instance_name]/deactivate-disks resource.
1019

1020
  """
1021
  def PUT(self):
1022
    """Deactivate disks for an instance.
1023

1024
    """
1025
    instance_name = self.items[0]
1026

    
1027
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1028

    
1029
    return self.SubmitJob([op])
1030

    
1031

    
1032
class R_2_instances_name_prepare_export(baserlib.ResourceBase):
1033
  """/2/instances/[instance_name]/prepare-export resource.
1034

1035
  """
1036
  def PUT(self):
1037
    """Prepares an export for an instance.
1038

1039
    @return: a job id
1040

1041
    """
1042
    instance_name = self.items[0]
1043
    mode = self._checkStringVariable("mode")
1044

    
1045
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1046
                                 mode=mode)
1047

    
1048
    return self.SubmitJob([op])
1049

    
1050

    
1051
def _ParseExportInstanceRequest(name, data):
1052
  """Parses a request for an instance export.
1053

1054
  @rtype: L{opcodes.OpBackupExport}
1055
  @return: Instance export opcode
1056

1057
  """
1058
  # Rename "destination" to "target_node"
1059
  try:
1060
    data["target_node"] = data.pop("destination")
1061
  except KeyError:
1062
    pass
1063

    
1064
  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1065
    "instance_name": name,
1066
    })
1067

    
1068

    
1069
class R_2_instances_name_export(baserlib.ResourceBase):
1070
  """/2/instances/[instance_name]/export resource.
1071

1072
  """
1073
  def PUT(self):
1074
    """Exports an instance.
1075

1076
    @return: a job id
1077

1078
    """
1079
    if not isinstance(self.request_body, dict):
1080
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1081

    
1082
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1083

    
1084
    return self.SubmitJob([op])
1085

    
1086

    
1087
def _ParseMigrateInstanceRequest(name, data):
1088
  """Parses a request for an instance migration.
1089

1090
  @rtype: L{opcodes.OpInstanceMigrate}
1091
  @return: Instance migration opcode
1092

1093
  """
1094
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1095
    "instance_name": name,
1096
    })
1097

    
1098

    
1099
class R_2_instances_name_migrate(baserlib.ResourceBase):
1100
  """/2/instances/[instance_name]/migrate resource.
1101

1102
  """
1103
  def PUT(self):
1104
    """Migrates an instance.
1105

1106
    @return: a job id
1107

1108
    """
1109
    baserlib.CheckType(self.request_body, dict, "Body contents")
1110

    
1111
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1112

    
1113
    return self.SubmitJob([op])
1114

    
1115

    
1116
class R_2_instances_name_failover(baserlib.ResourceBase):
1117
  """/2/instances/[instance_name]/failover resource.
1118

1119
  """
1120
  def PUT(self):
1121
    """Does a failover of an instance.
1122

1123
    @return: a job id
1124

1125
    """
1126
    baserlib.CheckType(self.request_body, dict, "Body contents")
1127

    
1128
    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1129
      "instance_name": self.items[0],
1130
      })
1131

    
1132
    return self.SubmitJob([op])
1133

    
1134

    
1135
def _ParseRenameInstanceRequest(name, data):
1136
  """Parses a request for renaming an instance.
1137

1138
  @rtype: L{opcodes.OpInstanceRename}
1139
  @return: Instance rename opcode
1140

1141
  """
1142
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1143
    "instance_name": name,
1144
    })
1145

    
1146

    
1147
class R_2_instances_name_rename(baserlib.ResourceBase):
1148
  """/2/instances/[instance_name]/rename resource.
1149

1150
  """
1151
  def PUT(self):
1152
    """Changes the name of an instance.
1153

1154
    @return: a job id
1155

1156
    """
1157
    baserlib.CheckType(self.request_body, dict, "Body contents")
1158

    
1159
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1160

    
1161
    return self.SubmitJob([op])
1162

    
1163

    
1164
def _ParseModifyInstanceRequest(name, data):
1165
  """Parses a request for modifying an instance.
1166

1167
  @rtype: L{opcodes.OpInstanceSetParams}
1168
  @return: Instance modify opcode
1169

1170
  """
1171
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1172
    "instance_name": name,
1173
    })
1174

    
1175

    
1176
class R_2_instances_name_modify(baserlib.ResourceBase):
1177
  """/2/instances/[instance_name]/modify resource.
1178

1179
  """
1180
  def PUT(self):
1181
    """Changes some parameters of an instance.
1182

1183
    @return: a job id
1184

1185
    """
1186
    baserlib.CheckType(self.request_body, dict, "Body contents")
1187

    
1188
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1189

    
1190
    return self.SubmitJob([op])
1191

    
1192

    
1193
class R_2_instances_name_disk_grow(baserlib.ResourceBase):
1194
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1195

1196
  """
1197
  def POST(self):
1198
    """Increases the size of an instance disk.
1199

1200
    @return: a job id
1201

1202
    """
1203
    op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1204
      "instance_name": self.items[0],
1205
      "disk": int(self.items[1]),
1206
      })
1207

    
1208
    return self.SubmitJob([op])
1209

    
1210

    
1211
class R_2_instances_name_console(baserlib.ResourceBase):
1212
  """/2/instances/[instance_name]/console resource.
1213

1214
  """
1215
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1216

    
1217
  def GET(self):
1218
    """Request information for connecting to instance's console.
1219

1220
    @return: Serialized instance console description, see
1221
             L{objects.InstanceConsole}
1222

1223
    """
1224
    client = self.GetClient()
1225

    
1226
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1227

    
1228
    if console is None:
1229
      raise http.HttpServiceUnavailable("Instance console unavailable")
1230

    
1231
    assert isinstance(console, dict)
1232
    return console
1233

    
1234

    
1235
def _GetQueryFields(args):
1236
  """
1237

1238
  """
1239
  try:
1240
    fields = args["fields"]
1241
  except KeyError:
1242
    raise http.HttpBadRequest("Missing 'fields' query argument")
1243

    
1244
  return _SplitQueryFields(fields[0])
1245

    
1246

    
1247
def _SplitQueryFields(fields):
1248
  """
1249

1250
  """
1251
  return [i.strip() for i in fields.split(",")]
1252

    
1253

    
1254
class R_2_query(baserlib.ResourceBase):
1255
  """/2/query/[resource] resource.
1256

1257
  """
1258
  # Results might contain sensitive information
1259
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1260

    
1261
  def _Query(self, fields, filter_):
1262
    return self.GetClient().Query(self.items[0], fields, filter_).ToDict()
1263

    
1264
  def GET(self):
1265
    """Returns resource information.
1266

1267
    @return: Query result, see L{objects.QueryResponse}
1268

1269
    """
1270
    return self._Query(_GetQueryFields(self.queryargs), None)
1271

    
1272
  def PUT(self):
1273
    """Submits job querying for resources.
1274

1275
    @return: Query result, see L{objects.QueryResponse}
1276

1277
    """
1278
    body = self.request_body
1279

    
1280
    baserlib.CheckType(body, dict, "Body contents")
1281

    
1282
    try:
1283
      fields = body["fields"]
1284
    except KeyError:
1285
      fields = _GetQueryFields(self.queryargs)
1286

    
1287
    return self._Query(fields, self.request_body.get("filter", None))
1288

    
1289

    
1290
class R_2_query_fields(baserlib.ResourceBase):
1291
  """/2/query/[resource]/fields resource.
1292

1293
  """
1294
  def GET(self):
1295
    """Retrieves list of available fields for a resource.
1296

1297
    @return: List of serialized L{objects.QueryFieldDefinition}
1298

1299
    """
1300
    try:
1301
      raw_fields = self.queryargs["fields"]
1302
    except KeyError:
1303
      fields = None
1304
    else:
1305
      fields = _SplitQueryFields(raw_fields[0])
1306

    
1307
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1308

    
1309

    
1310
class _R_Tags(baserlib.ResourceBase):
1311
  """ Quasiclass for tagging resources
1312

1313
  Manages tags. When inheriting this class you must define the
1314
  TAG_LEVEL for it.
1315

1316
  """
1317
  TAG_LEVEL = None
1318

    
1319
  def __init__(self, items, queryargs, req):
1320
    """A tag resource constructor.
1321

1322
    We have to override the default to sort out cluster naming case.
1323

1324
    """
1325
    baserlib.ResourceBase.__init__(self, items, queryargs, req)
1326

    
1327
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1328
      self.name = None
1329
    else:
1330
      self.name = items[0]
1331

    
1332
  def GET(self):
1333
    """Returns a list of tags.
1334

1335
    Example: ["tag1", "tag2", "tag3"]
1336

1337
    """
1338
    kind = self.TAG_LEVEL
1339

    
1340
    if kind in (constants.TAG_INSTANCE,
1341
                constants.TAG_NODEGROUP,
1342
                constants.TAG_NODE):
1343
      if not self.name:
1344
        raise http.HttpBadRequest("Missing name on tag request")
1345

    
1346
      cl = self.GetClient()
1347
      if kind == constants.TAG_INSTANCE:
1348
        fn = cl.QueryInstances
1349
      elif kind == constants.TAG_NODEGROUP:
1350
        fn = cl.QueryGroups
1351
      else:
1352
        fn = cl.QueryNodes
1353
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1354
      if not result or not result[0]:
1355
        raise http.HttpBadGateway("Invalid response from tag query")
1356
      tags = result[0][0]
1357

    
1358
    elif kind == constants.TAG_CLUSTER:
1359
      assert not self.name
1360
      # TODO: Use query API?
1361
      ssc = ssconf.SimpleStore()
1362
      tags = ssc.GetClusterTags()
1363

    
1364
    return list(tags)
1365

    
1366
  def PUT(self):
1367
    """Add a set of tags.
1368

1369
    The request as a list of strings should be PUT to this URI. And
1370
    you'll have back a job id.
1371

1372
    """
1373
    # pylint: disable-msg=W0212
1374
    if "tag" not in self.queryargs:
1375
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1376
                                " the 'tag' parameter")
1377
    op = opcodes.OpTagsSet(kind=self.TAG_LEVEL, name=self.name,
1378
                           tags=self.queryargs["tag"], dry_run=self.dryRun())
1379
    return self.SubmitJob([op])
1380

    
1381
  def DELETE(self):
1382
    """Delete a tag.
1383

1384
    In order to delete a set of tags, the DELETE
1385
    request should be addressed to URI like:
1386
    /tags?tag=[tag]&tag=[tag]
1387

1388
    """
1389
    # pylint: disable-msg=W0212
1390
    if "tag" not in self.queryargs:
1391
      # no we not gonna delete all tags
1392
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1393
                                " tag(s) using the 'tag' parameter")
1394
    op = opcodes.OpTagsDel(kind=self.TAG_LEVEL, name=self.name,
1395
                           tags=self.queryargs["tag"], dry_run=self.dryRun())
1396
    return self.SubmitJob([op])
1397

    
1398

    
1399
class R_2_instances_name_tags(_R_Tags):
1400
  """ /2/instances/[instance_name]/tags resource.
1401

1402
  Manages per-instance tags.
1403

1404
  """
1405
  TAG_LEVEL = constants.TAG_INSTANCE
1406

    
1407

    
1408
class R_2_nodes_name_tags(_R_Tags):
1409
  """ /2/nodes/[node_name]/tags resource.
1410

1411
  Manages per-node tags.
1412

1413
  """
1414
  TAG_LEVEL = constants.TAG_NODE
1415

    
1416

    
1417
class R_2_groups_name_tags(_R_Tags):
1418
  """ /2/groups/[group_name]/tags resource.
1419

1420
  Manages per-nodegroup tags.
1421

1422
  """
1423
  TAG_LEVEL = constants.TAG_NODEGROUP
1424

    
1425

    
1426
class R_2_tags(_R_Tags):
1427
  """ /2/tags resource.
1428

1429
  Manages cluster tags.
1430

1431
  """
1432
  TAG_LEVEL = constants.TAG_CLUSTER