Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 4edc512c

History | View | Annotate | Download (37.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
            "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
G_FIELDS = ["name", "uuid",
77
            "node_cnt", "node_list",
78
            "ctime", "mtime", "serial_no",
79
            ]  # "tags" is missing to be able to use _COMMON_FIELDS here.
80

    
81
_NR_DRAINED = "drained"
82
_NR_MASTER_CANDIATE = "master-candidate"
83
_NR_MASTER = "master"
84
_NR_OFFLINE = "offline"
85
_NR_REGULAR = "regular"
86

    
87
_NR_MAP = {
88
  "M": _NR_MASTER,
89
  "C": _NR_MASTER_CANDIATE,
90
  "D": _NR_DRAINED,
91
  "O": _NR_OFFLINE,
92
  "R": _NR_REGULAR,
93
  }
94

    
95
# Request data version field
96
_REQ_DATA_VERSION = "__version__"
97

    
98
# Feature string for instance creation request data version 1
99
_INST_CREATE_REQV1 = "instance-create-reqv1"
100

    
101
# Feature string for instance reinstall request version 1
102
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
103

    
104
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
105
_WFJC_TIMEOUT = 10
106

    
107

    
108
class R_version(baserlib.R_Generic):
109
  """/version resource.
110

111
  This resource should be used to determine the remote API version and
112
  to adapt clients accordingly.
113

114
  """
115
  @staticmethod
116
  def GET():
117
    """Returns the remote API version.
118

119
    """
120
    return constants.RAPI_VERSION
121

    
122

    
123
class R_2_info(baserlib.R_Generic):
124
  """Cluster info.
125

126
  """
127
  @staticmethod
128
  def GET():
129
    """Returns cluster information.
130

131
    """
132
    client = baserlib.GetClient()
133
    return client.QueryClusterInfo()
134

    
135

    
136
class R_2_features(baserlib.R_Generic):
137
  """/2/features resource.
138

139
  """
140
  @staticmethod
141
  def GET():
142
    """Returns list of optional RAPI features implemented.
143

144
    """
145
    return [_INST_CREATE_REQV1, _INST_REINSTALL_REQV1]
146

    
147

    
148
class R_2_os(baserlib.R_Generic):
149
  """/2/os resource.
150

151
  """
152
  @staticmethod
153
  def GET():
154
    """Return a list of all OSes.
155

156
    Can return error 500 in case of a problem.
157

158
    Example: ["debian-etch"]
159

160
    """
161
    cl = baserlib.GetClient()
162
    op = opcodes.OpDiagnoseOS(output_fields=["name", "variants"], names=[])
163
    job_id = baserlib.SubmitJob([op], cl)
164
    # we use custom feedback function, instead of print we log the status
165
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
166
    diagnose_data = result[0]
167

    
168
    if not isinstance(diagnose_data, list):
169
      raise http.HttpBadGateway(message="Can't get OS list")
170

    
171
    os_names = []
172
    for (name, variants) in diagnose_data:
173
      os_names.extend(cli.CalculateOSNames(name, variants))
174

    
175
    return os_names
176

    
177

    
178
class R_2_redist_config(baserlib.R_Generic):
179
  """/2/redistribute-config resource.
180

181
  """
182
  @staticmethod
183
  def PUT():
184
    """Redistribute configuration to all nodes.
185

186
    """
187
    return baserlib.SubmitJob([opcodes.OpRedistributeConfig()])
188

    
189

    
190
class R_2_jobs(baserlib.R_Generic):
191
  """/2/jobs resource.
192

193
  """
194
  @staticmethod
195
  def GET():
196
    """Returns a dictionary of jobs.
197

198
    @return: a dictionary with jobs id and uri.
199

200
    """
201
    fields = ["id"]
202
    cl = baserlib.GetClient()
203
    # Convert the list of lists to the list of ids
204
    result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
205
    return baserlib.BuildUriList(result, "/2/jobs/%s",
206
                                 uri_fields=("id", "uri"))
207

    
208

    
209
class R_2_jobs_id(baserlib.R_Generic):
210
  """/2/jobs/[job_id] resource.
211

212
  """
213
  def GET(self):
214
    """Returns a job status.
215

216
    @return: a dictionary with job parameters.
217
        The result includes:
218
            - id: job ID as a number
219
            - status: current job status as a string
220
            - ops: involved OpCodes as a list of dictionaries for each
221
              opcodes in the job
222
            - opstatus: OpCodes status as a list
223
            - opresult: OpCodes results as a list of lists
224

225
    """
226
    fields = ["id", "ops", "status", "summary",
227
              "opstatus", "opresult", "oplog",
228
              "received_ts", "start_ts", "end_ts",
229
              ]
230
    job_id = self.items[0]
231
    result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
232
    if result is None:
233
      raise http.HttpNotFound()
234
    return baserlib.MapFields(fields, result)
235

    
236
  def DELETE(self):
237
    """Cancel not-yet-started job.
238

239
    """
240
    job_id = self.items[0]
241
    result = baserlib.GetClient().CancelJob(job_id)
242
    return result
243

    
244

    
245
class R_2_jobs_id_wait(baserlib.R_Generic):
246
  """/2/jobs/[job_id]/wait resource.
247

248
  """
249
  # WaitForJobChange provides access to sensitive information and blocks
250
  # machine resources (it's a blocking RAPI call), hence restricting access.
251
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
252

    
253
  def GET(self):
254
    """Waits for job changes.
255

256
    """
257
    job_id = self.items[0]
258

    
259
    fields = self.getBodyParameter("fields")
260
    prev_job_info = self.getBodyParameter("previous_job_info", None)
261
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
262

    
263
    if not isinstance(fields, list):
264
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
265

    
266
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
267
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
268
                                " be a list")
269

    
270
    if not (prev_log_serial is None or
271
            isinstance(prev_log_serial, (int, long))):
272
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
273
                                " be a number")
274

    
275
    client = baserlib.GetClient()
276
    result = client.WaitForJobChangeOnce(job_id, fields,
277
                                         prev_job_info, prev_log_serial,
278
                                         timeout=_WFJC_TIMEOUT)
279
    if not result:
280
      raise http.HttpNotFound()
281

    
282
    if result == constants.JOB_NOTCHANGED:
283
      # No changes
284
      return None
285

    
286
    (job_info, log_entries) = result
287

    
288
    return {
289
      "job_info": job_info,
290
      "log_entries": log_entries,
291
      }
292

    
293

    
294
class R_2_nodes(baserlib.R_Generic):
295
  """/2/nodes resource.
296

297
  """
298
  def GET(self):
299
    """Returns a list of all nodes.
300

301
    """
302
    client = baserlib.GetClient()
303

    
304
    if self.useBulk():
305
      bulkdata = client.QueryNodes([], N_FIELDS, False)
306
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
307
    else:
308
      nodesdata = client.QueryNodes([], ["name"], False)
309
      nodeslist = [row[0] for row in nodesdata]
310
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
311
                                   uri_fields=("id", "uri"))
312

    
313

    
314
class R_2_nodes_name(baserlib.R_Generic):
315
  """/2/nodes/[node_name] resources.
316

317
  """
318
  def GET(self):
319
    """Send information about a node.
320

321
    """
322
    node_name = self.items[0]
323
    client = baserlib.GetClient()
324

    
325
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
326
                                            names=[node_name], fields=N_FIELDS,
327
                                            use_locking=self.useLocking())
328

    
329
    return baserlib.MapFields(N_FIELDS, result[0])
330

    
331

    
332
class R_2_nodes_name_role(baserlib.R_Generic):
333
  """ /2/nodes/[node_name]/role resource.
334

335
  """
336
  def GET(self):
337
    """Returns the current node role.
338

339
    @return: Node role
340

341
    """
342
    node_name = self.items[0]
343
    client = baserlib.GetClient()
344
    result = client.QueryNodes(names=[node_name], fields=["role"],
345
                               use_locking=self.useLocking())
346

    
347
    return _NR_MAP[result[0][0]]
348

    
349
  def PUT(self):
350
    """Sets the node role.
351

352
    @return: a job id
353

354
    """
355
    if not isinstance(self.request_body, basestring):
356
      raise http.HttpBadRequest("Invalid body contents, not a string")
357

    
358
    node_name = self.items[0]
359
    role = self.request_body
360

    
361
    if role == _NR_REGULAR:
362
      candidate = False
363
      offline = False
364
      drained = False
365

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

    
370
    elif role == _NR_DRAINED:
371
      drained = True
372
      candidate = offline = None
373

    
374
    elif role == _NR_OFFLINE:
375
      offline = True
376
      candidate = drained = None
377

    
378
    else:
379
      raise http.HttpBadRequest("Can't set '%s' role" % role)
380

    
381
    op = opcodes.OpSetNodeParams(node_name=node_name,
382
                                 master_candidate=candidate,
383
                                 offline=offline,
384
                                 drained=drained,
385
                                 force=bool(self.useForce()))
386

    
387
    return baserlib.SubmitJob([op])
388

    
389

    
390
class R_2_nodes_name_evacuate(baserlib.R_Generic):
391
  """/2/nodes/[node_name]/evacuate resource.
392

393
  """
394
  def POST(self):
395
    """Evacuate all secondary instances off a node.
396

397
    """
398
    node_name = self.items[0]
399
    remote_node = self._checkStringVariable("remote_node", default=None)
400
    iallocator = self._checkStringVariable("iallocator", default=None)
401
    early_r = bool(self._checkIntVariable("early_release", default=0))
402
    dry_run = bool(self.dryRun())
403

    
404
    cl = baserlib.GetClient()
405

    
406
    op = opcodes.OpNodeEvacuationStrategy(nodes=[node_name],
407
                                          iallocator=iallocator,
408
                                          remote_node=remote_node)
409

    
410
    job_id = baserlib.SubmitJob([op], cl)
411
    # we use custom feedback function, instead of print we log the status
412
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
413

    
414
    jobs = []
415
    for iname, node in result:
416
      if dry_run:
417
        jid = None
418
      else:
419
        op = opcodes.OpReplaceDisks(instance_name=iname,
420
                                    remote_node=node, disks=[],
421
                                    mode=constants.REPLACE_DISK_CHG,
422
                                    early_release=early_r)
423
        jid = baserlib.SubmitJob([op])
424
      jobs.append((jid, iname, node))
425

    
426
    return jobs
427

    
428

    
429
class R_2_nodes_name_migrate(baserlib.R_Generic):
430
  """/2/nodes/[node_name]/migrate resource.
431

432
  """
433
  def POST(self):
434
    """Migrate all primary instances from a node.
435

436
    """
437
    node_name = self.items[0]
438

    
439
    if "live" in self.queryargs and "mode" in self.queryargs:
440
      raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
441
                                " be passed")
442
    elif "live" in self.queryargs:
443
      if self._checkIntVariable("live", default=1):
444
        mode = constants.HT_MIGRATION_LIVE
445
      else:
446
        mode = constants.HT_MIGRATION_NONLIVE
447
    else:
448
      mode = self._checkStringVariable("mode", default=None)
449

    
450
    op = opcodes.OpMigrateNode(node_name=node_name, mode=mode)
451

    
452
    return baserlib.SubmitJob([op])
453

    
454

    
455
class R_2_nodes_name_storage(baserlib.R_Generic):
456
  """/2/nodes/[node_name]/storage ressource.
457

458
  """
459
  # LUQueryNodeStorage acquires locks, hence restricting access to GET
460
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
461

    
462
  def GET(self):
463
    node_name = self.items[0]
464

    
465
    storage_type = self._checkStringVariable("storage_type", None)
466
    if not storage_type:
467
      raise http.HttpBadRequest("Missing the required 'storage_type'"
468
                                " parameter")
469

    
470
    output_fields = self._checkStringVariable("output_fields", None)
471
    if not output_fields:
472
      raise http.HttpBadRequest("Missing the required 'output_fields'"
473
                                " parameter")
474

    
475
    op = opcodes.OpQueryNodeStorage(nodes=[node_name],
476
                                    storage_type=storage_type,
477
                                    output_fields=output_fields.split(","))
478
    return baserlib.SubmitJob([op])
479

    
480

    
481
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
482
  """/2/nodes/[node_name]/storage/modify ressource.
483

484
  """
485
  def PUT(self):
486
    node_name = self.items[0]
487

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

    
493
    name = self._checkStringVariable("name", None)
494
    if not name:
495
      raise http.HttpBadRequest("Missing the required 'name'"
496
                                " parameter")
497

    
498
    changes = {}
499

    
500
    if "allocatable" in self.queryargs:
501
      changes[constants.SF_ALLOCATABLE] = \
502
        bool(self._checkIntVariable("allocatable", default=1))
503

    
504
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
505
                                     storage_type=storage_type,
506
                                     name=name,
507
                                     changes=changes)
508
    return baserlib.SubmitJob([op])
509

    
510

    
511
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
512
  """/2/nodes/[node_name]/storage/repair ressource.
513

514
  """
515
  def PUT(self):
516
    node_name = self.items[0]
517

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

    
523
    name = self._checkStringVariable("name", None)
524
    if not name:
525
      raise http.HttpBadRequest("Missing the required 'name'"
526
                                " parameter")
527

    
528
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
529
                                     storage_type=storage_type,
530
                                     name=name)
531
    return baserlib.SubmitJob([op])
532

    
533

    
534
class R_2_groups(baserlib.R_Generic):
535
  """/2/groups resource.
536

537
  """
538
  def GET(self):
539
    """Returns a list of all node groups.
540

541
    """
542
    client = baserlib.GetClient()
543

    
544
    if self.useBulk():
545
      bulkdata = client.QueryGroups([], G_FIELDS, False)
546
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
547
    else:
548
      data = client.QueryGroups([], ["name"], False)
549
      groupnames = [row[0] for row in data]
550
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
551
                                   uri_fields=("name", "uri"))
552

    
553

    
554
class R_2_groups_name(baserlib.R_Generic):
555
  """/2/groups/[group_name] resources.
556

557
  """
558
  def GET(self):
559
    """Send information about a node group.
560

561
    """
562
    group_name = self.items[0]
563
    client = baserlib.GetClient()
564

    
565
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
566
                                            names=[group_name], fields=G_FIELDS,
567
                                            use_locking=self.useLocking())
568

    
569
    return baserlib.MapFields(G_FIELDS, result[0])
570

    
571

    
572

    
573
def _ParseInstanceCreateRequestVersion1(data, dry_run):
574
  """Parses an instance creation request version 1.
575

576
  @rtype: L{opcodes.OpCreateInstance}
577
  @return: Instance creation opcode
578

579
  """
580
  # Disks
581
  disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
582

    
583
  disks = []
584
  for idx, i in enumerate(disks_input):
585
    baserlib.CheckType(i, dict, "Disk %d specification" % idx)
586

    
587
    # Size is mandatory
588
    try:
589
      size = i[constants.IDISK_SIZE]
590
    except KeyError:
591
      raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
592
                                " size" % idx)
593

    
594
    disk = {
595
      constants.IDISK_SIZE: size,
596
      }
597

    
598
    # Optional disk access mode
599
    try:
600
      disk_access = i[constants.IDISK_MODE]
601
    except KeyError:
602
      pass
603
    else:
604
      disk[constants.IDISK_MODE] = disk_access
605

    
606
    disks.append(disk)
607

    
608
  assert len(disks_input) == len(disks)
609

    
610
  # Network interfaces
611
  nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
612

    
613
  nics = []
614
  for idx, i in enumerate(nics_input):
615
    baserlib.CheckType(i, dict, "NIC %d specification" % idx)
616

    
617
    nic = {}
618

    
619
    for field in constants.INIC_PARAMS:
620
      try:
621
        value = i[field]
622
      except KeyError:
623
        continue
624

    
625
      nic[field] = value
626

    
627
    nics.append(nic)
628

    
629
  assert len(nics_input) == len(nics)
630

    
631
  # HV/BE parameters
632
  hvparams = baserlib.CheckParameter(data, "hvparams", default={})
633
  utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
634

    
635
  beparams = baserlib.CheckParameter(data, "beparams", default={})
636
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
637

    
638
  return opcodes.OpCreateInstance(
639
    mode=baserlib.CheckParameter(data, "mode"),
640
    instance_name=baserlib.CheckParameter(data, "name"),
641
    os_type=baserlib.CheckParameter(data, "os"),
642
    osparams=baserlib.CheckParameter(data, "osparams", default={}),
643
    force_variant=baserlib.CheckParameter(data, "force_variant",
644
                                          default=False),
645
    no_install=baserlib.CheckParameter(data, "no_install", default=False),
646
    pnode=baserlib.CheckParameter(data, "pnode", default=None),
647
    snode=baserlib.CheckParameter(data, "snode", default=None),
648
    disk_template=baserlib.CheckParameter(data, "disk_template"),
649
    disks=disks,
650
    nics=nics,
651
    src_node=baserlib.CheckParameter(data, "src_node", default=None),
652
    src_path=baserlib.CheckParameter(data, "src_path", default=None),
653
    start=baserlib.CheckParameter(data, "start", default=True),
654
    wait_for_sync=True,
655
    ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
656
    name_check=baserlib.CheckParameter(data, "name_check", default=True),
657
    file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
658
                                             default=None),
659
    file_driver=baserlib.CheckParameter(data, "file_driver",
660
                                        default=constants.FD_LOOP),
661
    source_handshake=baserlib.CheckParameter(data, "source_handshake",
662
                                             default=None),
663
    source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
664
                                           default=None),
665
    source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
666
                                                 default=None),
667
    iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
668
    hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
669
    hvparams=hvparams,
670
    beparams=beparams,
671
    dry_run=dry_run,
672
    )
673

    
674

    
675
class R_2_instances(baserlib.R_Generic):
676
  """/2/instances resource.
677

678
  """
679
  def GET(self):
680
    """Returns a list of all available instances.
681

682
    """
683
    client = baserlib.GetClient()
684

    
685
    use_locking = self.useLocking()
686
    if self.useBulk():
687
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
688
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
689
    else:
690
      instancesdata = client.QueryInstances([], ["name"], use_locking)
691
      instanceslist = [row[0] for row in instancesdata]
692
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
693
                                   uri_fields=("id", "uri"))
694

    
695
  def _ParseVersion0CreateRequest(self):
696
    """Parses an instance creation request version 0.
697

698
    Request data version 0 is deprecated and should not be used anymore.
699

700
    @rtype: L{opcodes.OpCreateInstance}
701
    @return: Instance creation opcode
702

703
    """
704
    # Do not modify anymore, request data version 0 is deprecated
705
    beparams = baserlib.MakeParamsDict(self.request_body,
706
                                       constants.BES_PARAMETERS)
707
    hvparams = baserlib.MakeParamsDict(self.request_body,
708
                                       constants.HVS_PARAMETERS)
709
    fn = self.getBodyParameter
710

    
711
    # disk processing
712
    disk_data = fn('disks')
713
    if not isinstance(disk_data, list):
714
      raise http.HttpBadRequest("The 'disks' parameter should be a list")
715
    disks = []
716
    for idx, d in enumerate(disk_data):
717
      if not isinstance(d, int):
718
        raise http.HttpBadRequest("Disk %d specification wrong: should"
719
                                  " be an integer" % idx)
720
      disks.append({"size": d})
721

    
722
    # nic processing (one nic only)
723
    nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
724
    if fn("ip", None) is not None:
725
      nics[0]["ip"] = fn("ip")
726
    if fn("mode", None) is not None:
727
      nics[0]["mode"] = fn("mode")
728
    if fn("link", None) is not None:
729
      nics[0]["link"] = fn("link")
730
    if fn("bridge", None) is not None:
731
      nics[0]["bridge"] = fn("bridge")
732

    
733
    # Do not modify anymore, request data version 0 is deprecated
734
    return opcodes.OpCreateInstance(
735
      mode=constants.INSTANCE_CREATE,
736
      instance_name=fn('name'),
737
      disks=disks,
738
      disk_template=fn('disk_template'),
739
      os_type=fn('os'),
740
      pnode=fn('pnode', None),
741
      snode=fn('snode', None),
742
      iallocator=fn('iallocator', None),
743
      nics=nics,
744
      start=fn('start', True),
745
      ip_check=fn('ip_check', True),
746
      name_check=fn('name_check', True),
747
      wait_for_sync=True,
748
      hypervisor=fn('hypervisor', None),
749
      hvparams=hvparams,
750
      beparams=beparams,
751
      file_storage_dir=fn('file_storage_dir', None),
752
      file_driver=fn('file_driver', constants.FD_LOOP),
753
      dry_run=bool(self.dryRun()),
754
      )
755

    
756
  def POST(self):
757
    """Create an instance.
758

759
    @return: a job id
760

761
    """
762
    if not isinstance(self.request_body, dict):
763
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
764

    
765
    # Default to request data version 0
766
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
767

    
768
    if data_version == 0:
769
      op = self._ParseVersion0CreateRequest()
770
    elif data_version == 1:
771
      op = _ParseInstanceCreateRequestVersion1(self.request_body,
772
                                               self.dryRun())
773
    else:
774
      raise http.HttpBadRequest("Unsupported request data version %s" %
775
                                data_version)
776

    
777
    return baserlib.SubmitJob([op])
778

    
779

    
780
class R_2_instances_name(baserlib.R_Generic):
781
  """/2/instances/[instance_name] resources.
782

783
  """
784
  def GET(self):
785
    """Send information about an instance.
786

787
    """
788
    client = baserlib.GetClient()
789
    instance_name = self.items[0]
790

    
791
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
792
                                            names=[instance_name],
793
                                            fields=I_FIELDS,
794
                                            use_locking=self.useLocking())
795

    
796
    return baserlib.MapFields(I_FIELDS, result[0])
797

    
798
  def DELETE(self):
799
    """Delete an instance.
800

801
    """
802
    op = opcodes.OpRemoveInstance(instance_name=self.items[0],
803
                                  ignore_failures=False,
804
                                  dry_run=bool(self.dryRun()))
805
    return baserlib.SubmitJob([op])
806

    
807

    
808
class R_2_instances_name_info(baserlib.R_Generic):
809
  """/2/instances/[instance_name]/info resource.
810

811
  """
812
  def GET(self):
813
    """Request detailed instance information.
814

815
    """
816
    instance_name = self.items[0]
817
    static = bool(self._checkIntVariable("static", default=0))
818

    
819
    op = opcodes.OpQueryInstanceData(instances=[instance_name],
820
                                     static=static)
821
    return baserlib.SubmitJob([op])
822

    
823

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

827
  Implements an instance reboot.
828

829
  """
830
  def POST(self):
831
    """Reboot an instance.
832

833
    The URI takes type=[hard|soft|full] and
834
    ignore_secondaries=[False|True] parameters.
835

836
    """
837
    instance_name = self.items[0]
838
    reboot_type = self.queryargs.get('type',
839
                                     [constants.INSTANCE_REBOOT_HARD])[0]
840
    ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
841
    op = opcodes.OpRebootInstance(instance_name=instance_name,
842
                                  reboot_type=reboot_type,
843
                                  ignore_secondaries=ignore_secondaries,
844
                                  dry_run=bool(self.dryRun()))
845

    
846
    return baserlib.SubmitJob([op])
847

    
848

    
849
class R_2_instances_name_startup(baserlib.R_Generic):
850
  """/2/instances/[instance_name]/startup resource.
851

852
  Implements an instance startup.
853

854
  """
855
  def PUT(self):
856
    """Startup an instance.
857

858
    The URI takes force=[False|True] parameter to start the instance
859
    if even if secondary disks are failing.
860

861
    """
862
    instance_name = self.items[0]
863
    force_startup = bool(self._checkIntVariable('force'))
864
    op = opcodes.OpStartupInstance(instance_name=instance_name,
865
                                   force=force_startup,
866
                                   dry_run=bool(self.dryRun()))
867

    
868
    return baserlib.SubmitJob([op])
869

    
870

    
871
class R_2_instances_name_shutdown(baserlib.R_Generic):
872
  """/2/instances/[instance_name]/shutdown resource.
873

874
  Implements an instance shutdown.
875

876
  """
877
  def PUT(self):
878
    """Shutdown an instance.
879

880
    """
881
    instance_name = self.items[0]
882
    op = opcodes.OpShutdownInstance(instance_name=instance_name,
883
                                    dry_run=bool(self.dryRun()))
884

    
885
    return baserlib.SubmitJob([op])
886

    
887

    
888
def _ParseInstanceReinstallRequest(name, data):
889
  """Parses a request for reinstalling an instance.
890

891
  """
892
  if not isinstance(data, dict):
893
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
894

    
895
  ostype = baserlib.CheckParameter(data, "os")
896
  start = baserlib.CheckParameter(data, "start", exptype=bool,
897
                                  default=True)
898
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
899

    
900
  ops = [
901
    opcodes.OpShutdownInstance(instance_name=name),
902
    opcodes.OpReinstallInstance(instance_name=name, os_type=ostype,
903
                                osparams=osparams),
904
    ]
905

    
906
  if start:
907
    ops.append(opcodes.OpStartupInstance(instance_name=name, force=False))
908

    
909
  return ops
910

    
911

    
912
class R_2_instances_name_reinstall(baserlib.R_Generic):
913
  """/2/instances/[instance_name]/reinstall resource.
914

915
  Implements an instance reinstall.
916

917
  """
918
  def POST(self):
919
    """Reinstall an instance.
920

921
    The URI takes os=name and nostartup=[0|1] optional
922
    parameters. By default, the instance will be started
923
    automatically.
924

925
    """
926
    if self.request_body:
927
      if self.queryargs:
928
        raise http.HttpBadRequest("Can't combine query and body parameters")
929

    
930
      body = self.request_body
931
    else:
932
      if not self.queryargs:
933
        raise http.HttpBadRequest("Missing query parameters")
934
      # Legacy interface, do not modify/extend
935
      body = {
936
        "os": self._checkStringVariable("os"),
937
        "start": not self._checkIntVariable("nostartup"),
938
        }
939

    
940
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
941

    
942
    return baserlib.SubmitJob(ops)
943

    
944

    
945
class R_2_instances_name_replace_disks(baserlib.R_Generic):
946
  """/2/instances/[instance_name]/replace-disks resource.
947

948
  """
949
  def POST(self):
950
    """Replaces disks on an instance.
951

952
    """
953
    instance_name = self.items[0]
954
    remote_node = self._checkStringVariable("remote_node", default=None)
955
    mode = self._checkStringVariable("mode", default=None)
956
    raw_disks = self._checkStringVariable("disks", default=None)
957
    iallocator = self._checkStringVariable("iallocator", default=None)
958

    
959
    if raw_disks:
960
      try:
961
        disks = [int(part) for part in raw_disks.split(",")]
962
      except ValueError, err:
963
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
964
    else:
965
      disks = []
966

    
967
    op = opcodes.OpReplaceDisks(instance_name=instance_name,
968
                                remote_node=remote_node,
969
                                mode=mode,
970
                                disks=disks,
971
                                iallocator=iallocator)
972

    
973
    return baserlib.SubmitJob([op])
974

    
975

    
976
class R_2_instances_name_activate_disks(baserlib.R_Generic):
977
  """/2/instances/[instance_name]/activate-disks resource.
978

979
  """
980
  def PUT(self):
981
    """Activate disks for an instance.
982

983
    The URI might contain ignore_size to ignore current recorded size.
984

985
    """
986
    instance_name = self.items[0]
987
    ignore_size = bool(self._checkIntVariable('ignore_size'))
988

    
989
    op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
990
                                         ignore_size=ignore_size)
991

    
992
    return baserlib.SubmitJob([op])
993

    
994

    
995
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
996
  """/2/instances/[instance_name]/deactivate-disks resource.
997

998
  """
999
  def PUT(self):
1000
    """Deactivate disks for an instance.
1001

1002
    """
1003
    instance_name = self.items[0]
1004

    
1005
    op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
1006

    
1007
    return baserlib.SubmitJob([op])
1008

    
1009

    
1010
class R_2_instances_name_prepare_export(baserlib.R_Generic):
1011
  """/2/instances/[instance_name]/prepare-export resource.
1012

1013
  """
1014
  def PUT(self):
1015
    """Prepares an export for an instance.
1016

1017
    @return: a job id
1018

1019
    """
1020
    instance_name = self.items[0]
1021
    mode = self._checkStringVariable("mode")
1022

    
1023
    op = opcodes.OpPrepareExport(instance_name=instance_name,
1024
                                 mode=mode)
1025

    
1026
    return baserlib.SubmitJob([op])
1027

    
1028

    
1029
def _ParseExportInstanceRequest(name, data):
1030
  """Parses a request for an instance export.
1031

1032
  @rtype: L{opcodes.OpExportInstance}
1033
  @return: Instance export opcode
1034

1035
  """
1036
  mode = baserlib.CheckParameter(data, "mode",
1037
                                 default=constants.EXPORT_MODE_LOCAL)
1038
  target_node = baserlib.CheckParameter(data, "destination")
1039
  shutdown = baserlib.CheckParameter(data, "shutdown", exptype=bool)
1040
  remove_instance = baserlib.CheckParameter(data, "remove_instance",
1041
                                            exptype=bool, default=False)
1042
  x509_key_name = baserlib.CheckParameter(data, "x509_key_name", default=None)
1043
  destination_x509_ca = baserlib.CheckParameter(data, "destination_x509_ca",
1044
                                                default=None)
1045

    
1046
  return opcodes.OpExportInstance(instance_name=name,
1047
                                  mode=mode,
1048
                                  target_node=target_node,
1049
                                  shutdown=shutdown,
1050
                                  remove_instance=remove_instance,
1051
                                  x509_key_name=x509_key_name,
1052
                                  destination_x509_ca=destination_x509_ca)
1053

    
1054

    
1055
class R_2_instances_name_export(baserlib.R_Generic):
1056
  """/2/instances/[instance_name]/export resource.
1057

1058
  """
1059
  def PUT(self):
1060
    """Exports an instance.
1061

1062
    @return: a job id
1063

1064
    """
1065
    if not isinstance(self.request_body, dict):
1066
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1067

    
1068
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1069

    
1070
    return baserlib.SubmitJob([op])
1071

    
1072

    
1073
def _ParseMigrateInstanceRequest(name, data):
1074
  """Parses a request for an instance migration.
1075

1076
  @rtype: L{opcodes.OpMigrateInstance}
1077
  @return: Instance migration opcode
1078

1079
  """
1080
  mode = baserlib.CheckParameter(data, "mode", default=None)
1081
  cleanup = baserlib.CheckParameter(data, "cleanup", exptype=bool,
1082
                                    default=False)
1083

    
1084
  return opcodes.OpMigrateInstance(instance_name=name, mode=mode,
1085
                                   cleanup=cleanup)
1086

    
1087

    
1088
class R_2_instances_name_migrate(baserlib.R_Generic):
1089
  """/2/instances/[instance_name]/migrate resource.
1090

1091
  """
1092
  def PUT(self):
1093
    """Migrates an instance.
1094

1095
    @return: a job id
1096

1097
    """
1098
    baserlib.CheckType(self.request_body, dict, "Body contents")
1099

    
1100
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1101

    
1102
    return baserlib.SubmitJob([op])
1103

    
1104

    
1105
def _ParseRenameInstanceRequest(name, data):
1106
  """Parses a request for renaming an instance.
1107

1108
  @rtype: L{opcodes.OpRenameInstance}
1109
  @return: Instance rename opcode
1110

1111
  """
1112
  new_name = baserlib.CheckParameter(data, "new_name")
1113
  ip_check = baserlib.CheckParameter(data, "ip_check", default=True)
1114
  name_check = baserlib.CheckParameter(data, "name_check", default=True)
1115

    
1116
  return opcodes.OpRenameInstance(instance_name=name, new_name=new_name,
1117
                                  name_check=name_check, ip_check=ip_check)
1118

    
1119

    
1120
class R_2_instances_name_rename(baserlib.R_Generic):
1121
  """/2/instances/[instance_name]/rename resource.
1122

1123
  """
1124
  def PUT(self):
1125
    """Changes the name of an instance.
1126

1127
    @return: a job id
1128

1129
    """
1130
    baserlib.CheckType(self.request_body, dict, "Body contents")
1131

    
1132
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1133

    
1134
    return baserlib.SubmitJob([op])
1135

    
1136

    
1137
def _ParseModifyInstanceRequest(name, data):
1138
  """Parses a request for modifying an instance.
1139

1140
  @rtype: L{opcodes.OpSetInstanceParams}
1141
  @return: Instance modify opcode
1142

1143
  """
1144
  osparams = baserlib.CheckParameter(data, "osparams", default={})
1145
  force = baserlib.CheckParameter(data, "force", default=False)
1146
  nics = baserlib.CheckParameter(data, "nics", default=[])
1147
  disks = baserlib.CheckParameter(data, "disks", default=[])
1148
  disk_template = baserlib.CheckParameter(data, "disk_template", default=None)
1149
  remote_node = baserlib.CheckParameter(data, "remote_node", default=None)
1150
  os_name = baserlib.CheckParameter(data, "os_name", default=None)
1151
  force_variant = baserlib.CheckParameter(data, "force_variant", default=False)
1152

    
1153
  # HV/BE parameters
1154
  hvparams = baserlib.CheckParameter(data, "hvparams", default={})
1155
  utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES,
1156
                      allowed_values=[constants.VALUE_DEFAULT])
1157

    
1158
  beparams = baserlib.CheckParameter(data, "beparams", default={})
1159
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES,
1160
                      allowed_values=[constants.VALUE_DEFAULT])
1161

    
1162
  return opcodes.OpSetInstanceParams(instance_name=name, hvparams=hvparams,
1163
                                     beparams=beparams, osparams=osparams,
1164
                                     force=force, nics=nics, disks=disks,
1165
                                     disk_template=disk_template,
1166
                                     remote_node=remote_node, os_name=os_name,
1167
                                     force_variant=force_variant)
1168

    
1169

    
1170
class R_2_instances_name_modify(baserlib.R_Generic):
1171
  """/2/instances/[instance_name]/modify resource.
1172

1173
  """
1174
  def PUT(self):
1175
    """Changes some parameters of an instance.
1176

1177
    @return: a job id
1178

1179
    """
1180
    baserlib.CheckType(self.request_body, dict, "Body contents")
1181

    
1182
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1183

    
1184
    return baserlib.SubmitJob([op])
1185

    
1186

    
1187
class _R_Tags(baserlib.R_Generic):
1188
  """ Quasiclass for tagging resources
1189

1190
  Manages tags. When inheriting this class you must define the
1191
  TAG_LEVEL for it.
1192

1193
  """
1194
  TAG_LEVEL = None
1195

    
1196
  def __init__(self, items, queryargs, req):
1197
    """A tag resource constructor.
1198

1199
    We have to override the default to sort out cluster naming case.
1200

1201
    """
1202
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1203

    
1204
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1205
      self.name = None
1206
    else:
1207
      self.name = items[0]
1208

    
1209
  def GET(self):
1210
    """Returns a list of tags.
1211

1212
    Example: ["tag1", "tag2", "tag3"]
1213

1214
    """
1215
    # pylint: disable-msg=W0212
1216
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1217

    
1218
  def PUT(self):
1219
    """Add a set of tags.
1220

1221
    The request as a list of strings should be PUT to this URI. And
1222
    you'll have back a job id.
1223

1224
    """
1225
    # pylint: disable-msg=W0212
1226
    if 'tag' not in self.queryargs:
1227
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1228
                                " the 'tag' parameter")
1229
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1230
                              self.queryargs['tag'], name=self.name,
1231
                              dry_run=bool(self.dryRun()))
1232

    
1233
  def DELETE(self):
1234
    """Delete a tag.
1235

1236
    In order to delete a set of tags, the DELETE
1237
    request should be addressed to URI like:
1238
    /tags?tag=[tag]&tag=[tag]
1239

1240
    """
1241
    # pylint: disable-msg=W0212
1242
    if 'tag' not in self.queryargs:
1243
      # no we not gonna delete all tags
1244
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1245
                                " tag(s) using the 'tag' parameter")
1246
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1247
                                 self.queryargs['tag'],
1248
                                 name=self.name,
1249
                                 dry_run=bool(self.dryRun()))
1250

    
1251

    
1252
class R_2_instances_name_tags(_R_Tags):
1253
  """ /2/instances/[instance_name]/tags resource.
1254

1255
  Manages per-instance tags.
1256

1257
  """
1258
  TAG_LEVEL = constants.TAG_INSTANCE
1259

    
1260

    
1261
class R_2_nodes_name_tags(_R_Tags):
1262
  """ /2/nodes/[node_name]/tags resource.
1263

1264
  Manages per-node tags.
1265

1266
  """
1267
  TAG_LEVEL = constants.TAG_NODE
1268

    
1269

    
1270
class R_2_tags(_R_Tags):
1271
  """ /2/instances/tags resource.
1272

1273
  Manages cluster tags.
1274

1275
  """
1276
  TAG_LEVEL = constants.TAG_CLUSTER