Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 04a8865b

History | View | Annotate | Download (35.1 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
            "custom_hvparams", "custom_beparams", "custom_nicparams",
63
            ] + _COMMON_FIELDS
64

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

    
76
_NR_DRAINED = "drained"
77
_NR_MASTER_CANDIATE = "master-candidate"
78
_NR_MASTER = "master"
79
_NR_OFFLINE = "offline"
80
_NR_REGULAR = "regular"
81

    
82
_NR_MAP = {
83
  "M": _NR_MASTER,
84
  "C": _NR_MASTER_CANDIATE,
85
  "D": _NR_DRAINED,
86
  "O": _NR_OFFLINE,
87
  "R": _NR_REGULAR,
88
  }
89

    
90
# Request data version field
91
_REQ_DATA_VERSION = "__version__"
92

    
93
# Feature string for instance creation request data version 1
94
_INST_CREATE_REQV1 = "instance-create-reqv1"
95

    
96
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
97
_WFJC_TIMEOUT = 10
98

    
99

    
100
class R_version(baserlib.R_Generic):
101
  """/version resource.
102

103
  This resource should be used to determine the remote API version and
104
  to adapt clients accordingly.
105

106
  """
107
  @staticmethod
108
  def GET():
109
    """Returns the remote API version.
110

111
    """
112
    return constants.RAPI_VERSION
113

    
114

    
115
class R_2_info(baserlib.R_Generic):
116
  """Cluster info.
117

118
  """
119
  @staticmethod
120
  def GET():
121
    """Returns cluster information.
122

123
    """
124
    client = baserlib.GetClient()
125
    return client.QueryClusterInfo()
126

    
127

    
128
class R_2_features(baserlib.R_Generic):
129
  """/2/features resource.
130

131
  """
132
  @staticmethod
133
  def GET():
134
    """Returns list of optional RAPI features implemented.
135

136
    """
137
    return [_INST_CREATE_REQV1]
138

    
139

    
140
class R_2_os(baserlib.R_Generic):
141
  """/2/os resource.
142

143
  """
144
  @staticmethod
145
  def GET():
146
    """Return a list of all OSes.
147

148
    Can return error 500 in case of a problem.
149

150
    Example: ["debian-etch"]
151

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

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

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

    
167
    return os_names
168

    
169

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

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

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

    
181

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

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

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

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

    
200

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

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

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

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

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

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

    
236

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

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

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

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

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

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

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

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

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

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

    
278
    (job_info, log_entries) = result
279

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

    
285

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

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

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

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

    
305

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

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

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

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

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

    
323

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

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

331
    @return: Node role
332

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

    
339
    return _NR_MAP[result[0][0]]
340

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

344
    @return: a job id
345

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

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

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

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

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

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

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

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

    
379
    return baserlib.SubmitJob([op])
380

    
381

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

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

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

    
396
    cl = baserlib.GetClient()
397

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

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

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

    
418
    return jobs
419

    
420

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

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

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

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

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

    
444
    return baserlib.SubmitJob([op])
445

    
446

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

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

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

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

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

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

    
472

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

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

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

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

    
490
    changes = {}
491

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

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

    
502

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

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

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

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

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

    
525

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

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

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

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

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

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

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

    
559
    disks.append(disk)
560

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

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

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

    
570
    nic = {}
571

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

    
578
      nic[field] = value
579

    
580
    nics.append(nic)
581

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

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

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

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

    
627

    
628
class R_2_instances(baserlib.R_Generic):
629
  """/2/instances resource.
630

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

635
    """
636
    client = baserlib.GetClient()
637

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

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

651
    Request data version 0 is deprecated and should not be used anymore.
652

653
    @rtype: L{opcodes.OpCreateInstance}
654
    @return: Instance creation opcode
655

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

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

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

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

    
709
  def POST(self):
710
    """Create an instance.
711

712
    @return: a job id
713

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

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

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

    
730
    return baserlib.SubmitJob([op])
731

    
732

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

736
  """
737
  def GET(self):
738
    """Send information about an instance.
739

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

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

    
749
    return baserlib.MapFields(I_FIELDS, result[0])
750

    
751
  def DELETE(self):
752
    """Delete an instance.
753

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

    
760

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

764
  """
765
  def GET(self):
766
    """Request detailed instance information.
767

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

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

    
776

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

780
  Implements an instance reboot.
781

782
  """
783
  def POST(self):
784
    """Reboot an instance.
785

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

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

    
799
    return baserlib.SubmitJob([op])
800

    
801

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

805
  Implements an instance startup.
806

807
  """
808
  def PUT(self):
809
    """Startup an instance.
810

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

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

    
821
    return baserlib.SubmitJob([op])
822

    
823

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

827
  Implements an instance shutdown.
828

829
  """
830
  def PUT(self):
831
    """Shutdown an instance.
832

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

    
838
    return baserlib.SubmitJob([op])
839

    
840

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

844
  Implements an instance reinstall.
845

846
  """
847
  def POST(self):
848
    """Reinstall an instance.
849

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

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

    
867

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

871
  """
872
  def POST(self):
873
    """Replaces disks on an instance.
874

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

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

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

    
896
    return baserlib.SubmitJob([op])
897

    
898

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

902
  """
903
  def PUT(self):
904
    """Activate disks for an instance.
905

906
    The URI might contain ignore_size to ignore current recorded size.
907

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

    
912
    op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
913
                                         ignore_size=ignore_size)
914

    
915
    return baserlib.SubmitJob([op])
916

    
917

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

921
  """
922
  def PUT(self):
923
    """Deactivate disks for an instance.
924

925
    """
926
    instance_name = self.items[0]
927

    
928
    op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
929

    
930
    return baserlib.SubmitJob([op])
931

    
932

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

936
  """
937
  def PUT(self):
938
    """Prepares an export for an instance.
939

940
    @return: a job id
941

942
    """
943
    instance_name = self.items[0]
944
    mode = self._checkStringVariable("mode")
945

    
946
    op = opcodes.OpPrepareExport(instance_name=instance_name,
947
                                 mode=mode)
948

    
949
    return baserlib.SubmitJob([op])
950

    
951

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

955
  @rtype: L{opcodes.OpExportInstance}
956
  @return: Instance export opcode
957

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

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

    
977

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

981
  """
982
  def PUT(self):
983
    """Exports an instance.
984

985
    @return: a job id
986

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

    
991
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
992

    
993
    return baserlib.SubmitJob([op])
994

    
995

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

999
  @rtype: L{opcodes.OpMigrateInstance}
1000
  @return: Instance migration opcode
1001

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

    
1007
  return opcodes.OpMigrateInstance(instance_name=name, mode=mode,
1008
                                   cleanup=cleanup)
1009

    
1010

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

1014
  """
1015
  def PUT(self):
1016
    """Migrates an instance.
1017

1018
    @return: a job id
1019

1020
    """
1021
    baserlib.CheckType(self.request_body, dict, "Body contents")
1022

    
1023
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1024

    
1025
    return baserlib.SubmitJob([op])
1026

    
1027

    
1028
def _ParseRenameInstanceRequest(name, data):
1029
  """Parses a request for renaming an instance.
1030

1031
  @rtype: L{opcodes.OpRenameInstance}
1032
  @return: Instance rename opcode
1033

1034
  """
1035
  new_name = baserlib.CheckParameter(data, "new_name")
1036
  ip_check = baserlib.CheckParameter(data, "ip_check", default=True)
1037
  name_check = baserlib.CheckParameter(data, "name_check", default=True)
1038

    
1039
  return opcodes.OpRenameInstance(instance_name=name, new_name=new_name,
1040
                                  name_check=name_check, ip_check=ip_check)
1041

    
1042

    
1043
class R_2_instances_name_rename(baserlib.R_Generic):
1044
  """/2/instances/[instance_name]/rename resource.
1045

1046
  """
1047
  def PUT(self):
1048
    """Changes the name of an instance.
1049

1050
    @return: a job id
1051

1052
    """
1053
    baserlib.CheckType(self.request_body, dict, "Body contents")
1054

    
1055
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1056

    
1057
    return baserlib.SubmitJob([op])
1058

    
1059

    
1060
def _ParseModifyInstanceRequest(name, data):
1061
  """Parses a request for modifying an instance.
1062

1063
  @rtype: L{opcodes.OpSetInstanceParams}
1064
  @return: Instance modify opcode
1065

1066
  """
1067
  osparams = baserlib.CheckParameter(data, "osparams", default={})
1068
  force = baserlib.CheckParameter(data, "force", default=False)
1069
  nics = baserlib.CheckParameter(data, "nics", default=[])
1070
  disks = baserlib.CheckParameter(data, "disks", default=[])
1071
  disk_template = baserlib.CheckParameter(data, "disk_template", default=None)
1072
  remote_node = baserlib.CheckParameter(data, "remote_node", default=None)
1073
  os_name = baserlib.CheckParameter(data, "os_name", default=None)
1074
  force_variant = baserlib.CheckParameter(data, "force_variant", default=False)
1075

    
1076
  # HV/BE parameters
1077
  hvparams = baserlib.CheckParameter(data, "hvparams", default={})
1078
  utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES,
1079
                      allowed_values=[constants.VALUE_DEFAULT])
1080

    
1081
  beparams = baserlib.CheckParameter(data, "beparams", default={})
1082
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES,
1083
                      allowed_values=[constants.VALUE_DEFAULT])
1084

    
1085
  return opcodes.OpSetInstanceParams(instance_name=name, hvparams=hvparams,
1086
                                     beparams=beparams, osparams=osparams,
1087
                                     force=force, nics=nics, disks=disks,
1088
                                     disk_template=disk_template,
1089
                                     remote_node=remote_node, os_name=os_name,
1090
                                     force_variant=force_variant)
1091

    
1092

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

1096
  """
1097
  def PUT(self):
1098
    """Changes some parameters of an instance.
1099

1100
    @return: a job id
1101

1102
    """
1103
    baserlib.CheckType(self.request_body, dict, "Body contents")
1104

    
1105
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1106

    
1107
    return baserlib.SubmitJob([op])
1108

    
1109

    
1110
class _R_Tags(baserlib.R_Generic):
1111
  """ Quasiclass for tagging resources
1112

1113
  Manages tags. When inheriting this class you must define the
1114
  TAG_LEVEL for it.
1115

1116
  """
1117
  TAG_LEVEL = None
1118

    
1119
  def __init__(self, items, queryargs, req):
1120
    """A tag resource constructor.
1121

1122
    We have to override the default to sort out cluster naming case.
1123

1124
    """
1125
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1126

    
1127
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1128
      self.name = None
1129
    else:
1130
      self.name = items[0]
1131

    
1132
  def GET(self):
1133
    """Returns a list of tags.
1134

1135
    Example: ["tag1", "tag2", "tag3"]
1136

1137
    """
1138
    # pylint: disable-msg=W0212
1139
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1140

    
1141
  def PUT(self):
1142
    """Add a set of tags.
1143

1144
    The request as a list of strings should be PUT to this URI. And
1145
    you'll have back a job id.
1146

1147
    """
1148
    # pylint: disable-msg=W0212
1149
    if 'tag' not in self.queryargs:
1150
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1151
                                " the 'tag' parameter")
1152
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1153
                              self.queryargs['tag'], name=self.name,
1154
                              dry_run=bool(self.dryRun()))
1155

    
1156
  def DELETE(self):
1157
    """Delete a tag.
1158

1159
    In order to delete a set of tags, the DELETE
1160
    request should be addressed to URI like:
1161
    /tags?tag=[tag]&tag=[tag]
1162

1163
    """
1164
    # pylint: disable-msg=W0212
1165
    if 'tag' not in self.queryargs:
1166
      # no we not gonna delete all tags
1167
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1168
                                " tag(s) using the 'tag' parameter")
1169
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1170
                                 self.queryargs['tag'],
1171
                                 name=self.name,
1172
                                 dry_run=bool(self.dryRun()))
1173

    
1174

    
1175
class R_2_instances_name_tags(_R_Tags):
1176
  """ /2/instances/[instance_name]/tags resource.
1177

1178
  Manages per-instance tags.
1179

1180
  """
1181
  TAG_LEVEL = constants.TAG_INSTANCE
1182

    
1183

    
1184
class R_2_nodes_name_tags(_R_Tags):
1185
  """ /2/nodes/[node_name]/tags resource.
1186

1187
  Manages per-node tags.
1188

1189
  """
1190
  TAG_LEVEL = constants.TAG_NODE
1191

    
1192

    
1193
class R_2_tags(_R_Tags):
1194
  """ /2/instances/tags resource.
1195

1196
  Manages cluster tags.
1197

1198
  """
1199
  TAG_LEVEL = constants.TAG_CLUSTER