Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ e987f166

History | View | Annotate | Download (36.3 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 version 2 baserlib.library.
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
  To be in context of this module for instance creation POST on
32
  /2/instances is legitim while PUT would be not, due to it does create a
33
  new entity and not just replace /2/instances with it.
34

35
  So when adding new methods, if they are operating on the URI entity itself,
36
  PUT should be prefered over POST.
37

38
"""
39

    
40
# pylint: disable-msg=C0103
41

    
42
# C0103: Invalid name, since the R_* names are not conforming
43

    
44
from ganeti import opcodes
45
from ganeti import http
46
from ganeti import constants
47
from ganeti import cli
48
from ganeti import rapi
49
from ganeti import ht
50
from ganeti import compat
51
from ganeti.rapi import baserlib
52

    
53

    
54
_COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
55
I_FIELDS = ["name", "admin_state", "os",
56
            "pnode", "snodes",
57
            "disk_template",
58
            "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
59
            "network_port",
60
            "disk.sizes", "disk_usage",
61
            "beparams", "hvparams",
62
            "oper_state", "oper_ram", "oper_vcpus", "status",
63
            "custom_hvparams", "custom_beparams", "custom_nicparams",
64
            ] + _COMMON_FIELDS
65

    
66
N_FIELDS = ["name", "offline", "master_candidate", "drained",
67
            "dtotal", "dfree",
68
            "mtotal", "mnode", "mfree",
69
            "pinst_cnt", "sinst_cnt",
70
            "ctotal", "cnodes", "csockets",
71
            "pip", "sip", "role",
72
            "pinst_list", "sinst_list",
73
            "master_capable", "vm_capable",
74
            "group.uuid",
75
            ] + _COMMON_FIELDS
76

    
77
G_FIELDS = ["name", "uuid",
78
            "alloc_policy",
79
            "node_cnt", "node_list",
80
            "ctime", "mtime", "serial_no",
81
            ]  # "tags" is missing to be able to use _COMMON_FIELDS here.
82

    
83
J_FIELDS = [
84
  "id", "ops", "status", "summary",
85
  "opstatus", "opresult", "oplog",
86
  "received_ts", "start_ts", "end_ts",
87
  ]
88

    
89
_NR_DRAINED = "drained"
90
_NR_MASTER_CANDIATE = "master-candidate"
91
_NR_MASTER = "master"
92
_NR_OFFLINE = "offline"
93
_NR_REGULAR = "regular"
94

    
95
_NR_MAP = {
96
  constants.NR_MASTER: _NR_MASTER,
97
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIATE,
98
  constants.NR_DRAINED: _NR_DRAINED,
99
  constants.NR_OFFLINE: _NR_OFFLINE,
100
  constants.NR_REGULAR: _NR_REGULAR,
101
  }
102

    
103
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
104

    
105
# Request data version field
106
_REQ_DATA_VERSION = "__version__"
107

    
108
# Feature string for instance creation request data version 1
109
_INST_CREATE_REQV1 = "instance-create-reqv1"
110

    
111
# Feature string for instance reinstall request version 1
112
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
113

    
114
# Feature string for node migration version 1
115
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
116

    
117
# Feature string for node evacuation with LU-generated jobs
118
_NODE_EVAC_RES1 = "node-evac-res1"
119

    
120
ALL_FEATURES = frozenset([
121
  _INST_CREATE_REQV1,
122
  _INST_REINSTALL_REQV1,
123
  _NODE_MIGRATE_REQV1,
124
  _NODE_EVAC_RES1,
125
  ])
126

    
127
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
128
_WFJC_TIMEOUT = 10
129

    
130

    
131
class R_version(baserlib.R_Generic):
132
  """/version resource.
133

134
  This resource should be used to determine the remote API version and
135
  to adapt clients accordingly.
136

137
  """
138
  @staticmethod
139
  def GET():
140
    """Returns the remote API version.
141

142
    """
143
    return constants.RAPI_VERSION
144

    
145

    
146
class R_2_info(baserlib.R_Generic):
147
  """/2/info resource.
148

149
  """
150
  @staticmethod
151
  def GET():
152
    """Returns cluster information.
153

154
    """
155
    client = baserlib.GetClient()
156
    return client.QueryClusterInfo()
157

    
158

    
159
class R_2_features(baserlib.R_Generic):
160
  """/2/features resource.
161

162
  """
163
  @staticmethod
164
  def GET():
165
    """Returns list of optional RAPI features implemented.
166

167
    """
168
    return list(ALL_FEATURES)
169

    
170

    
171
class R_2_os(baserlib.R_Generic):
172
  """/2/os resource.
173

174
  """
175
  @staticmethod
176
  def GET():
177
    """Return a list of all OSes.
178

179
    Can return error 500 in case of a problem.
180

181
    Example: ["debian-etch"]
182

183
    """
184
    cl = baserlib.GetClient()
185
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
186
    job_id = baserlib.SubmitJob([op], cl)
187
    # we use custom feedback function, instead of print we log the status
188
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
189
    diagnose_data = result[0]
190

    
191
    if not isinstance(diagnose_data, list):
192
      raise http.HttpBadGateway(message="Can't get OS list")
193

    
194
    os_names = []
195
    for (name, variants) in diagnose_data:
196
      os_names.extend(cli.CalculateOSNames(name, variants))
197

    
198
    return os_names
199

    
200

    
201
class R_2_redist_config(baserlib.R_Generic):
202
  """/2/redistribute-config resource.
203

204
  """
205
  @staticmethod
206
  def PUT():
207
    """Redistribute configuration to all nodes.
208

209
    """
210
    return baserlib.SubmitJob([opcodes.OpClusterRedistConf()])
211

    
212

    
213
class R_2_cluster_modify(baserlib.R_Generic):
214
  """/2/modify resource.
215

216
  """
217
  def PUT(self):
218
    """Modifies cluster parameters.
219

220
    @return: a job id
221

222
    """
223
    op = baserlib.FillOpcode(opcodes.OpClusterSetParams, self.request_body,
224
                             None)
225

    
226
    return baserlib.SubmitJob([op])
227

    
228

    
229
class R_2_jobs(baserlib.R_Generic):
230
  """/2/jobs resource.
231

232
  """
233
  def GET(self):
234
    """Returns a dictionary of jobs.
235

236
    @return: a dictionary with jobs id and uri.
237

238
    """
239
    client = baserlib.GetClient()
240

    
241
    if self.useBulk():
242
      bulkdata = client.QueryJobs(None, J_FIELDS)
243
      return baserlib.MapBulkFields(bulkdata, J_FIELDS)
244
    else:
245
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
246
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
247
                                   uri_fields=("id", "uri"))
248

    
249

    
250
class R_2_jobs_id(baserlib.R_Generic):
251
  """/2/jobs/[job_id] resource.
252

253
  """
254
  def GET(self):
255
    """Returns a job status.
256

257
    @return: a dictionary with job parameters.
258
        The result includes:
259
            - id: job ID as a number
260
            - status: current job status as a string
261
            - ops: involved OpCodes as a list of dictionaries for each
262
              opcodes in the job
263
            - opstatus: OpCodes status as a list
264
            - opresult: OpCodes results as a list of lists
265

266
    """
267
    job_id = self.items[0]
268
    result = baserlib.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
269
    if result is None:
270
      raise http.HttpNotFound()
271
    return baserlib.MapFields(J_FIELDS, result)
272

    
273
  def DELETE(self):
274
    """Cancel not-yet-started job.
275

276
    """
277
    job_id = self.items[0]
278
    result = baserlib.GetClient().CancelJob(job_id)
279
    return result
280

    
281

    
282
class R_2_jobs_id_wait(baserlib.R_Generic):
283
  """/2/jobs/[job_id]/wait resource.
284

285
  """
286
  # WaitForJobChange provides access to sensitive information and blocks
287
  # machine resources (it's a blocking RAPI call), hence restricting access.
288
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
289

    
290
  def GET(self):
291
    """Waits for job changes.
292

293
    """
294
    job_id = self.items[0]
295

    
296
    fields = self.getBodyParameter("fields")
297
    prev_job_info = self.getBodyParameter("previous_job_info", None)
298
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
299

    
300
    if not isinstance(fields, list):
301
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
302

    
303
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
304
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
305
                                " be a list")
306

    
307
    if not (prev_log_serial is None or
308
            isinstance(prev_log_serial, (int, long))):
309
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
310
                                " be a number")
311

    
312
    client = baserlib.GetClient()
313
    result = client.WaitForJobChangeOnce(job_id, fields,
314
                                         prev_job_info, prev_log_serial,
315
                                         timeout=_WFJC_TIMEOUT)
316
    if not result:
317
      raise http.HttpNotFound()
318

    
319
    if result == constants.JOB_NOTCHANGED:
320
      # No changes
321
      return None
322

    
323
    (job_info, log_entries) = result
324

    
325
    return {
326
      "job_info": job_info,
327
      "log_entries": log_entries,
328
      }
329

    
330

    
331
class R_2_nodes(baserlib.R_Generic):
332
  """/2/nodes resource.
333

334
  """
335
  def GET(self):
336
    """Returns a list of all nodes.
337

338
    """
339
    client = baserlib.GetClient()
340

    
341
    if self.useBulk():
342
      bulkdata = client.QueryNodes([], N_FIELDS, False)
343
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
344
    else:
345
      nodesdata = client.QueryNodes([], ["name"], False)
346
      nodeslist = [row[0] for row in nodesdata]
347
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
348
                                   uri_fields=("id", "uri"))
349

    
350

    
351
class R_2_nodes_name(baserlib.R_Generic):
352
  """/2/nodes/[node_name] resource.
353

354
  """
355
  def GET(self):
356
    """Send information about a node.
357

358
    """
359
    node_name = self.items[0]
360
    client = baserlib.GetClient()
361

    
362
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
363
                                            names=[node_name], fields=N_FIELDS,
364
                                            use_locking=self.useLocking())
365

    
366
    return baserlib.MapFields(N_FIELDS, result[0])
367

    
368

    
369
class R_2_nodes_name_role(baserlib.R_Generic):
370
  """ /2/nodes/[node_name]/role resource.
371

372
  """
373
  def GET(self):
374
    """Returns the current node role.
375

376
    @return: Node role
377

378
    """
379
    node_name = self.items[0]
380
    client = baserlib.GetClient()
381
    result = client.QueryNodes(names=[node_name], fields=["role"],
382
                               use_locking=self.useLocking())
383

    
384
    return _NR_MAP[result[0][0]]
385

    
386
  def PUT(self):
387
    """Sets the node role.
388

389
    @return: a job id
390

391
    """
392
    if not isinstance(self.request_body, basestring):
393
      raise http.HttpBadRequest("Invalid body contents, not a string")
394

    
395
    node_name = self.items[0]
396
    role = self.request_body
397

    
398
    if role == _NR_REGULAR:
399
      candidate = False
400
      offline = False
401
      drained = False
402

    
403
    elif role == _NR_MASTER_CANDIATE:
404
      candidate = True
405
      offline = drained = None
406

    
407
    elif role == _NR_DRAINED:
408
      drained = True
409
      candidate = offline = None
410

    
411
    elif role == _NR_OFFLINE:
412
      offline = True
413
      candidate = drained = None
414

    
415
    else:
416
      raise http.HttpBadRequest("Can't set '%s' role" % role)
417

    
418
    op = opcodes.OpNodeSetParams(node_name=node_name,
419
                                 master_candidate=candidate,
420
                                 offline=offline,
421
                                 drained=drained,
422
                                 force=bool(self.useForce()))
423

    
424
    return baserlib.SubmitJob([op])
425

    
426

    
427
class R_2_nodes_name_evacuate(baserlib.R_Generic):
428
  """/2/nodes/[node_name]/evacuate resource.
429

430
  """
431
  def POST(self):
432
    """Evacuate all instances off a node.
433

434
    """
435
    op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, {
436
      "node_name": self.items[0],
437
      "dry_run": self.dryRun(),
438
      })
439

    
440
    return baserlib.SubmitJob([op])
441

    
442

    
443
class R_2_nodes_name_migrate(baserlib.R_Generic):
444
  """/2/nodes/[node_name]/migrate resource.
445

446
  """
447
  def POST(self):
448
    """Migrate all primary instances from a node.
449

450
    """
451
    node_name = self.items[0]
452

    
453
    if self.queryargs:
454
      # Support old-style requests
455
      if "live" in self.queryargs and "mode" in self.queryargs:
456
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
457
                                  " be passed")
458

    
459
      if "live" in self.queryargs:
460
        if self._checkIntVariable("live", default=1):
461
          mode = constants.HT_MIGRATION_LIVE
462
        else:
463
          mode = constants.HT_MIGRATION_NONLIVE
464
      else:
465
        mode = self._checkStringVariable("mode", default=None)
466

    
467
      data = {
468
        "mode": mode,
469
        }
470
    else:
471
      data = self.request_body
472

    
473
    op = baserlib.FillOpcode(opcodes.OpNodeMigrate, data, {
474
      "node_name": node_name,
475
      })
476

    
477
    return baserlib.SubmitJob([op])
478

    
479

    
480
class R_2_nodes_name_storage(baserlib.R_Generic):
481
  """/2/nodes/[node_name]/storage resource.
482

483
  """
484
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
485
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
486

    
487
  def GET(self):
488
    node_name = self.items[0]
489

    
490
    storage_type = self._checkStringVariable("storage_type", None)
491
    if not storage_type:
492
      raise http.HttpBadRequest("Missing the required 'storage_type'"
493
                                " parameter")
494

    
495
    output_fields = self._checkStringVariable("output_fields", None)
496
    if not output_fields:
497
      raise http.HttpBadRequest("Missing the required 'output_fields'"
498
                                " parameter")
499

    
500
    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
501
                                    storage_type=storage_type,
502
                                    output_fields=output_fields.split(","))
503
    return baserlib.SubmitJob([op])
504

    
505

    
506
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
507
  """/2/nodes/[node_name]/storage/modify resource.
508

509
  """
510
  def PUT(self):
511
    node_name = self.items[0]
512

    
513
    storage_type = self._checkStringVariable("storage_type", None)
514
    if not storage_type:
515
      raise http.HttpBadRequest("Missing the required 'storage_type'"
516
                                " parameter")
517

    
518
    name = self._checkStringVariable("name", None)
519
    if not name:
520
      raise http.HttpBadRequest("Missing the required 'name'"
521
                                " parameter")
522

    
523
    changes = {}
524

    
525
    if "allocatable" in self.queryargs:
526
      changes[constants.SF_ALLOCATABLE] = \
527
        bool(self._checkIntVariable("allocatable", default=1))
528

    
529
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
530
                                     storage_type=storage_type,
531
                                     name=name,
532
                                     changes=changes)
533
    return baserlib.SubmitJob([op])
534

    
535

    
536
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
537
  """/2/nodes/[node_name]/storage/repair resource.
538

539
  """
540
  def PUT(self):
541
    node_name = self.items[0]
542

    
543
    storage_type = self._checkStringVariable("storage_type", None)
544
    if not storage_type:
545
      raise http.HttpBadRequest("Missing the required 'storage_type'"
546
                                " parameter")
547

    
548
    name = self._checkStringVariable("name", None)
549
    if not name:
550
      raise http.HttpBadRequest("Missing the required 'name'"
551
                                " parameter")
552

    
553
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
554
                                     storage_type=storage_type,
555
                                     name=name)
556
    return baserlib.SubmitJob([op])
557

    
558

    
559
def _ParseCreateGroupRequest(data, dry_run):
560
  """Parses a request for creating a node group.
561

562
  @rtype: L{opcodes.OpGroupAdd}
563
  @return: Group creation opcode
564

565
  """
566
  override = {
567
    "dry_run": dry_run,
568
    }
569

    
570
  rename = {
571
    "name": "group_name",
572
    }
573

    
574
  return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
575
                             rename=rename)
576

    
577

    
578
class R_2_groups(baserlib.R_Generic):
579
  """/2/groups resource.
580

581
  """
582
  def GET(self):
583
    """Returns a list of all node groups.
584

585
    """
586
    client = baserlib.GetClient()
587

    
588
    if self.useBulk():
589
      bulkdata = client.QueryGroups([], G_FIELDS, False)
590
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
591
    else:
592
      data = client.QueryGroups([], ["name"], False)
593
      groupnames = [row[0] for row in data]
594
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
595
                                   uri_fields=("name", "uri"))
596

    
597
  def POST(self):
598
    """Create a node group.
599

600
    @return: a job id
601

602
    """
603
    baserlib.CheckType(self.request_body, dict, "Body contents")
604
    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
605
    return baserlib.SubmitJob([op])
606

    
607

    
608
class R_2_groups_name(baserlib.R_Generic):
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 = baserlib.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 baserlib.SubmitJob([op])
633

    
634

    
635
def _ParseModifyGroupRequest(name, data):
636
  """Parses a request for modifying a node group.
637

638
  @rtype: L{opcodes.OpGroupSetParams}
639
  @return: Group modify opcode
640

641
  """
642
  return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
643
    "group_name": name,
644
    })
645

    
646

    
647

    
648
class R_2_groups_name_modify(baserlib.R_Generic):
649
  """/2/groups/[group_name]/modify resource.
650

651
  """
652
  def PUT(self):
653
    """Changes some parameters of node group.
654

655
    @return: a job id
656

657
    """
658
    baserlib.CheckType(self.request_body, dict, "Body contents")
659

    
660
    op = _ParseModifyGroupRequest(self.items[0], self.request_body)
661

    
662
    return baserlib.SubmitJob([op])
663

    
664

    
665
def _ParseRenameGroupRequest(name, data, dry_run):
666
  """Parses a request for renaming a node group.
667

668
  @type name: string
669
  @param name: name of the node group to rename
670
  @type data: dict
671
  @param data: the body received by the rename request
672
  @type dry_run: bool
673
  @param dry_run: whether to perform a dry run
674

675
  @rtype: L{opcodes.OpGroupRename}
676
  @return: Node group rename opcode
677

678
  """
679
  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
680
    "group_name": name,
681
    "dry_run": dry_run,
682
    })
683

    
684

    
685
class R_2_groups_name_rename(baserlib.R_Generic):
686
  """/2/groups/[group_name]/rename resource.
687

688
  """
689
  def PUT(self):
690
    """Changes the name of a node group.
691

692
    @return: a job id
693

694
    """
695
    baserlib.CheckType(self.request_body, dict, "Body contents")
696
    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
697
                                  self.dryRun())
698
    return baserlib.SubmitJob([op])
699

    
700

    
701
class R_2_groups_name_assign_nodes(baserlib.R_Generic):
702
  """/2/groups/[group_name]/assign-nodes resource.
703

704
  """
705
  def PUT(self):
706
    """Assigns nodes to a group.
707

708
    @return: a job id
709

710
    """
711
    op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
712
      "group_name": self.items[0],
713
      "dry_run": self.dryRun(),
714
      "force": self.useForce(),
715
      })
716

    
717
    return baserlib.SubmitJob([op])
718

    
719

    
720
def _ParseInstanceCreateRequestVersion1(data, dry_run):
721
  """Parses an instance creation request version 1.
722

723
  @rtype: L{opcodes.OpInstanceCreate}
724
  @return: Instance creation opcode
725

726
  """
727
  override = {
728
    "dry_run": dry_run,
729
    }
730

    
731
  rename = {
732
    "os": "os_type",
733
    "name": "instance_name",
734
    }
735

    
736
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
737
                             rename=rename)
738

    
739

    
740
class R_2_instances(baserlib.R_Generic):
741
  """/2/instances resource.
742

743
  """
744
  def GET(self):
745
    """Returns a list of all available instances.
746

747
    """
748
    client = baserlib.GetClient()
749

    
750
    use_locking = self.useLocking()
751
    if self.useBulk():
752
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
753
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
754
    else:
755
      instancesdata = client.QueryInstances([], ["name"], use_locking)
756
      instanceslist = [row[0] for row in instancesdata]
757
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
758
                                   uri_fields=("id", "uri"))
759

    
760
  def POST(self):
761
    """Create an instance.
762

763
    @return: a job id
764

765
    """
766
    if not isinstance(self.request_body, dict):
767
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
768

    
769
    # Default to request data version 0
770
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
771

    
772
    if data_version == 0:
773
      raise http.HttpBadRequest("Instance creation request version 0 is no"
774
                                " longer supported")
775
    elif data_version == 1:
776
      data = self.request_body.copy()
777
      # Remove "__version__"
778
      data.pop(_REQ_DATA_VERSION, None)
779
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
780
    else:
781
      raise http.HttpBadRequest("Unsupported request data version %s" %
782
                                data_version)
783

    
784
    return baserlib.SubmitJob([op])
785

    
786

    
787
class R_2_instances_name(baserlib.R_Generic):
788
  """/2/instances/[instance_name] resource.
789

790
  """
791
  def GET(self):
792
    """Send information about an instance.
793

794
    """
795
    client = baserlib.GetClient()
796
    instance_name = self.items[0]
797

    
798
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
799
                                            names=[instance_name],
800
                                            fields=I_FIELDS,
801
                                            use_locking=self.useLocking())
802

    
803
    return baserlib.MapFields(I_FIELDS, result[0])
804

    
805
  def DELETE(self):
806
    """Delete an instance.
807

808
    """
809
    op = opcodes.OpInstanceRemove(instance_name=self.items[0],
810
                                  ignore_failures=False,
811
                                  dry_run=bool(self.dryRun()))
812
    return baserlib.SubmitJob([op])
813

    
814

    
815
class R_2_instances_name_info(baserlib.R_Generic):
816
  """/2/instances/[instance_name]/info resource.
817

818
  """
819
  def GET(self):
820
    """Request detailed instance information.
821

822
    """
823
    instance_name = self.items[0]
824
    static = bool(self._checkIntVariable("static", default=0))
825

    
826
    op = opcodes.OpInstanceQueryData(instances=[instance_name],
827
                                     static=static)
828
    return baserlib.SubmitJob([op])
829

    
830

    
831
class R_2_instances_name_reboot(baserlib.R_Generic):
832
  """/2/instances/[instance_name]/reboot resource.
833

834
  Implements an instance reboot.
835

836
  """
837
  def POST(self):
838
    """Reboot an instance.
839

840
    The URI takes type=[hard|soft|full] and
841
    ignore_secondaries=[False|True] parameters.
842

843
    """
844
    instance_name = self.items[0]
845
    reboot_type = self.queryargs.get("type",
846
                                     [constants.INSTANCE_REBOOT_HARD])[0]
847
    ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
848
    op = opcodes.OpInstanceReboot(instance_name=instance_name,
849
                                  reboot_type=reboot_type,
850
                                  ignore_secondaries=ignore_secondaries,
851
                                  dry_run=bool(self.dryRun()))
852

    
853
    return baserlib.SubmitJob([op])
854

    
855

    
856
class R_2_instances_name_startup(baserlib.R_Generic):
857
  """/2/instances/[instance_name]/startup resource.
858

859
  Implements an instance startup.
860

861
  """
862
  def PUT(self):
863
    """Startup an instance.
864

865
    The URI takes force=[False|True] parameter to start the instance
866
    if even if secondary disks are failing.
867

868
    """
869
    instance_name = self.items[0]
870
    force_startup = bool(self._checkIntVariable("force"))
871
    no_remember = bool(self._checkIntVariable("no_remember"))
872
    op = opcodes.OpInstanceStartup(instance_name=instance_name,
873
                                   force=force_startup,
874
                                   dry_run=bool(self.dryRun()),
875
                                   no_remember=no_remember)
876

    
877
    return baserlib.SubmitJob([op])
878

    
879

    
880
def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
881
  """Parses a request for an instance shutdown.
882

883
  @rtype: L{opcodes.OpInstanceShutdown}
884
  @return: Instance shutdown opcode
885

886
  """
887
  return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
888
    "instance_name": name,
889
    "dry_run": dry_run,
890
    "no_remember": no_remember,
891
    })
892

    
893

    
894
class R_2_instances_name_shutdown(baserlib.R_Generic):
895
  """/2/instances/[instance_name]/shutdown resource.
896

897
  Implements an instance shutdown.
898

899
  """
900
  def PUT(self):
901
    """Shutdown an instance.
902

903
    @return: a job id
904

905
    """
906
    baserlib.CheckType(self.request_body, dict, "Body contents")
907

    
908
    no_remember = bool(self._checkIntVariable("no_remember"))
909
    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
910
                                       bool(self.dryRun()), no_remember)
911

    
912
    return baserlib.SubmitJob([op])
913

    
914

    
915
def _ParseInstanceReinstallRequest(name, data):
916
  """Parses a request for reinstalling an instance.
917

918
  """
919
  if not isinstance(data, dict):
920
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
921

    
922
  ostype = baserlib.CheckParameter(data, "os", default=None)
923
  start = baserlib.CheckParameter(data, "start", exptype=bool,
924
                                  default=True)
925
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
926

    
927
  ops = [
928
    opcodes.OpInstanceShutdown(instance_name=name),
929
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
930
                                osparams=osparams),
931
    ]
932

    
933
  if start:
934
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
935

    
936
  return ops
937

    
938

    
939
class R_2_instances_name_reinstall(baserlib.R_Generic):
940
  """/2/instances/[instance_name]/reinstall resource.
941

942
  Implements an instance reinstall.
943

944
  """
945
  def POST(self):
946
    """Reinstall an instance.
947

948
    The URI takes os=name and nostartup=[0|1] optional
949
    parameters. By default, the instance will be started
950
    automatically.
951

952
    """
953
    if self.request_body:
954
      if self.queryargs:
955
        raise http.HttpBadRequest("Can't combine query and body parameters")
956

    
957
      body = self.request_body
958
    elif self.queryargs:
959
      # Legacy interface, do not modify/extend
960
      body = {
961
        "os": self._checkStringVariable("os"),
962
        "start": not self._checkIntVariable("nostartup"),
963
        }
964
    else:
965
      body = {}
966

    
967
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
968

    
969
    return baserlib.SubmitJob(ops)
970

    
971

    
972
def _ParseInstanceReplaceDisksRequest(name, data):
973
  """Parses a request for an instance export.
974

975
  @rtype: L{opcodes.OpInstanceReplaceDisks}
976
  @return: Instance export opcode
977

978
  """
979
  override = {
980
    "instance_name": name,
981
    }
982

    
983
  # Parse disks
984
  try:
985
    raw_disks = data["disks"]
986
  except KeyError:
987
    pass
988
  else:
989
    if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
990
      # Backwards compatibility for strings of the format "1, 2, 3"
991
      try:
992
        data["disks"] = [int(part) for part in raw_disks.split(",")]
993
      except (TypeError, ValueError), err:
994
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
995

    
996
  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
997

    
998

    
999
class R_2_instances_name_replace_disks(baserlib.R_Generic):
1000
  """/2/instances/[instance_name]/replace-disks resource.
1001

1002
  """
1003
  def POST(self):
1004
    """Replaces disks on an instance.
1005

1006
    """
1007
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1008

    
1009
    return baserlib.SubmitJob([op])
1010

    
1011

    
1012
class R_2_instances_name_activate_disks(baserlib.R_Generic):
1013
  """/2/instances/[instance_name]/activate-disks resource.
1014

1015
  """
1016
  def PUT(self):
1017
    """Activate disks for an instance.
1018

1019
    The URI might contain ignore_size to ignore current recorded size.
1020

1021
    """
1022
    instance_name = self.items[0]
1023
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1024

    
1025
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1026
                                         ignore_size=ignore_size)
1027

    
1028
    return baserlib.SubmitJob([op])
1029

    
1030

    
1031
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1032
  """/2/instances/[instance_name]/deactivate-disks resource.
1033

1034
  """
1035
  def PUT(self):
1036
    """Deactivate disks for an instance.
1037

1038
    """
1039
    instance_name = self.items[0]
1040

    
1041
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1042

    
1043
    return baserlib.SubmitJob([op])
1044

    
1045

    
1046
class R_2_instances_name_prepare_export(baserlib.R_Generic):
1047
  """/2/instances/[instance_name]/prepare-export resource.
1048

1049
  """
1050
  def PUT(self):
1051
    """Prepares an export for an instance.
1052

1053
    @return: a job id
1054

1055
    """
1056
    instance_name = self.items[0]
1057
    mode = self._checkStringVariable("mode")
1058

    
1059
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1060
                                 mode=mode)
1061

    
1062
    return baserlib.SubmitJob([op])
1063

    
1064

    
1065
def _ParseExportInstanceRequest(name, data):
1066
  """Parses a request for an instance export.
1067

1068
  @rtype: L{opcodes.OpBackupExport}
1069
  @return: Instance export opcode
1070

1071
  """
1072
  # Rename "destination" to "target_node"
1073
  try:
1074
    data["target_node"] = data.pop("destination")
1075
  except KeyError:
1076
    pass
1077

    
1078
  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1079
    "instance_name": name,
1080
    })
1081

    
1082

    
1083
class R_2_instances_name_export(baserlib.R_Generic):
1084
  """/2/instances/[instance_name]/export resource.
1085

1086
  """
1087
  def PUT(self):
1088
    """Exports an instance.
1089

1090
    @return: a job id
1091

1092
    """
1093
    if not isinstance(self.request_body, dict):
1094
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1095

    
1096
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1097

    
1098
    return baserlib.SubmitJob([op])
1099

    
1100

    
1101
def _ParseMigrateInstanceRequest(name, data):
1102
  """Parses a request for an instance migration.
1103

1104
  @rtype: L{opcodes.OpInstanceMigrate}
1105
  @return: Instance migration opcode
1106

1107
  """
1108
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1109
    "instance_name": name,
1110
    })
1111

    
1112

    
1113
class R_2_instances_name_migrate(baserlib.R_Generic):
1114
  """/2/instances/[instance_name]/migrate resource.
1115

1116
  """
1117
  def PUT(self):
1118
    """Migrates an instance.
1119

1120
    @return: a job id
1121

1122
    """
1123
    baserlib.CheckType(self.request_body, dict, "Body contents")
1124

    
1125
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1126

    
1127
    return baserlib.SubmitJob([op])
1128

    
1129

    
1130
class R_2_instances_name_failover(baserlib.R_Generic):
1131
  """/2/instances/[instance_name]/failover resource.
1132

1133
  """
1134
  def PUT(self):
1135
    """Does a failover of an instance.
1136

1137
    @return: a job id
1138

1139
    """
1140
    baserlib.CheckType(self.request_body, dict, "Body contents")
1141

    
1142
    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1143
      "instance_name": self.items[0],
1144
      })
1145

    
1146
    return baserlib.SubmitJob([op])
1147

    
1148

    
1149
def _ParseRenameInstanceRequest(name, data):
1150
  """Parses a request for renaming an instance.
1151

1152
  @rtype: L{opcodes.OpInstanceRename}
1153
  @return: Instance rename opcode
1154

1155
  """
1156
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1157
    "instance_name": name,
1158
    })
1159

    
1160

    
1161
class R_2_instances_name_rename(baserlib.R_Generic):
1162
  """/2/instances/[instance_name]/rename resource.
1163

1164
  """
1165
  def PUT(self):
1166
    """Changes the name of an instance.
1167

1168
    @return: a job id
1169

1170
    """
1171
    baserlib.CheckType(self.request_body, dict, "Body contents")
1172

    
1173
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1174

    
1175
    return baserlib.SubmitJob([op])
1176

    
1177

    
1178
def _ParseModifyInstanceRequest(name, data):
1179
  """Parses a request for modifying an instance.
1180

1181
  @rtype: L{opcodes.OpInstanceSetParams}
1182
  @return: Instance modify opcode
1183

1184
  """
1185
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1186
    "instance_name": name,
1187
    })
1188

    
1189

    
1190
class R_2_instances_name_modify(baserlib.R_Generic):
1191
  """/2/instances/[instance_name]/modify resource.
1192

1193
  """
1194
  def PUT(self):
1195
    """Changes some parameters of an instance.
1196

1197
    @return: a job id
1198

1199
    """
1200
    baserlib.CheckType(self.request_body, dict, "Body contents")
1201

    
1202
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1203

    
1204
    return baserlib.SubmitJob([op])
1205

    
1206

    
1207
class R_2_instances_name_disk_grow(baserlib.R_Generic):
1208
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1209

1210
  """
1211
  def POST(self):
1212
    """Increases the size of an instance disk.
1213

1214
    @return: a job id
1215

1216
    """
1217
    op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1218
      "instance_name": self.items[0],
1219
      "disk": int(self.items[1]),
1220
      })
1221

    
1222
    return baserlib.SubmitJob([op])
1223

    
1224

    
1225
class R_2_instances_name_console(baserlib.R_Generic):
1226
  """/2/instances/[instance_name]/console resource.
1227

1228
  """
1229
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1230

    
1231
  def GET(self):
1232
    """Request information for connecting to instance's console.
1233

1234
    @return: Serialized instance console description, see
1235
             L{objects.InstanceConsole}
1236

1237
    """
1238
    client = baserlib.GetClient()
1239

    
1240
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1241

    
1242
    if console is None:
1243
      raise http.HttpServiceUnavailable("Instance console unavailable")
1244

    
1245
    assert isinstance(console, dict)
1246
    return console
1247

    
1248

    
1249
def _GetQueryFields(args):
1250
  """
1251

1252
  """
1253
  try:
1254
    fields = args["fields"]
1255
  except KeyError:
1256
    raise http.HttpBadRequest("Missing 'fields' query argument")
1257

    
1258
  return _SplitQueryFields(fields[0])
1259

    
1260

    
1261
def _SplitQueryFields(fields):
1262
  """
1263

1264
  """
1265
  return [i.strip() for i in fields.split(",")]
1266

    
1267

    
1268
class R_2_query(baserlib.R_Generic):
1269
  """/2/query/[resource] resource.
1270

1271
  """
1272
  # Results might contain sensitive information
1273
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1274

    
1275
  def _Query(self, fields, filter_):
1276
    return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1277

    
1278
  def GET(self):
1279
    """Returns resource information.
1280

1281
    @return: Query result, see L{objects.QueryResponse}
1282

1283
    """
1284
    return self._Query(_GetQueryFields(self.queryargs), None)
1285

    
1286
  def PUT(self):
1287
    """Submits job querying for resources.
1288

1289
    @return: Query result, see L{objects.QueryResponse}
1290

1291
    """
1292
    body = self.request_body
1293

    
1294
    baserlib.CheckType(body, dict, "Body contents")
1295

    
1296
    try:
1297
      fields = body["fields"]
1298
    except KeyError:
1299
      fields = _GetQueryFields(self.queryargs)
1300

    
1301
    return self._Query(fields, self.request_body.get("filter", None))
1302

    
1303

    
1304
class R_2_query_fields(baserlib.R_Generic):
1305
  """/2/query/[resource]/fields resource.
1306

1307
  """
1308
  def GET(self):
1309
    """Retrieves list of available fields for a resource.
1310

1311
    @return: List of serialized L{objects.QueryFieldDefinition}
1312

1313
    """
1314
    try:
1315
      raw_fields = self.queryargs["fields"]
1316
    except KeyError:
1317
      fields = None
1318
    else:
1319
      fields = _SplitQueryFields(raw_fields[0])
1320

    
1321
    return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1322

    
1323

    
1324
class _R_Tags(baserlib.R_Generic):
1325
  """ Quasiclass for tagging resources
1326

1327
  Manages tags. When inheriting this class you must define the
1328
  TAG_LEVEL for it.
1329

1330
  """
1331
  TAG_LEVEL = None
1332

    
1333
  def __init__(self, items, queryargs, req):
1334
    """A tag resource constructor.
1335

1336
    We have to override the default to sort out cluster naming case.
1337

1338
    """
1339
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1340

    
1341
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1342
      self.name = None
1343
    else:
1344
      self.name = items[0]
1345

    
1346
  def GET(self):
1347
    """Returns a list of tags.
1348

1349
    Example: ["tag1", "tag2", "tag3"]
1350

1351
    """
1352
    # pylint: disable-msg=W0212
1353
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1354

    
1355
  def PUT(self):
1356
    """Add a set of tags.
1357

1358
    The request as a list of strings should be PUT to this URI. And
1359
    you'll have back a job id.
1360

1361
    """
1362
    # pylint: disable-msg=W0212
1363
    if "tag" not in self.queryargs:
1364
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1365
                                " the 'tag' parameter")
1366
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1367
                              self.queryargs["tag"], name=self.name,
1368
                              dry_run=bool(self.dryRun()))
1369

    
1370
  def DELETE(self):
1371
    """Delete a tag.
1372

1373
    In order to delete a set of tags, the DELETE
1374
    request should be addressed to URI like:
1375
    /tags?tag=[tag]&tag=[tag]
1376

1377
    """
1378
    # pylint: disable-msg=W0212
1379
    if "tag" not in self.queryargs:
1380
      # no we not gonna delete all tags
1381
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1382
                                " tag(s) using the 'tag' parameter")
1383
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1384
                                 self.queryargs["tag"],
1385
                                 name=self.name,
1386
                                 dry_run=bool(self.dryRun()))
1387

    
1388

    
1389
class R_2_instances_name_tags(_R_Tags):
1390
  """ /2/instances/[instance_name]/tags resource.
1391

1392
  Manages per-instance tags.
1393

1394
  """
1395
  TAG_LEVEL = constants.TAG_INSTANCE
1396

    
1397

    
1398
class R_2_nodes_name_tags(_R_Tags):
1399
  """ /2/nodes/[node_name]/tags resource.
1400

1401
  Manages per-node tags.
1402

1403
  """
1404
  TAG_LEVEL = constants.TAG_NODE
1405

    
1406

    
1407
class R_2_groups_name_tags(_R_Tags):
1408
  """ /2/groups/[group_name]/tags resource.
1409

1410
  Manages per-nodegroup tags.
1411

1412
  """
1413
  TAG_LEVEL = constants.TAG_NODEGROUP
1414

    
1415

    
1416
class R_2_tags(_R_Tags):
1417
  """ /2/tags resource.
1418

1419
  Manages cluster tags.
1420

1421
  """
1422
  TAG_LEVEL = constants.TAG_CLUSTER