Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 52194140

History | View | Annotate | Download (31.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010 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 utils
49
from ganeti import rapi
50
from ganeti.rapi import baserlib
51

    
52

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

    
64
N_FIELDS = ["name", "offline", "master_candidate", "drained",
65
            "dtotal", "dfree",
66
            "mtotal", "mnode", "mfree",
67
            "pinst_cnt", "sinst_cnt",
68
            "ctotal", "cnodes", "csockets",
69
            "pip", "sip", "role",
70
            "pinst_list", "sinst_list",
71
            ] + _COMMON_FIELDS
72

    
73
_NR_DRAINED = "drained"
74
_NR_MASTER_CANDIATE = "master-candidate"
75
_NR_MASTER = "master"
76
_NR_OFFLINE = "offline"
77
_NR_REGULAR = "regular"
78

    
79
_NR_MAP = {
80
  "M": _NR_MASTER,
81
  "C": _NR_MASTER_CANDIATE,
82
  "D": _NR_DRAINED,
83
  "O": _NR_OFFLINE,
84
  "R": _NR_REGULAR,
85
  }
86

    
87
# Request data version field
88
_REQ_DATA_VERSION = "__version__"
89

    
90
# Feature string for instance creation request data version 1
91
_INST_CREATE_REQV1 = "instance-create-reqv1"
92

    
93
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
94
_WFJC_TIMEOUT = 10
95

    
96

    
97
class R_version(baserlib.R_Generic):
98
  """/version resource.
99

100
  This resource should be used to determine the remote API version and
101
  to adapt clients accordingly.
102

103
  """
104
  @staticmethod
105
  def GET():
106
    """Returns the remote API version.
107

108
    """
109
    return constants.RAPI_VERSION
110

    
111

    
112
class R_2_info(baserlib.R_Generic):
113
  """Cluster info.
114

115
  """
116
  @staticmethod
117
  def GET():
118
    """Returns cluster information.
119

120
    """
121
    client = baserlib.GetClient()
122
    return client.QueryClusterInfo()
123

    
124

    
125
class R_2_features(baserlib.R_Generic):
126
  """/2/features resource.
127

128
  """
129
  @staticmethod
130
  def GET():
131
    """Returns list of optional RAPI features implemented.
132

133
    """
134
    return [_INST_CREATE_REQV1]
135

    
136

    
137
class R_2_os(baserlib.R_Generic):
138
  """/2/os resource.
139

140
  """
141
  @staticmethod
142
  def GET():
143
    """Return a list of all OSes.
144

145
    Can return error 500 in case of a problem.
146

147
    Example: ["debian-etch"]
148

149
    """
150
    cl = baserlib.GetClient()
151
    op = opcodes.OpDiagnoseOS(output_fields=["name", "valid", "variants"],
152
                              names=[])
153
    job_id = baserlib.SubmitJob([op], cl)
154
    # we use custom feedback function, instead of print we log the status
155
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
156
    diagnose_data = result[0]
157

    
158
    if not isinstance(diagnose_data, list):
159
      raise http.HttpBadGateway(message="Can't get OS list")
160

    
161
    os_names = []
162
    for (name, valid, variants) in diagnose_data:
163
      if valid:
164
        os_names.extend(cli.CalculateOSNames(name, variants))
165

    
166
    return os_names
167

    
168

    
169
class R_2_redist_config(baserlib.R_Generic):
170
  """/2/redistribute-config resource.
171

172
  """
173
  @staticmethod
174
  def PUT():
175
    """Redistribute configuration to all nodes.
176

177
    """
178
    return baserlib.SubmitJob([opcodes.OpRedistributeConfig()])
179

    
180

    
181
class R_2_jobs(baserlib.R_Generic):
182
  """/2/jobs resource.
183

184
  """
185
  @staticmethod
186
  def GET():
187
    """Returns a dictionary of jobs.
188

189
    @return: a dictionary with jobs id and uri.
190

191
    """
192
    fields = ["id"]
193
    cl = baserlib.GetClient()
194
    # Convert the list of lists to the list of ids
195
    result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
196
    return baserlib.BuildUriList(result, "/2/jobs/%s",
197
                                 uri_fields=("id", "uri"))
198

    
199

    
200
class R_2_jobs_id(baserlib.R_Generic):
201
  """/2/jobs/[job_id] resource.
202

203
  """
204
  def GET(self):
205
    """Returns a job status.
206

207
    @return: a dictionary with job parameters.
208
        The result includes:
209
            - id: job ID as a number
210
            - status: current job status as a string
211
            - ops: involved OpCodes as a list of dictionaries for each
212
              opcodes in the job
213
            - opstatus: OpCodes status as a list
214
            - opresult: OpCodes results as a list of lists
215

216
    """
217
    fields = ["id", "ops", "status", "summary",
218
              "opstatus", "opresult", "oplog",
219
              "received_ts", "start_ts", "end_ts",
220
              ]
221
    job_id = self.items[0]
222
    result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
223
    if result is None:
224
      raise http.HttpNotFound()
225
    return baserlib.MapFields(fields, result)
226

    
227
  def DELETE(self):
228
    """Cancel not-yet-started job.
229

230
    """
231
    job_id = self.items[0]
232
    result = baserlib.GetClient().CancelJob(job_id)
233
    return result
234

    
235

    
236
class R_2_jobs_id_wait(baserlib.R_Generic):
237
  """/2/jobs/[job_id]/wait resource.
238

239
  """
240
  # WaitForJobChange provides access to sensitive information and blocks
241
  # machine resources (it's a blocking RAPI call), hence restricting access.
242
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
243

    
244
  def GET(self):
245
    """Waits for job changes.
246

247
    """
248
    job_id = self.items[0]
249

    
250
    fields = self.getBodyParameter("fields")
251
    prev_job_info = self.getBodyParameter("previous_job_info", None)
252
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
253

    
254
    if not isinstance(fields, list):
255
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
256

    
257
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
258
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
259
                                " be a list")
260

    
261
    if not (prev_log_serial is None or
262
            isinstance(prev_log_serial, (int, long))):
263
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
264
                                " be a number")
265

    
266
    client = baserlib.GetClient()
267
    result = client.WaitForJobChangeOnce(job_id, fields,
268
                                         prev_job_info, prev_log_serial,
269
                                         timeout=_WFJC_TIMEOUT)
270
    if not result:
271
      raise http.HttpNotFound()
272

    
273
    if result == constants.JOB_NOTCHANGED:
274
      # No changes
275
      return None
276

    
277
    (job_info, log_entries) = result
278

    
279
    return {
280
      "job_info": job_info,
281
      "log_entries": log_entries,
282
      }
283

    
284

    
285
class R_2_nodes(baserlib.R_Generic):
286
  """/2/nodes resource.
287

288
  """
289
  def GET(self):
290
    """Returns a list of all nodes.
291

292
    """
293
    client = baserlib.GetClient()
294

    
295
    if self.useBulk():
296
      bulkdata = client.QueryNodes([], N_FIELDS, False)
297
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
298
    else:
299
      nodesdata = client.QueryNodes([], ["name"], False)
300
      nodeslist = [row[0] for row in nodesdata]
301
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
302
                                   uri_fields=("id", "uri"))
303

    
304

    
305
class R_2_nodes_name(baserlib.R_Generic):
306
  """/2/nodes/[node_name] resources.
307

308
  """
309
  def GET(self):
310
    """Send information about a node.
311

312
    """
313
    node_name = self.items[0]
314
    client = baserlib.GetClient()
315

    
316
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
317
                                            names=[node_name], fields=N_FIELDS,
318
                                            use_locking=self.useLocking())
319

    
320
    return baserlib.MapFields(N_FIELDS, result[0])
321

    
322

    
323
class R_2_nodes_name_role(baserlib.R_Generic):
324
  """ /2/nodes/[node_name]/role resource.
325

326
  """
327
  def GET(self):
328
    """Returns the current node role.
329

330
    @return: Node role
331

332
    """
333
    node_name = self.items[0]
334
    client = baserlib.GetClient()
335
    result = client.QueryNodes(names=[node_name], fields=["role"],
336
                               use_locking=self.useLocking())
337

    
338
    return _NR_MAP[result[0][0]]
339

    
340
  def PUT(self):
341
    """Sets the node role.
342

343
    @return: a job id
344

345
    """
346
    if not isinstance(self.request_body, basestring):
347
      raise http.HttpBadRequest("Invalid body contents, not a string")
348

    
349
    node_name = self.items[0]
350
    role = self.request_body
351

    
352
    if role == _NR_REGULAR:
353
      candidate = False
354
      offline = False
355
      drained = False
356

    
357
    elif role == _NR_MASTER_CANDIATE:
358
      candidate = True
359
      offline = drained = None
360

    
361
    elif role == _NR_DRAINED:
362
      drained = True
363
      candidate = offline = None
364

    
365
    elif role == _NR_OFFLINE:
366
      offline = True
367
      candidate = drained = None
368

    
369
    else:
370
      raise http.HttpBadRequest("Can't set '%s' role" % role)
371

    
372
    op = opcodes.OpSetNodeParams(node_name=node_name,
373
                                 master_candidate=candidate,
374
                                 offline=offline,
375
                                 drained=drained,
376
                                 force=bool(self.useForce()))
377

    
378
    return baserlib.SubmitJob([op])
379

    
380

    
381
class R_2_nodes_name_evacuate(baserlib.R_Generic):
382
  """/2/nodes/[node_name]/evacuate resource.
383

384
  """
385
  def POST(self):
386
    """Evacuate all secondary instances off a node.
387

388
    """
389
    node_name = self.items[0]
390
    remote_node = self._checkStringVariable("remote_node", default=None)
391
    iallocator = self._checkStringVariable("iallocator", default=None)
392
    early_r = bool(self._checkIntVariable("early_release", default=0))
393
    dry_run = bool(self.dryRun())
394

    
395
    cl = baserlib.GetClient()
396

    
397
    op = opcodes.OpNodeEvacuationStrategy(nodes=[node_name],
398
                                          iallocator=iallocator,
399
                                          remote_node=remote_node)
400

    
401
    job_id = baserlib.SubmitJob([op], cl)
402
    # we use custom feedback function, instead of print we log the status
403
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
404

    
405
    jobs = []
406
    for iname, node in result:
407
      if dry_run:
408
        jid = None
409
      else:
410
        op = opcodes.OpReplaceDisks(instance_name=iname,
411
                                    remote_node=node, disks=[],
412
                                    mode=constants.REPLACE_DISK_CHG,
413
                                    early_release=early_r)
414
        jid = baserlib.SubmitJob([op])
415
      jobs.append((jid, iname, node))
416

    
417
    return jobs
418

    
419

    
420
class R_2_nodes_name_migrate(baserlib.R_Generic):
421
  """/2/nodes/[node_name]/migrate resource.
422

423
  """
424
  def POST(self):
425
    """Migrate all primary instances from a node.
426

427
    """
428
    node_name = self.items[0]
429

    
430
    if "live" in self.queryargs and "mode" in self.queryargs:
431
      raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
432
                                " be passed")
433
    elif "live" in self.queryargs:
434
      if self._checkIntVariable("live", default=1):
435
        mode = constants.HT_MIGRATION_LIVE
436
      else:
437
        mode = constants.HT_MIGRATION_NONLIVE
438
    else:
439
      mode = self._checkStringVariable("mode", default=None)
440

    
441
    op = opcodes.OpMigrateNode(node_name=node_name, mode=mode)
442

    
443
    return baserlib.SubmitJob([op])
444

    
445

    
446
class R_2_nodes_name_storage(baserlib.R_Generic):
447
  """/2/nodes/[node_name]/storage ressource.
448

449
  """
450
  # LUQueryNodeStorage acquires locks, hence restricting access to GET
451
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
452

    
453
  def GET(self):
454
    node_name = self.items[0]
455

    
456
    storage_type = self._checkStringVariable("storage_type", None)
457
    if not storage_type:
458
      raise http.HttpBadRequest("Missing the required 'storage_type'"
459
                                " parameter")
460

    
461
    output_fields = self._checkStringVariable("output_fields", None)
462
    if not output_fields:
463
      raise http.HttpBadRequest("Missing the required 'output_fields'"
464
                                " parameter")
465

    
466
    op = opcodes.OpQueryNodeStorage(nodes=[node_name],
467
                                    storage_type=storage_type,
468
                                    output_fields=output_fields.split(","))
469
    return baserlib.SubmitJob([op])
470

    
471

    
472
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
473
  """/2/nodes/[node_name]/storage/modify ressource.
474

475
  """
476
  def PUT(self):
477
    node_name = self.items[0]
478

    
479
    storage_type = self._checkStringVariable("storage_type", None)
480
    if not storage_type:
481
      raise http.HttpBadRequest("Missing the required 'storage_type'"
482
                                " parameter")
483

    
484
    name = self._checkStringVariable("name", None)
485
    if not name:
486
      raise http.HttpBadRequest("Missing the required 'name'"
487
                                " parameter")
488

    
489
    changes = {}
490

    
491
    if "allocatable" in self.queryargs:
492
      changes[constants.SF_ALLOCATABLE] = \
493
        bool(self._checkIntVariable("allocatable", default=1))
494

    
495
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
496
                                     storage_type=storage_type,
497
                                     name=name,
498
                                     changes=changes)
499
    return baserlib.SubmitJob([op])
500

    
501

    
502
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
503
  """/2/nodes/[node_name]/storage/repair ressource.
504

505
  """
506
  def PUT(self):
507
    node_name = self.items[0]
508

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

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

    
519
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
520
                                     storage_type=storage_type,
521
                                     name=name)
522
    return baserlib.SubmitJob([op])
523

    
524

    
525
def _ParseInstanceCreateRequestVersion1(data, dry_run):
526
  """Parses an instance creation request version 1.
527

528
  @rtype: L{opcodes.OpCreateInstance}
529
  @return: Instance creation opcode
530

531
  """
532
  # Disks
533
  disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
534

    
535
  disks = []
536
  for idx, i in enumerate(disks_input):
537
    baserlib.CheckType(i, dict, "Disk %d specification" % idx)
538

    
539
    # Size is mandatory
540
    try:
541
      size = i[constants.IDISK_SIZE]
542
    except KeyError:
543
      raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
544
                                " size" % idx)
545

    
546
    disk = {
547
      constants.IDISK_SIZE: size,
548
      }
549

    
550
    # Optional disk access mode
551
    try:
552
      disk_access = i[constants.IDISK_MODE]
553
    except KeyError:
554
      pass
555
    else:
556
      disk[constants.IDISK_MODE] = disk_access
557

    
558
    disks.append(disk)
559

    
560
  assert len(disks_input) == len(disks)
561

    
562
  # Network interfaces
563
  nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
564

    
565
  nics = []
566
  for idx, i in enumerate(nics_input):
567
    baserlib.CheckType(i, dict, "NIC %d specification" % idx)
568

    
569
    nic = {}
570

    
571
    for field in constants.INIC_PARAMS:
572
      try:
573
        value = i[field]
574
      except KeyError:
575
        continue
576

    
577
      nic[field] = value
578

    
579
    nics.append(nic)
580

    
581
  assert len(nics_input) == len(nics)
582

    
583
  # HV/BE parameters
584
  hvparams = baserlib.CheckParameter(data, "hvparams", default={})
585
  utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
586

    
587
  beparams = baserlib.CheckParameter(data, "beparams", default={})
588
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
589

    
590
  return opcodes.OpCreateInstance(
591
    mode=baserlib.CheckParameter(data, "mode"),
592
    instance_name=baserlib.CheckParameter(data, "name"),
593
    os_type=baserlib.CheckParameter(data, "os", default=None),
594
    force_variant=baserlib.CheckParameter(data, "force_variant",
595
                                          default=False),
596
    pnode=baserlib.CheckParameter(data, "pnode", default=None),
597
    snode=baserlib.CheckParameter(data, "snode", default=None),
598
    disk_template=baserlib.CheckParameter(data, "disk_template"),
599
    disks=disks,
600
    nics=nics,
601
    src_node=baserlib.CheckParameter(data, "src_node", default=None),
602
    src_path=baserlib.CheckParameter(data, "src_path", default=None),
603
    start=baserlib.CheckParameter(data, "start", default=True),
604
    wait_for_sync=True,
605
    ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
606
    name_check=baserlib.CheckParameter(data, "name_check", default=True),
607
    file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
608
                                             default=None),
609
    file_driver=baserlib.CheckParameter(data, "file_driver",
610
                                        default=constants.FD_LOOP),
611
    source_handshake=baserlib.CheckParameter(data, "source_handshake",
612
                                             default=None),
613
    source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
614
                                           default=None),
615
    source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
616
                                                 default=None),
617
    iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
618
    hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
619
    hvparams=hvparams,
620
    beparams=beparams,
621
    dry_run=dry_run,
622
    )
623

    
624

    
625
class R_2_instances(baserlib.R_Generic):
626
  """/2/instances resource.
627

628
  """
629
  def GET(self):
630
    """Returns a list of all available instances.
631

632
    """
633
    client = baserlib.GetClient()
634

    
635
    use_locking = self.useLocking()
636
    if self.useBulk():
637
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
638
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
639
    else:
640
      instancesdata = client.QueryInstances([], ["name"], use_locking)
641
      instanceslist = [row[0] for row in instancesdata]
642
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
643
                                   uri_fields=("id", "uri"))
644

    
645
  def _ParseVersion0CreateRequest(self):
646
    """Parses an instance creation request version 0.
647

648
    Request data version 0 is deprecated and should not be used anymore.
649

650
    @rtype: L{opcodes.OpCreateInstance}
651
    @return: Instance creation opcode
652

653
    """
654
    # Do not modify anymore, request data version 0 is deprecated
655
    beparams = baserlib.MakeParamsDict(self.request_body,
656
                                       constants.BES_PARAMETERS)
657
    hvparams = baserlib.MakeParamsDict(self.request_body,
658
                                       constants.HVS_PARAMETERS)
659
    fn = self.getBodyParameter
660

    
661
    # disk processing
662
    disk_data = fn('disks')
663
    if not isinstance(disk_data, list):
664
      raise http.HttpBadRequest("The 'disks' parameter should be a list")
665
    disks = []
666
    for idx, d in enumerate(disk_data):
667
      if not isinstance(d, int):
668
        raise http.HttpBadRequest("Disk %d specification wrong: should"
669
                                  " be an integer" % idx)
670
      disks.append({"size": d})
671

    
672
    # nic processing (one nic only)
673
    nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
674
    if fn("ip", None) is not None:
675
      nics[0]["ip"] = fn("ip")
676
    if fn("mode", None) is not None:
677
      nics[0]["mode"] = fn("mode")
678
    if fn("link", None) is not None:
679
      nics[0]["link"] = fn("link")
680
    if fn("bridge", None) is not None:
681
      nics[0]["bridge"] = fn("bridge")
682

    
683
    # Do not modify anymore, request data version 0 is deprecated
684
    return opcodes.OpCreateInstance(
685
      mode=constants.INSTANCE_CREATE,
686
      instance_name=fn('name'),
687
      disks=disks,
688
      disk_template=fn('disk_template'),
689
      os_type=fn('os'),
690
      pnode=fn('pnode', None),
691
      snode=fn('snode', None),
692
      iallocator=fn('iallocator', None),
693
      nics=nics,
694
      start=fn('start', True),
695
      ip_check=fn('ip_check', True),
696
      name_check=fn('name_check', True),
697
      wait_for_sync=True,
698
      hypervisor=fn('hypervisor', None),
699
      hvparams=hvparams,
700
      beparams=beparams,
701
      file_storage_dir=fn('file_storage_dir', None),
702
      file_driver=fn('file_driver', constants.FD_LOOP),
703
      dry_run=bool(self.dryRun()),
704
      )
705

    
706
  def POST(self):
707
    """Create an instance.
708

709
    @return: a job id
710

711
    """
712
    if not isinstance(self.request_body, dict):
713
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
714

    
715
    # Default to request data version 0
716
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
717

    
718
    if data_version == 0:
719
      op = self._ParseVersion0CreateRequest()
720
    elif data_version == 1:
721
      op = _ParseInstanceCreateRequestVersion1(self.request_body,
722
                                               self.dryRun())
723
    else:
724
      raise http.HttpBadRequest("Unsupported request data version %s" %
725
                                data_version)
726

    
727
    return baserlib.SubmitJob([op])
728

    
729

    
730
class R_2_instances_name(baserlib.R_Generic):
731
  """/2/instances/[instance_name] resources.
732

733
  """
734
  def GET(self):
735
    """Send information about an instance.
736

737
    """
738
    client = baserlib.GetClient()
739
    instance_name = self.items[0]
740

    
741
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
742
                                            names=[instance_name],
743
                                            fields=I_FIELDS,
744
                                            use_locking=self.useLocking())
745

    
746
    return baserlib.MapFields(I_FIELDS, result[0])
747

    
748
  def DELETE(self):
749
    """Delete an instance.
750

751
    """
752
    op = opcodes.OpRemoveInstance(instance_name=self.items[0],
753
                                  ignore_failures=False,
754
                                  dry_run=bool(self.dryRun()))
755
    return baserlib.SubmitJob([op])
756

    
757

    
758
class R_2_instances_name_info(baserlib.R_Generic):
759
  """/2/instances/[instance_name]/info resource.
760

761
  """
762
  def GET(self):
763
    """Request detailed instance information.
764

765
    """
766
    instance_name = self.items[0]
767
    static = bool(self._checkIntVariable("static", default=0))
768

    
769
    op = opcodes.OpQueryInstanceData(instances=[instance_name],
770
                                     static=static)
771
    return baserlib.SubmitJob([op])
772

    
773

    
774
class R_2_instances_name_reboot(baserlib.R_Generic):
775
  """/2/instances/[instance_name]/reboot resource.
776

777
  Implements an instance reboot.
778

779
  """
780
  def POST(self):
781
    """Reboot an instance.
782

783
    The URI takes type=[hard|soft|full] and
784
    ignore_secondaries=[False|True] parameters.
785

786
    """
787
    instance_name = self.items[0]
788
    reboot_type = self.queryargs.get('type',
789
                                     [constants.INSTANCE_REBOOT_HARD])[0]
790
    ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
791
    op = opcodes.OpRebootInstance(instance_name=instance_name,
792
                                  reboot_type=reboot_type,
793
                                  ignore_secondaries=ignore_secondaries,
794
                                  dry_run=bool(self.dryRun()))
795

    
796
    return baserlib.SubmitJob([op])
797

    
798

    
799
class R_2_instances_name_startup(baserlib.R_Generic):
800
  """/2/instances/[instance_name]/startup resource.
801

802
  Implements an instance startup.
803

804
  """
805
  def PUT(self):
806
    """Startup an instance.
807

808
    The URI takes force=[False|True] parameter to start the instance
809
    if even if secondary disks are failing.
810

811
    """
812
    instance_name = self.items[0]
813
    force_startup = bool(self._checkIntVariable('force'))
814
    op = opcodes.OpStartupInstance(instance_name=instance_name,
815
                                   force=force_startup,
816
                                   dry_run=bool(self.dryRun()))
817

    
818
    return baserlib.SubmitJob([op])
819

    
820

    
821
class R_2_instances_name_shutdown(baserlib.R_Generic):
822
  """/2/instances/[instance_name]/shutdown resource.
823

824
  Implements an instance shutdown.
825

826
  """
827
  def PUT(self):
828
    """Shutdown an instance.
829

830
    """
831
    instance_name = self.items[0]
832
    op = opcodes.OpShutdownInstance(instance_name=instance_name,
833
                                    dry_run=bool(self.dryRun()))
834

    
835
    return baserlib.SubmitJob([op])
836

    
837

    
838
class R_2_instances_name_reinstall(baserlib.R_Generic):
839
  """/2/instances/[instance_name]/reinstall resource.
840

841
  Implements an instance reinstall.
842

843
  """
844
  def POST(self):
845
    """Reinstall an instance.
846

847
    The URI takes os=name and nostartup=[0|1] optional
848
    parameters. By default, the instance will be started
849
    automatically.
850

851
    """
852
    instance_name = self.items[0]
853
    ostype = self._checkStringVariable('os')
854
    nostartup = self._checkIntVariable('nostartup')
855
    ops = [
856
      opcodes.OpShutdownInstance(instance_name=instance_name),
857
      opcodes.OpReinstallInstance(instance_name=instance_name, os_type=ostype),
858
      ]
859
    if not nostartup:
860
      ops.append(opcodes.OpStartupInstance(instance_name=instance_name,
861
                                           force=False))
862
    return baserlib.SubmitJob(ops)
863

    
864

    
865
class R_2_instances_name_replace_disks(baserlib.R_Generic):
866
  """/2/instances/[instance_name]/replace-disks resource.
867

868
  """
869
  def POST(self):
870
    """Replaces disks on an instance.
871

872
    """
873
    instance_name = self.items[0]
874
    remote_node = self._checkStringVariable("remote_node", default=None)
875
    mode = self._checkStringVariable("mode", default=None)
876
    raw_disks = self._checkStringVariable("disks", default=None)
877
    iallocator = self._checkStringVariable("iallocator", default=None)
878

    
879
    if raw_disks:
880
      try:
881
        disks = [int(part) for part in raw_disks.split(",")]
882
      except ValueError, err:
883
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
884
    else:
885
      disks = []
886

    
887
    op = opcodes.OpReplaceDisks(instance_name=instance_name,
888
                                remote_node=remote_node,
889
                                mode=mode,
890
                                disks=disks,
891
                                iallocator=iallocator)
892

    
893
    return baserlib.SubmitJob([op])
894

    
895

    
896
class R_2_instances_name_activate_disks(baserlib.R_Generic):
897
  """/2/instances/[instance_name]/activate-disks resource.
898

899
  """
900
  def PUT(self):
901
    """Activate disks for an instance.
902

903
    The URI might contain ignore_size to ignore current recorded size.
904

905
    """
906
    instance_name = self.items[0]
907
    ignore_size = bool(self._checkIntVariable('ignore_size'))
908

    
909
    op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
910
                                         ignore_size=ignore_size)
911

    
912
    return baserlib.SubmitJob([op])
913

    
914

    
915
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
916
  """/2/instances/[instance_name]/deactivate-disks resource.
917

918
  """
919
  def PUT(self):
920
    """Deactivate disks for an instance.
921

922
    """
923
    instance_name = self.items[0]
924

    
925
    op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
926

    
927
    return baserlib.SubmitJob([op])
928

    
929

    
930
class R_2_instances_name_prepare_export(baserlib.R_Generic):
931
  """/2/instances/[instance_name]/prepare-export resource.
932

933
  """
934
  def PUT(self):
935
    """Prepares an export for an instance.
936

937
    @return: a job id
938

939
    """
940
    instance_name = self.items[0]
941
    mode = self._checkStringVariable("mode")
942

    
943
    op = opcodes.OpPrepareExport(instance_name=instance_name,
944
                                 mode=mode)
945

    
946
    return baserlib.SubmitJob([op])
947

    
948

    
949
def _ParseExportInstanceRequest(name, data):
950
  """Parses a request for an instance export.
951

952
  @rtype: L{opcodes.OpExportInstance}
953
  @return: Instance export opcode
954

955
  """
956
  mode = baserlib.CheckParameter(data, "mode",
957
                                 default=constants.EXPORT_MODE_LOCAL)
958
  target_node = baserlib.CheckParameter(data, "destination")
959
  shutdown = baserlib.CheckParameter(data, "shutdown", exptype=bool)
960
  remove_instance = baserlib.CheckParameter(data, "remove_instance",
961
                                            exptype=bool, default=False)
962
  x509_key_name = baserlib.CheckParameter(data, "x509_key_name", default=None)
963
  destination_x509_ca = baserlib.CheckParameter(data, "destination_x509_ca",
964
                                                default=None)
965

    
966
  return opcodes.OpExportInstance(instance_name=name,
967
                                  mode=mode,
968
                                  target_node=target_node,
969
                                  shutdown=shutdown,
970
                                  remove_instance=remove_instance,
971
                                  x509_key_name=x509_key_name,
972
                                  destination_x509_ca=destination_x509_ca)
973

    
974

    
975
class R_2_instances_name_export(baserlib.R_Generic):
976
  """/2/instances/[instance_name]/export resource.
977

978
  """
979
  def PUT(self):
980
    """Exports an instance.
981

982
    @return: a job id
983

984
    """
985
    if not isinstance(self.request_body, dict):
986
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
987

    
988
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
989

    
990
    return baserlib.SubmitJob([op])
991

    
992

    
993
class _R_Tags(baserlib.R_Generic):
994
  """ Quasiclass for tagging resources
995

996
  Manages tags. When inheriting this class you must define the
997
  TAG_LEVEL for it.
998

999
  """
1000
  TAG_LEVEL = None
1001

    
1002
  def __init__(self, items, queryargs, req):
1003
    """A tag resource constructor.
1004

1005
    We have to override the default to sort out cluster naming case.
1006

1007
    """
1008
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1009

    
1010
    if self.TAG_LEVEL != constants.TAG_CLUSTER:
1011
      self.name = items[0]
1012
    else:
1013
      self.name = ""
1014

    
1015
  def GET(self):
1016
    """Returns a list of tags.
1017

1018
    Example: ["tag1", "tag2", "tag3"]
1019

1020
    """
1021
    # pylint: disable-msg=W0212
1022
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1023

    
1024
  def PUT(self):
1025
    """Add a set of tags.
1026

1027
    The request as a list of strings should be PUT to this URI. And
1028
    you'll have back a job id.
1029

1030
    """
1031
    # pylint: disable-msg=W0212
1032
    if 'tag' not in self.queryargs:
1033
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1034
                                " the 'tag' parameter")
1035
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1036
                              self.queryargs['tag'], name=self.name,
1037
                              dry_run=bool(self.dryRun()))
1038

    
1039
  def DELETE(self):
1040
    """Delete a tag.
1041

1042
    In order to delete a set of tags, the DELETE
1043
    request should be addressed to URI like:
1044
    /tags?tag=[tag]&tag=[tag]
1045

1046
    """
1047
    # pylint: disable-msg=W0212
1048
    if 'tag' not in self.queryargs:
1049
      # no we not gonna delete all tags
1050
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1051
                                " tag(s) using the 'tag' parameter")
1052
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1053
                                 self.queryargs['tag'],
1054
                                 name=self.name,
1055
                                 dry_run=bool(self.dryRun()))
1056

    
1057

    
1058
class R_2_instances_name_tags(_R_Tags):
1059
  """ /2/instances/[instance_name]/tags resource.
1060

1061
  Manages per-instance tags.
1062

1063
  """
1064
  TAG_LEVEL = constants.TAG_INSTANCE
1065

    
1066

    
1067
class R_2_nodes_name_tags(_R_Tags):
1068
  """ /2/nodes/[node_name]/tags resource.
1069

1070
  Manages per-node tags.
1071

1072
  """
1073
  TAG_LEVEL = constants.TAG_NODE
1074

    
1075

    
1076
class R_2_tags(_R_Tags):
1077
  """ /2/instances/tags resource.
1078

1079
  Manages cluster tags.
1080

1081
  """
1082
  TAG_LEVEL = constants.TAG_CLUSTER