Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 5823e0d2

History | View | Annotate | Download (32.2 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"),
594
    osparams=baserlib.CheckParameter(data, "osparams", default={}),
595
    force_variant=baserlib.CheckParameter(data, "force_variant",
596
                                          default=False),
597
    pnode=baserlib.CheckParameter(data, "pnode", default=None),
598
    snode=baserlib.CheckParameter(data, "snode", default=None),
599
    disk_template=baserlib.CheckParameter(data, "disk_template"),
600
    disks=disks,
601
    nics=nics,
602
    src_node=baserlib.CheckParameter(data, "src_node", default=None),
603
    src_path=baserlib.CheckParameter(data, "src_path", default=None),
604
    start=baserlib.CheckParameter(data, "start", default=True),
605
    wait_for_sync=True,
606
    ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
607
    name_check=baserlib.CheckParameter(data, "name_check", default=True),
608
    file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
609
                                             default=None),
610
    file_driver=baserlib.CheckParameter(data, "file_driver",
611
                                        default=constants.FD_LOOP),
612
    source_handshake=baserlib.CheckParameter(data, "source_handshake",
613
                                             default=None),
614
    source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
615
                                           default=None),
616
    source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
617
                                                 default=None),
618
    iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
619
    hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
620
    hvparams=hvparams,
621
    beparams=beparams,
622
    dry_run=dry_run,
623
    )
624

    
625

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

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

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

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

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

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

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

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

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

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

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

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

710
    @return: a job id
711

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

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

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

    
728
    return baserlib.SubmitJob([op])
729

    
730

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

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

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

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

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

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

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

    
758

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

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

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

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

    
774

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

778
  Implements an instance reboot.
779

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

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

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

    
797
    return baserlib.SubmitJob([op])
798

    
799

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

803
  Implements an instance startup.
804

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

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

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

    
819
    return baserlib.SubmitJob([op])
820

    
821

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

825
  Implements an instance shutdown.
826

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

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

    
836
    return baserlib.SubmitJob([op])
837

    
838

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

842
  Implements an instance reinstall.
843

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

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

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

    
865

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

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

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

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

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

    
894
    return baserlib.SubmitJob([op])
895

    
896

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

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

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

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

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

    
913
    return baserlib.SubmitJob([op])
914

    
915

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

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

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

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

    
928
    return baserlib.SubmitJob([op])
929

    
930

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

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

938
    @return: a job id
939

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

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

    
947
    return baserlib.SubmitJob([op])
948

    
949

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

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

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

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

    
975

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

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

983
    @return: a job id
984

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

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

    
991
    return baserlib.SubmitJob([op])
992

    
993

    
994
def _ParseMigrateInstanceRequest(name, data):
995
  """Parses a request for an instance migration.
996

997
  @rtype: L{opcodes.OpMigrateInstance}
998
  @return: Instance migration opcode
999

1000
  """
1001
  mode = baserlib.CheckParameter(data, "mode", default=None)
1002
  cleanup = baserlib.CheckParameter(data, "cleanup", exptype=bool,
1003
                                    default=False)
1004

    
1005
  return opcodes.OpMigrateInstance(instance_name=name, mode=mode,
1006
                                   cleanup=cleanup)
1007

    
1008

    
1009
class R_2_instances_name_migrate(baserlib.R_Generic):
1010
  """/2/instances/[instance_name]/migrate resource.
1011

1012
  """
1013
  def PUT(self):
1014
    """Migrates an instance.
1015

1016
    @return: a job id
1017

1018
    """
1019
    baserlib.CheckType(self.request_body, dict, "Body contents")
1020

    
1021
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1022

    
1023
    return baserlib.SubmitJob([op])
1024

    
1025

    
1026
class _R_Tags(baserlib.R_Generic):
1027
  """ Quasiclass for tagging resources
1028

1029
  Manages tags. When inheriting this class you must define the
1030
  TAG_LEVEL for it.
1031

1032
  """
1033
  TAG_LEVEL = None
1034

    
1035
  def __init__(self, items, queryargs, req):
1036
    """A tag resource constructor.
1037

1038
    We have to override the default to sort out cluster naming case.
1039

1040
    """
1041
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1042

    
1043
    if self.TAG_LEVEL != constants.TAG_CLUSTER:
1044
      self.name = items[0]
1045
    else:
1046
      self.name = ""
1047

    
1048
  def GET(self):
1049
    """Returns a list of tags.
1050

1051
    Example: ["tag1", "tag2", "tag3"]
1052

1053
    """
1054
    # pylint: disable-msg=W0212
1055
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1056

    
1057
  def PUT(self):
1058
    """Add a set of tags.
1059

1060
    The request as a list of strings should be PUT to this URI. And
1061
    you'll have back a job id.
1062

1063
    """
1064
    # pylint: disable-msg=W0212
1065
    if 'tag' not in self.queryargs:
1066
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1067
                                " the 'tag' parameter")
1068
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1069
                              self.queryargs['tag'], name=self.name,
1070
                              dry_run=bool(self.dryRun()))
1071

    
1072
  def DELETE(self):
1073
    """Delete a tag.
1074

1075
    In order to delete a set of tags, the DELETE
1076
    request should be addressed to URI like:
1077
    /tags?tag=[tag]&tag=[tag]
1078

1079
    """
1080
    # pylint: disable-msg=W0212
1081
    if 'tag' not in self.queryargs:
1082
      # no we not gonna delete all tags
1083
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1084
                                " tag(s) using the 'tag' parameter")
1085
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1086
                                 self.queryargs['tag'],
1087
                                 name=self.name,
1088
                                 dry_run=bool(self.dryRun()))
1089

    
1090

    
1091
class R_2_instances_name_tags(_R_Tags):
1092
  """ /2/instances/[instance_name]/tags resource.
1093

1094
  Manages per-instance tags.
1095

1096
  """
1097
  TAG_LEVEL = constants.TAG_INSTANCE
1098

    
1099

    
1100
class R_2_nodes_name_tags(_R_Tags):
1101
  """ /2/nodes/[node_name]/tags resource.
1102

1103
  Manages per-node tags.
1104

1105
  """
1106
  TAG_LEVEL = constants.TAG_NODE
1107

    
1108

    
1109
class R_2_tags(_R_Tags):
1110
  """ /2/instances/tags resource.
1111

1112
  Manages cluster tags.
1113

1114
  """
1115
  TAG_LEVEL = constants.TAG_CLUSTER