Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 62e999a5

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

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

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

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

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

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

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

    
108

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

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

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

120
    """
121
    return constants.RAPI_VERSION
122

    
123

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

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

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

    
136

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

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

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

    
148

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

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

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

159
    Example: ["debian-etch"]
160

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

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

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

    
176
    return os_names
177

    
178

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

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

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

    
190

    
191
class R_2_cluster_modify(baserlib.R_Generic):
192
  """/2/modify resource.
193

194
  """
195
  def PUT(self):
196
    """Modifies cluster parameters.
197

198
    @return: a job id
199

200
    """
201
    op = baserlib.FillOpcode(opcodes.OpSetClusterParams, self.request_body,
202
                             None)
203

    
204
    return baserlib.SubmitJob([op])
205

    
206

    
207
class R_2_jobs(baserlib.R_Generic):
208
  """/2/jobs resource.
209

210
  """
211
  @staticmethod
212
  def GET():
213
    """Returns a dictionary of jobs.
214

215
    @return: a dictionary with jobs id and uri.
216

217
    """
218
    fields = ["id"]
219
    cl = baserlib.GetClient()
220
    # Convert the list of lists to the list of ids
221
    result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
222
    return baserlib.BuildUriList(result, "/2/jobs/%s",
223
                                 uri_fields=("id", "uri"))
224

    
225

    
226
class R_2_jobs_id(baserlib.R_Generic):
227
  """/2/jobs/[job_id] resource.
228

229
  """
230
  def GET(self):
231
    """Returns a job status.
232

233
    @return: a dictionary with job parameters.
234
        The result includes:
235
            - id: job ID as a number
236
            - status: current job status as a string
237
            - ops: involved OpCodes as a list of dictionaries for each
238
              opcodes in the job
239
            - opstatus: OpCodes status as a list
240
            - opresult: OpCodes results as a list of lists
241

242
    """
243
    fields = ["id", "ops", "status", "summary",
244
              "opstatus", "opresult", "oplog",
245
              "received_ts", "start_ts", "end_ts",
246
              ]
247
    job_id = self.items[0]
248
    result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
249
    if result is None:
250
      raise http.HttpNotFound()
251
    return baserlib.MapFields(fields, result)
252

    
253
  def DELETE(self):
254
    """Cancel not-yet-started job.
255

256
    """
257
    job_id = self.items[0]
258
    result = baserlib.GetClient().CancelJob(job_id)
259
    return result
260

    
261

    
262
class R_2_jobs_id_wait(baserlib.R_Generic):
263
  """/2/jobs/[job_id]/wait resource.
264

265
  """
266
  # WaitForJobChange provides access to sensitive information and blocks
267
  # machine resources (it's a blocking RAPI call), hence restricting access.
268
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
269

    
270
  def GET(self):
271
    """Waits for job changes.
272

273
    """
274
    job_id = self.items[0]
275

    
276
    fields = self.getBodyParameter("fields")
277
    prev_job_info = self.getBodyParameter("previous_job_info", None)
278
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
279

    
280
    if not isinstance(fields, list):
281
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
282

    
283
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
284
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
285
                                " be a list")
286

    
287
    if not (prev_log_serial is None or
288
            isinstance(prev_log_serial, (int, long))):
289
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
290
                                " be a number")
291

    
292
    client = baserlib.GetClient()
293
    result = client.WaitForJobChangeOnce(job_id, fields,
294
                                         prev_job_info, prev_log_serial,
295
                                         timeout=_WFJC_TIMEOUT)
296
    if not result:
297
      raise http.HttpNotFound()
298

    
299
    if result == constants.JOB_NOTCHANGED:
300
      # No changes
301
      return None
302

    
303
    (job_info, log_entries) = result
304

    
305
    return {
306
      "job_info": job_info,
307
      "log_entries": log_entries,
308
      }
309

    
310

    
311
class R_2_nodes(baserlib.R_Generic):
312
  """/2/nodes resource.
313

314
  """
315
  def GET(self):
316
    """Returns a list of all nodes.
317

318
    """
319
    client = baserlib.GetClient()
320

    
321
    if self.useBulk():
322
      bulkdata = client.QueryNodes([], N_FIELDS, False)
323
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
324
    else:
325
      nodesdata = client.QueryNodes([], ["name"], False)
326
      nodeslist = [row[0] for row in nodesdata]
327
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
328
                                   uri_fields=("id", "uri"))
329

    
330

    
331
class R_2_nodes_name(baserlib.R_Generic):
332
  """/2/nodes/[node_name] resources.
333

334
  """
335
  def GET(self):
336
    """Send information about a node.
337

338
    """
339
    node_name = self.items[0]
340
    client = baserlib.GetClient()
341

    
342
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
343
                                            names=[node_name], fields=N_FIELDS,
344
                                            use_locking=self.useLocking())
345

    
346
    return baserlib.MapFields(N_FIELDS, result[0])
347

    
348

    
349
class R_2_nodes_name_role(baserlib.R_Generic):
350
  """ /2/nodes/[node_name]/role resource.
351

352
  """
353
  def GET(self):
354
    """Returns the current node role.
355

356
    @return: Node role
357

358
    """
359
    node_name = self.items[0]
360
    client = baserlib.GetClient()
361
    result = client.QueryNodes(names=[node_name], fields=["role"],
362
                               use_locking=self.useLocking())
363

    
364
    return _NR_MAP[result[0][0]]
365

    
366
  def PUT(self):
367
    """Sets the node role.
368

369
    @return: a job id
370

371
    """
372
    if not isinstance(self.request_body, basestring):
373
      raise http.HttpBadRequest("Invalid body contents, not a string")
374

    
375
    node_name = self.items[0]
376
    role = self.request_body
377

    
378
    if role == _NR_REGULAR:
379
      candidate = False
380
      offline = False
381
      drained = False
382

    
383
    elif role == _NR_MASTER_CANDIATE:
384
      candidate = True
385
      offline = drained = None
386

    
387
    elif role == _NR_DRAINED:
388
      drained = True
389
      candidate = offline = None
390

    
391
    elif role == _NR_OFFLINE:
392
      offline = True
393
      candidate = drained = None
394

    
395
    else:
396
      raise http.HttpBadRequest("Can't set '%s' role" % role)
397

    
398
    op = opcodes.OpSetNodeParams(node_name=node_name,
399
                                 master_candidate=candidate,
400
                                 offline=offline,
401
                                 drained=drained,
402
                                 force=bool(self.useForce()))
403

    
404
    return baserlib.SubmitJob([op])
405

    
406

    
407
class R_2_nodes_name_evacuate(baserlib.R_Generic):
408
  """/2/nodes/[node_name]/evacuate resource.
409

410
  """
411
  def POST(self):
412
    """Evacuate all secondary instances off a node.
413

414
    """
415
    node_name = self.items[0]
416
    remote_node = self._checkStringVariable("remote_node", default=None)
417
    iallocator = self._checkStringVariable("iallocator", default=None)
418
    early_r = bool(self._checkIntVariable("early_release", default=0))
419
    dry_run = bool(self.dryRun())
420

    
421
    cl = baserlib.GetClient()
422

    
423
    op = opcodes.OpNodeEvacuationStrategy(nodes=[node_name],
424
                                          iallocator=iallocator,
425
                                          remote_node=remote_node)
426

    
427
    job_id = baserlib.SubmitJob([op], cl)
428
    # we use custom feedback function, instead of print we log the status
429
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
430

    
431
    jobs = []
432
    for iname, node in result:
433
      if dry_run:
434
        jid = None
435
      else:
436
        op = opcodes.OpReplaceDisks(instance_name=iname,
437
                                    remote_node=node, disks=[],
438
                                    mode=constants.REPLACE_DISK_CHG,
439
                                    early_release=early_r)
440
        jid = baserlib.SubmitJob([op])
441
      jobs.append((jid, iname, node))
442

    
443
    return jobs
444

    
445

    
446
class R_2_nodes_name_migrate(baserlib.R_Generic):
447
  """/2/nodes/[node_name]/migrate resource.
448

449
  """
450
  def POST(self):
451
    """Migrate all primary instances from a node.
452

453
    """
454
    node_name = self.items[0]
455

    
456
    if "live" in self.queryargs and "mode" in self.queryargs:
457
      raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
458
                                " be passed")
459
    elif "live" in self.queryargs:
460
      if self._checkIntVariable("live", default=1):
461
        mode = constants.HT_MIGRATION_LIVE
462
      else:
463
        mode = constants.HT_MIGRATION_NONLIVE
464
    else:
465
      mode = self._checkStringVariable("mode", default=None)
466

    
467
    op = opcodes.OpMigrateNode(node_name=node_name, mode=mode)
468

    
469
    return baserlib.SubmitJob([op])
470

    
471

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

475
  """
476
  # LUQueryNodeStorage acquires locks, hence restricting access to GET
477
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
478

    
479
  def GET(self):
480
    node_name = self.items[0]
481

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

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

    
492
    op = opcodes.OpQueryNodeStorage(nodes=[node_name],
493
                                    storage_type=storage_type,
494
                                    output_fields=output_fields.split(","))
495
    return baserlib.SubmitJob([op])
496

    
497

    
498
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
499
  """/2/nodes/[node_name]/storage/modify ressource.
500

501
  """
502
  def PUT(self):
503
    node_name = self.items[0]
504

    
505
    storage_type = self._checkStringVariable("storage_type", None)
506
    if not storage_type:
507
      raise http.HttpBadRequest("Missing the required 'storage_type'"
508
                                " parameter")
509

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

    
515
    changes = {}
516

    
517
    if "allocatable" in self.queryargs:
518
      changes[constants.SF_ALLOCATABLE] = \
519
        bool(self._checkIntVariable("allocatable", default=1))
520

    
521
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
522
                                     storage_type=storage_type,
523
                                     name=name,
524
                                     changes=changes)
525
    return baserlib.SubmitJob([op])
526

    
527

    
528
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
529
  """/2/nodes/[node_name]/storage/repair ressource.
530

531
  """
532
  def PUT(self):
533
    node_name = self.items[0]
534

    
535
    storage_type = self._checkStringVariable("storage_type", None)
536
    if not storage_type:
537
      raise http.HttpBadRequest("Missing the required 'storage_type'"
538
                                " parameter")
539

    
540
    name = self._checkStringVariable("name", None)
541
    if not name:
542
      raise http.HttpBadRequest("Missing the required 'name'"
543
                                " parameter")
544

    
545
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
546
                                     storage_type=storage_type,
547
                                     name=name)
548
    return baserlib.SubmitJob([op])
549

    
550

    
551
def _ParseCreateGroupRequest(data, dry_run):
552
  """Parses a request for creating a node group.
553

554
  @rtype: L{opcodes.OpAddGroup}
555
  @return: Group creation opcode
556

557
  """
558
  group_name = baserlib.CheckParameter(data, "name")
559
  alloc_policy = baserlib.CheckParameter(data, "alloc_policy", default=None)
560

    
561
  return opcodes.OpAddGroup(group_name=group_name,
562
                            alloc_policy=alloc_policy,
563
                            dry_run=dry_run)
564

    
565

    
566
class R_2_groups(baserlib.R_Generic):
567
  """/2/groups resource.
568

569
  """
570
  def GET(self):
571
    """Returns a list of all node groups.
572

573
    """
574
    client = baserlib.GetClient()
575

    
576
    if self.useBulk():
577
      bulkdata = client.QueryGroups([], G_FIELDS, False)
578
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
579
    else:
580
      data = client.QueryGroups([], ["name"], False)
581
      groupnames = [row[0] for row in data]
582
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
583
                                   uri_fields=("name", "uri"))
584

    
585
  def POST(self):
586
    """Create a node group.
587

588
    @return: a job id
589

590
    """
591
    baserlib.CheckType(self.request_body, dict, "Body contents")
592
    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
593
    return baserlib.SubmitJob([op])
594

    
595

    
596
class R_2_groups_name(baserlib.R_Generic):
597
  """/2/groups/[group_name] resources.
598

599
  """
600
  def GET(self):
601
    """Send information about a node group.
602

603
    """
604
    group_name = self.items[0]
605
    client = baserlib.GetClient()
606

    
607
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
608
                                            names=[group_name], fields=G_FIELDS,
609
                                            use_locking=self.useLocking())
610

    
611
    return baserlib.MapFields(G_FIELDS, result[0])
612

    
613
  def DELETE(self):
614
    """Delete a node group.
615

616
    """
617
    op = opcodes.OpRemoveGroup(group_name=self.items[0],
618
                               dry_run=bool(self.dryRun()))
619

    
620
    return baserlib.SubmitJob([op])
621

    
622

    
623
def _ParseModifyGroupRequest(name, data):
624
  """Parses a request for modifying a node group.
625

626
  @rtype: L{opcodes.OpSetGroupParams}
627
  @return: Group modify opcode
628

629
  """
630
  alloc_policy = baserlib.CheckParameter(data, "alloc_policy", default=None)
631
  return opcodes.OpSetGroupParams(group_name=name, alloc_policy=alloc_policy)
632

    
633

    
634
class R_2_groups_name_modify(baserlib.R_Generic):
635
  """/2/groups/[group_name]/modify resource.
636

637
  """
638
  def PUT(self):
639
    """Changes some parameters of node group.
640

641
    @return: a job id
642

643
    """
644
    baserlib.CheckType(self.request_body, dict, "Body contents")
645

    
646
    op = _ParseModifyGroupRequest(self.items[0], self.request_body)
647

    
648
    return baserlib.SubmitJob([op])
649

    
650

    
651
def _ParseRenameGroupRequest(name, data, dry_run):
652
  """Parses a request for renaming a node group.
653

654
  @type name: string
655
  @param name: name of the node group to rename
656
  @type data: dict
657
  @param data: the body received by the rename request
658
  @type dry_run: bool
659
  @param dry_run: whether to perform a dry run
660

661
  @rtype: L{opcodes.OpRenameGroup}
662
  @return: Node group rename opcode
663

664
  """
665
  old_name = name
666
  new_name = baserlib.CheckParameter(data, "new_name")
667

    
668
  return opcodes.OpRenameGroup(old_name=old_name, new_name=new_name,
669
                               dry_run=dry_run)
670

    
671

    
672
class R_2_groups_name_rename(baserlib.R_Generic):
673
  """/2/groups/[groupe_name]/rename resource.
674

675
  """
676
  def PUT(self):
677
    """Changes the name of a node group.
678

679
    @return: a job id
680

681
    """
682
    baserlib.CheckType(self.request_body, dict, "Body contents")
683
    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
684
                                  self.dryRun())
685
    return baserlib.SubmitJob([op])
686

    
687

    
688
def _ParseInstanceCreateRequestVersion1(data, dry_run):
689
  """Parses an instance creation request version 1.
690

691
  @rtype: L{opcodes.OpCreateInstance}
692
  @return: Instance creation opcode
693

694
  """
695
  # Disks
696
  disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
697

    
698
  disks = []
699
  for idx, i in enumerate(disks_input):
700
    baserlib.CheckType(i, dict, "Disk %d specification" % idx)
701

    
702
    # Size is mandatory
703
    try:
704
      size = i[constants.IDISK_SIZE]
705
    except KeyError:
706
      raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
707
                                " size" % idx)
708

    
709
    disk = {
710
      constants.IDISK_SIZE: size,
711
      }
712

    
713
    # Optional disk access mode
714
    try:
715
      disk_access = i[constants.IDISK_MODE]
716
    except KeyError:
717
      pass
718
    else:
719
      disk[constants.IDISK_MODE] = disk_access
720

    
721
    disks.append(disk)
722

    
723
  assert len(disks_input) == len(disks)
724

    
725
  # Network interfaces
726
  nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
727

    
728
  nics = []
729
  for idx, i in enumerate(nics_input):
730
    baserlib.CheckType(i, dict, "NIC %d specification" % idx)
731

    
732
    nic = {}
733

    
734
    for field in constants.INIC_PARAMS:
735
      try:
736
        value = i[field]
737
      except KeyError:
738
        continue
739

    
740
      nic[field] = value
741

    
742
    nics.append(nic)
743

    
744
  assert len(nics_input) == len(nics)
745

    
746
  # HV/BE parameters
747
  hvparams = baserlib.CheckParameter(data, "hvparams", default={})
748
  utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
749

    
750
  beparams = baserlib.CheckParameter(data, "beparams", default={})
751
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
752

    
753
  return opcodes.OpCreateInstance(
754
    mode=baserlib.CheckParameter(data, "mode"),
755
    instance_name=baserlib.CheckParameter(data, "name"),
756
    os_type=baserlib.CheckParameter(data, "os"),
757
    osparams=baserlib.CheckParameter(data, "osparams", default={}),
758
    force_variant=baserlib.CheckParameter(data, "force_variant",
759
                                          default=False),
760
    no_install=baserlib.CheckParameter(data, "no_install", default=False),
761
    pnode=baserlib.CheckParameter(data, "pnode", default=None),
762
    snode=baserlib.CheckParameter(data, "snode", default=None),
763
    disk_template=baserlib.CheckParameter(data, "disk_template"),
764
    disks=disks,
765
    nics=nics,
766
    src_node=baserlib.CheckParameter(data, "src_node", default=None),
767
    src_path=baserlib.CheckParameter(data, "src_path", default=None),
768
    start=baserlib.CheckParameter(data, "start", default=True),
769
    wait_for_sync=True,
770
    ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
771
    name_check=baserlib.CheckParameter(data, "name_check", default=True),
772
    file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
773
                                             default=None),
774
    file_driver=baserlib.CheckParameter(data, "file_driver",
775
                                        default=constants.FD_LOOP),
776
    source_handshake=baserlib.CheckParameter(data, "source_handshake",
777
                                             default=None),
778
    source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
779
                                           default=None),
780
    source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
781
                                                 default=None),
782
    iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
783
    hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
784
    hvparams=hvparams,
785
    beparams=beparams,
786
    dry_run=dry_run,
787
    )
788

    
789

    
790
class R_2_instances(baserlib.R_Generic):
791
  """/2/instances resource.
792

793
  """
794
  def GET(self):
795
    """Returns a list of all available instances.
796

797
    """
798
    client = baserlib.GetClient()
799

    
800
    use_locking = self.useLocking()
801
    if self.useBulk():
802
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
803
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
804
    else:
805
      instancesdata = client.QueryInstances([], ["name"], use_locking)
806
      instanceslist = [row[0] for row in instancesdata]
807
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
808
                                   uri_fields=("id", "uri"))
809

    
810
  def _ParseVersion0CreateRequest(self):
811
    """Parses an instance creation request version 0.
812

813
    Request data version 0 is deprecated and should not be used anymore.
814

815
    @rtype: L{opcodes.OpCreateInstance}
816
    @return: Instance creation opcode
817

818
    """
819
    # Do not modify anymore, request data version 0 is deprecated
820
    beparams = baserlib.MakeParamsDict(self.request_body,
821
                                       constants.BES_PARAMETERS)
822
    hvparams = baserlib.MakeParamsDict(self.request_body,
823
                                       constants.HVS_PARAMETERS)
824
    fn = self.getBodyParameter
825

    
826
    # disk processing
827
    disk_data = fn('disks')
828
    if not isinstance(disk_data, list):
829
      raise http.HttpBadRequest("The 'disks' parameter should be a list")
830
    disks = []
831
    for idx, d in enumerate(disk_data):
832
      if not isinstance(d, int):
833
        raise http.HttpBadRequest("Disk %d specification wrong: should"
834
                                  " be an integer" % idx)
835
      disks.append({"size": d})
836

    
837
    # nic processing (one nic only)
838
    nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
839
    if fn("ip", None) is not None:
840
      nics[0]["ip"] = fn("ip")
841
    if fn("mode", None) is not None:
842
      nics[0]["mode"] = fn("mode")
843
    if fn("link", None) is not None:
844
      nics[0]["link"] = fn("link")
845
    if fn("bridge", None) is not None:
846
      nics[0]["bridge"] = fn("bridge")
847

    
848
    # Do not modify anymore, request data version 0 is deprecated
849
    return opcodes.OpCreateInstance(
850
      mode=constants.INSTANCE_CREATE,
851
      instance_name=fn('name'),
852
      disks=disks,
853
      disk_template=fn('disk_template'),
854
      os_type=fn('os'),
855
      pnode=fn('pnode', None),
856
      snode=fn('snode', None),
857
      iallocator=fn('iallocator', None),
858
      nics=nics,
859
      start=fn('start', True),
860
      ip_check=fn('ip_check', True),
861
      name_check=fn('name_check', True),
862
      wait_for_sync=True,
863
      hypervisor=fn('hypervisor', None),
864
      hvparams=hvparams,
865
      beparams=beparams,
866
      file_storage_dir=fn('file_storage_dir', None),
867
      file_driver=fn('file_driver', constants.FD_LOOP),
868
      dry_run=bool(self.dryRun()),
869
      )
870

    
871
  def POST(self):
872
    """Create an instance.
873

874
    @return: a job id
875

876
    """
877
    if not isinstance(self.request_body, dict):
878
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
879

    
880
    # Default to request data version 0
881
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
882

    
883
    if data_version == 0:
884
      op = self._ParseVersion0CreateRequest()
885
    elif data_version == 1:
886
      op = _ParseInstanceCreateRequestVersion1(self.request_body,
887
                                               self.dryRun())
888
    else:
889
      raise http.HttpBadRequest("Unsupported request data version %s" %
890
                                data_version)
891

    
892
    return baserlib.SubmitJob([op])
893

    
894

    
895
class R_2_instances_name(baserlib.R_Generic):
896
  """/2/instances/[instance_name] resources.
897

898
  """
899
  def GET(self):
900
    """Send information about an instance.
901

902
    """
903
    client = baserlib.GetClient()
904
    instance_name = self.items[0]
905

    
906
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
907
                                            names=[instance_name],
908
                                            fields=I_FIELDS,
909
                                            use_locking=self.useLocking())
910

    
911
    return baserlib.MapFields(I_FIELDS, result[0])
912

    
913
  def DELETE(self):
914
    """Delete an instance.
915

916
    """
917
    op = opcodes.OpRemoveInstance(instance_name=self.items[0],
918
                                  ignore_failures=False,
919
                                  dry_run=bool(self.dryRun()))
920
    return baserlib.SubmitJob([op])
921

    
922

    
923
class R_2_instances_name_info(baserlib.R_Generic):
924
  """/2/instances/[instance_name]/info resource.
925

926
  """
927
  def GET(self):
928
    """Request detailed instance information.
929

930
    """
931
    instance_name = self.items[0]
932
    static = bool(self._checkIntVariable("static", default=0))
933

    
934
    op = opcodes.OpQueryInstanceData(instances=[instance_name],
935
                                     static=static)
936
    return baserlib.SubmitJob([op])
937

    
938

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

942
  Implements an instance reboot.
943

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

948
    The URI takes type=[hard|soft|full] and
949
    ignore_secondaries=[False|True] parameters.
950

951
    """
952
    instance_name = self.items[0]
953
    reboot_type = self.queryargs.get('type',
954
                                     [constants.INSTANCE_REBOOT_HARD])[0]
955
    ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
956
    op = opcodes.OpRebootInstance(instance_name=instance_name,
957
                                  reboot_type=reboot_type,
958
                                  ignore_secondaries=ignore_secondaries,
959
                                  dry_run=bool(self.dryRun()))
960

    
961
    return baserlib.SubmitJob([op])
962

    
963

    
964
class R_2_instances_name_startup(baserlib.R_Generic):
965
  """/2/instances/[instance_name]/startup resource.
966

967
  Implements an instance startup.
968

969
  """
970
  def PUT(self):
971
    """Startup an instance.
972

973
    The URI takes force=[False|True] parameter to start the instance
974
    if even if secondary disks are failing.
975

976
    """
977
    instance_name = self.items[0]
978
    force_startup = bool(self._checkIntVariable('force'))
979
    op = opcodes.OpStartupInstance(instance_name=instance_name,
980
                                   force=force_startup,
981
                                   dry_run=bool(self.dryRun()))
982

    
983
    return baserlib.SubmitJob([op])
984

    
985

    
986
class R_2_instances_name_shutdown(baserlib.R_Generic):
987
  """/2/instances/[instance_name]/shutdown resource.
988

989
  Implements an instance shutdown.
990

991
  """
992
  def PUT(self):
993
    """Shutdown an instance.
994

995
    """
996
    instance_name = self.items[0]
997
    op = opcodes.OpShutdownInstance(instance_name=instance_name,
998
                                    dry_run=bool(self.dryRun()))
999

    
1000
    return baserlib.SubmitJob([op])
1001

    
1002

    
1003
def _ParseInstanceReinstallRequest(name, data):
1004
  """Parses a request for reinstalling an instance.
1005

1006
  """
1007
  if not isinstance(data, dict):
1008
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1009

    
1010
  ostype = baserlib.CheckParameter(data, "os")
1011
  start = baserlib.CheckParameter(data, "start", exptype=bool,
1012
                                  default=True)
1013
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
1014

    
1015
  ops = [
1016
    opcodes.OpShutdownInstance(instance_name=name),
1017
    opcodes.OpReinstallInstance(instance_name=name, os_type=ostype,
1018
                                osparams=osparams),
1019
    ]
1020

    
1021
  if start:
1022
    ops.append(opcodes.OpStartupInstance(instance_name=name, force=False))
1023

    
1024
  return ops
1025

    
1026

    
1027
class R_2_instances_name_reinstall(baserlib.R_Generic):
1028
  """/2/instances/[instance_name]/reinstall resource.
1029

1030
  Implements an instance reinstall.
1031

1032
  """
1033
  def POST(self):
1034
    """Reinstall an instance.
1035

1036
    The URI takes os=name and nostartup=[0|1] optional
1037
    parameters. By default, the instance will be started
1038
    automatically.
1039

1040
    """
1041
    if self.request_body:
1042
      if self.queryargs:
1043
        raise http.HttpBadRequest("Can't combine query and body parameters")
1044

    
1045
      body = self.request_body
1046
    else:
1047
      if not self.queryargs:
1048
        raise http.HttpBadRequest("Missing query parameters")
1049
      # Legacy interface, do not modify/extend
1050
      body = {
1051
        "os": self._checkStringVariable("os"),
1052
        "start": not self._checkIntVariable("nostartup"),
1053
        }
1054

    
1055
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1056

    
1057
    return baserlib.SubmitJob(ops)
1058

    
1059

    
1060
class R_2_instances_name_replace_disks(baserlib.R_Generic):
1061
  """/2/instances/[instance_name]/replace-disks resource.
1062

1063
  """
1064
  def POST(self):
1065
    """Replaces disks on an instance.
1066

1067
    """
1068
    instance_name = self.items[0]
1069
    remote_node = self._checkStringVariable("remote_node", default=None)
1070
    mode = self._checkStringVariable("mode", default=None)
1071
    raw_disks = self._checkStringVariable("disks", default=None)
1072
    iallocator = self._checkStringVariable("iallocator", default=None)
1073

    
1074
    if raw_disks:
1075
      try:
1076
        disks = [int(part) for part in raw_disks.split(",")]
1077
      except ValueError, err:
1078
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1079
    else:
1080
      disks = []
1081

    
1082
    op = opcodes.OpReplaceDisks(instance_name=instance_name,
1083
                                remote_node=remote_node,
1084
                                mode=mode,
1085
                                disks=disks,
1086
                                iallocator=iallocator)
1087

    
1088
    return baserlib.SubmitJob([op])
1089

    
1090

    
1091
class R_2_instances_name_activate_disks(baserlib.R_Generic):
1092
  """/2/instances/[instance_name]/activate-disks resource.
1093

1094
  """
1095
  def PUT(self):
1096
    """Activate disks for an instance.
1097

1098
    The URI might contain ignore_size to ignore current recorded size.
1099

1100
    """
1101
    instance_name = self.items[0]
1102
    ignore_size = bool(self._checkIntVariable('ignore_size'))
1103

    
1104
    op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
1105
                                         ignore_size=ignore_size)
1106

    
1107
    return baserlib.SubmitJob([op])
1108

    
1109

    
1110
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1111
  """/2/instances/[instance_name]/deactivate-disks resource.
1112

1113
  """
1114
  def PUT(self):
1115
    """Deactivate disks for an instance.
1116

1117
    """
1118
    instance_name = self.items[0]
1119

    
1120
    op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
1121

    
1122
    return baserlib.SubmitJob([op])
1123

    
1124

    
1125
class R_2_instances_name_prepare_export(baserlib.R_Generic):
1126
  """/2/instances/[instance_name]/prepare-export resource.
1127

1128
  """
1129
  def PUT(self):
1130
    """Prepares an export for an instance.
1131

1132
    @return: a job id
1133

1134
    """
1135
    instance_name = self.items[0]
1136
    mode = self._checkStringVariable("mode")
1137

    
1138
    op = opcodes.OpPrepareExport(instance_name=instance_name,
1139
                                 mode=mode)
1140

    
1141
    return baserlib.SubmitJob([op])
1142

    
1143

    
1144
def _ParseExportInstanceRequest(name, data):
1145
  """Parses a request for an instance export.
1146

1147
  @rtype: L{opcodes.OpExportInstance}
1148
  @return: Instance export opcode
1149

1150
  """
1151
  mode = baserlib.CheckParameter(data, "mode",
1152
                                 default=constants.EXPORT_MODE_LOCAL)
1153
  target_node = baserlib.CheckParameter(data, "destination")
1154
  shutdown = baserlib.CheckParameter(data, "shutdown", exptype=bool)
1155
  remove_instance = baserlib.CheckParameter(data, "remove_instance",
1156
                                            exptype=bool, default=False)
1157
  x509_key_name = baserlib.CheckParameter(data, "x509_key_name", default=None)
1158
  destination_x509_ca = baserlib.CheckParameter(data, "destination_x509_ca",
1159
                                                default=None)
1160

    
1161
  return opcodes.OpExportInstance(instance_name=name,
1162
                                  mode=mode,
1163
                                  target_node=target_node,
1164
                                  shutdown=shutdown,
1165
                                  remove_instance=remove_instance,
1166
                                  x509_key_name=x509_key_name,
1167
                                  destination_x509_ca=destination_x509_ca)
1168

    
1169

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

1173
  """
1174
  def PUT(self):
1175
    """Exports an instance.
1176

1177
    @return: a job id
1178

1179
    """
1180
    if not isinstance(self.request_body, dict):
1181
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1182

    
1183
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1184

    
1185
    return baserlib.SubmitJob([op])
1186

    
1187

    
1188
def _ParseMigrateInstanceRequest(name, data):
1189
  """Parses a request for an instance migration.
1190

1191
  @rtype: L{opcodes.OpMigrateInstance}
1192
  @return: Instance migration opcode
1193

1194
  """
1195
  mode = baserlib.CheckParameter(data, "mode", default=None)
1196
  cleanup = baserlib.CheckParameter(data, "cleanup", exptype=bool,
1197
                                    default=False)
1198

    
1199
  return opcodes.OpMigrateInstance(instance_name=name, mode=mode,
1200
                                   cleanup=cleanup)
1201

    
1202

    
1203
class R_2_instances_name_migrate(baserlib.R_Generic):
1204
  """/2/instances/[instance_name]/migrate resource.
1205

1206
  """
1207
  def PUT(self):
1208
    """Migrates an instance.
1209

1210
    @return: a job id
1211

1212
    """
1213
    baserlib.CheckType(self.request_body, dict, "Body contents")
1214

    
1215
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1216

    
1217
    return baserlib.SubmitJob([op])
1218

    
1219

    
1220
def _ParseRenameInstanceRequest(name, data):
1221
  """Parses a request for renaming an instance.
1222

1223
  @rtype: L{opcodes.OpRenameInstance}
1224
  @return: Instance rename opcode
1225

1226
  """
1227
  new_name = baserlib.CheckParameter(data, "new_name")
1228
  ip_check = baserlib.CheckParameter(data, "ip_check", default=True)
1229
  name_check = baserlib.CheckParameter(data, "name_check", default=True)
1230

    
1231
  return opcodes.OpRenameInstance(instance_name=name, new_name=new_name,
1232
                                  name_check=name_check, ip_check=ip_check)
1233

    
1234

    
1235
class R_2_instances_name_rename(baserlib.R_Generic):
1236
  """/2/instances/[instance_name]/rename resource.
1237

1238
  """
1239
  def PUT(self):
1240
    """Changes the name of an instance.
1241

1242
    @return: a job id
1243

1244
    """
1245
    baserlib.CheckType(self.request_body, dict, "Body contents")
1246

    
1247
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1248

    
1249
    return baserlib.SubmitJob([op])
1250

    
1251

    
1252
def _ParseModifyInstanceRequest(name, data):
1253
  """Parses a request for modifying an instance.
1254

1255
  @rtype: L{opcodes.OpSetInstanceParams}
1256
  @return: Instance modify opcode
1257

1258
  """
1259
  osparams = baserlib.CheckParameter(data, "osparams", default={})
1260
  force = baserlib.CheckParameter(data, "force", default=False)
1261
  nics = baserlib.CheckParameter(data, "nics", default=[])
1262
  disks = baserlib.CheckParameter(data, "disks", default=[])
1263
  disk_template = baserlib.CheckParameter(data, "disk_template", default=None)
1264
  remote_node = baserlib.CheckParameter(data, "remote_node", default=None)
1265
  os_name = baserlib.CheckParameter(data, "os_name", default=None)
1266
  force_variant = baserlib.CheckParameter(data, "force_variant", default=False)
1267

    
1268
  # HV/BE parameters
1269
  hvparams = baserlib.CheckParameter(data, "hvparams", default={})
1270
  utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES,
1271
                      allowed_values=[constants.VALUE_DEFAULT])
1272

    
1273
  beparams = baserlib.CheckParameter(data, "beparams", default={})
1274
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES,
1275
                      allowed_values=[constants.VALUE_DEFAULT])
1276

    
1277
  return opcodes.OpSetInstanceParams(instance_name=name, hvparams=hvparams,
1278
                                     beparams=beparams, osparams=osparams,
1279
                                     force=force, nics=nics, disks=disks,
1280
                                     disk_template=disk_template,
1281
                                     remote_node=remote_node, os_name=os_name,
1282
                                     force_variant=force_variant)
1283

    
1284

    
1285
class R_2_instances_name_modify(baserlib.R_Generic):
1286
  """/2/instances/[instance_name]/modify resource.
1287

1288
  """
1289
  def PUT(self):
1290
    """Changes some parameters of an instance.
1291

1292
    @return: a job id
1293

1294
    """
1295
    baserlib.CheckType(self.request_body, dict, "Body contents")
1296

    
1297
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1298

    
1299
    return baserlib.SubmitJob([op])
1300

    
1301

    
1302
class _R_Tags(baserlib.R_Generic):
1303
  """ Quasiclass for tagging resources
1304

1305
  Manages tags. When inheriting this class you must define the
1306
  TAG_LEVEL for it.
1307

1308
  """
1309
  TAG_LEVEL = None
1310

    
1311
  def __init__(self, items, queryargs, req):
1312
    """A tag resource constructor.
1313

1314
    We have to override the default to sort out cluster naming case.
1315

1316
    """
1317
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1318

    
1319
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1320
      self.name = None
1321
    else:
1322
      self.name = items[0]
1323

    
1324
  def GET(self):
1325
    """Returns a list of tags.
1326

1327
    Example: ["tag1", "tag2", "tag3"]
1328

1329
    """
1330
    # pylint: disable-msg=W0212
1331
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1332

    
1333
  def PUT(self):
1334
    """Add a set of tags.
1335

1336
    The request as a list of strings should be PUT to this URI. And
1337
    you'll have back a job id.
1338

1339
    """
1340
    # pylint: disable-msg=W0212
1341
    if 'tag' not in self.queryargs:
1342
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1343
                                " the 'tag' parameter")
1344
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1345
                              self.queryargs['tag'], name=self.name,
1346
                              dry_run=bool(self.dryRun()))
1347

    
1348
  def DELETE(self):
1349
    """Delete a tag.
1350

1351
    In order to delete a set of tags, the DELETE
1352
    request should be addressed to URI like:
1353
    /tags?tag=[tag]&tag=[tag]
1354

1355
    """
1356
    # pylint: disable-msg=W0212
1357
    if 'tag' not in self.queryargs:
1358
      # no we not gonna delete all tags
1359
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1360
                                " tag(s) using the 'tag' parameter")
1361
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1362
                                 self.queryargs['tag'],
1363
                                 name=self.name,
1364
                                 dry_run=bool(self.dryRun()))
1365

    
1366

    
1367
class R_2_instances_name_tags(_R_Tags):
1368
  """ /2/instances/[instance_name]/tags resource.
1369

1370
  Manages per-instance tags.
1371

1372
  """
1373
  TAG_LEVEL = constants.TAG_INSTANCE
1374

    
1375

    
1376
class R_2_nodes_name_tags(_R_Tags):
1377
  """ /2/nodes/[node_name]/tags resource.
1378

1379
  Manages per-node tags.
1380

1381
  """
1382
  TAG_LEVEL = constants.TAG_NODE
1383

    
1384

    
1385
class R_2_tags(_R_Tags):
1386
  """ /2/instances/tags resource.
1387

1388
  Manages cluster tags.
1389

1390
  """
1391
  TAG_LEVEL = constants.TAG_CLUSTER