Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 0dbaa9ca

History | View | Annotate | Download (38.7 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
  def POST(self):
554
    """Create a node group.
555

556
    @return: a job id
557

558
    """
559
    baserlib.CheckType(self.request_body, dict, "Body contents")
560
    group_name = baserlib.CheckParameter(self.request_body, "name")
561
    op = opcodes.OpAddGroup(group_name=group_name, dry_run=self.dryRun())
562
    return baserlib.SubmitJob([op])
563

    
564

    
565
class R_2_groups_name(baserlib.R_Generic):
566
  """/2/groups/[group_name] resources.
567

568
  """
569
  def GET(self):
570
    """Send information about a node group.
571

572
    """
573
    group_name = self.items[0]
574
    client = baserlib.GetClient()
575

    
576
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
577
                                            names=[group_name], fields=G_FIELDS,
578
                                            use_locking=self.useLocking())
579

    
580
    return baserlib.MapFields(G_FIELDS, result[0])
581

    
582
  def DELETE(self):
583
    """Delete a node group.
584

585
    """
586
    op = opcodes.OpRemoveGroup(group_name=self.items[0],
587
                               dry_run=bool(self.dryRun()))
588

    
589
    return baserlib.SubmitJob([op])
590

    
591

    
592
def _ParseRenameGroupRequest(name, data, dry_run):
593
  """Parses a request for renaming a node group.
594

595
  @type name: string
596
  @param name: name of the node group to rename
597
  @type data: dict
598
  @param data: the body received by the rename request
599
  @type dry_run: bool
600
  @param dry_run: whether to perform a dry run
601

602
  @rtype: L{opcodes.OpRenameGroup}
603
  @return: Node group rename opcode
604

605
  """
606
  old_name = name
607
  new_name = baserlib.CheckParameter(data, "new_name")
608

    
609
  return opcodes.OpRenameGroup(old_name=old_name, new_name=new_name,
610
                               dry_run=dry_run)
611

    
612

    
613
class R_2_groups_name_rename(baserlib.R_Generic):
614
  """/2/groups/[groupe_name]/rename resource.
615

616
  """
617
  def PUT(self):
618
    """Changes the name of a node group.
619

620
    @return: a job id
621

622
    """
623
    baserlib.CheckType(self.request_body, dict, "Body contents")
624
    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
625
                                  self.dryRun())
626
    return baserlib.SubmitJob([op])
627

    
628

    
629
def _ParseInstanceCreateRequestVersion1(data, dry_run):
630
  """Parses an instance creation request version 1.
631

632
  @rtype: L{opcodes.OpCreateInstance}
633
  @return: Instance creation opcode
634

635
  """
636
  # Disks
637
  disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
638

    
639
  disks = []
640
  for idx, i in enumerate(disks_input):
641
    baserlib.CheckType(i, dict, "Disk %d specification" % idx)
642

    
643
    # Size is mandatory
644
    try:
645
      size = i[constants.IDISK_SIZE]
646
    except KeyError:
647
      raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
648
                                " size" % idx)
649

    
650
    disk = {
651
      constants.IDISK_SIZE: size,
652
      }
653

    
654
    # Optional disk access mode
655
    try:
656
      disk_access = i[constants.IDISK_MODE]
657
    except KeyError:
658
      pass
659
    else:
660
      disk[constants.IDISK_MODE] = disk_access
661

    
662
    disks.append(disk)
663

    
664
  assert len(disks_input) == len(disks)
665

    
666
  # Network interfaces
667
  nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
668

    
669
  nics = []
670
  for idx, i in enumerate(nics_input):
671
    baserlib.CheckType(i, dict, "NIC %d specification" % idx)
672

    
673
    nic = {}
674

    
675
    for field in constants.INIC_PARAMS:
676
      try:
677
        value = i[field]
678
      except KeyError:
679
        continue
680

    
681
      nic[field] = value
682

    
683
    nics.append(nic)
684

    
685
  assert len(nics_input) == len(nics)
686

    
687
  # HV/BE parameters
688
  hvparams = baserlib.CheckParameter(data, "hvparams", default={})
689
  utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
690

    
691
  beparams = baserlib.CheckParameter(data, "beparams", default={})
692
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
693

    
694
  return opcodes.OpCreateInstance(
695
    mode=baserlib.CheckParameter(data, "mode"),
696
    instance_name=baserlib.CheckParameter(data, "name"),
697
    os_type=baserlib.CheckParameter(data, "os"),
698
    osparams=baserlib.CheckParameter(data, "osparams", default={}),
699
    force_variant=baserlib.CheckParameter(data, "force_variant",
700
                                          default=False),
701
    no_install=baserlib.CheckParameter(data, "no_install", default=False),
702
    pnode=baserlib.CheckParameter(data, "pnode", default=None),
703
    snode=baserlib.CheckParameter(data, "snode", default=None),
704
    disk_template=baserlib.CheckParameter(data, "disk_template"),
705
    disks=disks,
706
    nics=nics,
707
    src_node=baserlib.CheckParameter(data, "src_node", default=None),
708
    src_path=baserlib.CheckParameter(data, "src_path", default=None),
709
    start=baserlib.CheckParameter(data, "start", default=True),
710
    wait_for_sync=True,
711
    ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
712
    name_check=baserlib.CheckParameter(data, "name_check", default=True),
713
    file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
714
                                             default=None),
715
    file_driver=baserlib.CheckParameter(data, "file_driver",
716
                                        default=constants.FD_LOOP),
717
    source_handshake=baserlib.CheckParameter(data, "source_handshake",
718
                                             default=None),
719
    source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
720
                                           default=None),
721
    source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
722
                                                 default=None),
723
    iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
724
    hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
725
    hvparams=hvparams,
726
    beparams=beparams,
727
    dry_run=dry_run,
728
    )
729

    
730

    
731
class R_2_instances(baserlib.R_Generic):
732
  """/2/instances resource.
733

734
  """
735
  def GET(self):
736
    """Returns a list of all available instances.
737

738
    """
739
    client = baserlib.GetClient()
740

    
741
    use_locking = self.useLocking()
742
    if self.useBulk():
743
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
744
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
745
    else:
746
      instancesdata = client.QueryInstances([], ["name"], use_locking)
747
      instanceslist = [row[0] for row in instancesdata]
748
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
749
                                   uri_fields=("id", "uri"))
750

    
751
  def _ParseVersion0CreateRequest(self):
752
    """Parses an instance creation request version 0.
753

754
    Request data version 0 is deprecated and should not be used anymore.
755

756
    @rtype: L{opcodes.OpCreateInstance}
757
    @return: Instance creation opcode
758

759
    """
760
    # Do not modify anymore, request data version 0 is deprecated
761
    beparams = baserlib.MakeParamsDict(self.request_body,
762
                                       constants.BES_PARAMETERS)
763
    hvparams = baserlib.MakeParamsDict(self.request_body,
764
                                       constants.HVS_PARAMETERS)
765
    fn = self.getBodyParameter
766

    
767
    # disk processing
768
    disk_data = fn('disks')
769
    if not isinstance(disk_data, list):
770
      raise http.HttpBadRequest("The 'disks' parameter should be a list")
771
    disks = []
772
    for idx, d in enumerate(disk_data):
773
      if not isinstance(d, int):
774
        raise http.HttpBadRequest("Disk %d specification wrong: should"
775
                                  " be an integer" % idx)
776
      disks.append({"size": d})
777

    
778
    # nic processing (one nic only)
779
    nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
780
    if fn("ip", None) is not None:
781
      nics[0]["ip"] = fn("ip")
782
    if fn("mode", None) is not None:
783
      nics[0]["mode"] = fn("mode")
784
    if fn("link", None) is not None:
785
      nics[0]["link"] = fn("link")
786
    if fn("bridge", None) is not None:
787
      nics[0]["bridge"] = fn("bridge")
788

    
789
    # Do not modify anymore, request data version 0 is deprecated
790
    return opcodes.OpCreateInstance(
791
      mode=constants.INSTANCE_CREATE,
792
      instance_name=fn('name'),
793
      disks=disks,
794
      disk_template=fn('disk_template'),
795
      os_type=fn('os'),
796
      pnode=fn('pnode', None),
797
      snode=fn('snode', None),
798
      iallocator=fn('iallocator', None),
799
      nics=nics,
800
      start=fn('start', True),
801
      ip_check=fn('ip_check', True),
802
      name_check=fn('name_check', True),
803
      wait_for_sync=True,
804
      hypervisor=fn('hypervisor', None),
805
      hvparams=hvparams,
806
      beparams=beparams,
807
      file_storage_dir=fn('file_storage_dir', None),
808
      file_driver=fn('file_driver', constants.FD_LOOP),
809
      dry_run=bool(self.dryRun()),
810
      )
811

    
812
  def POST(self):
813
    """Create an instance.
814

815
    @return: a job id
816

817
    """
818
    if not isinstance(self.request_body, dict):
819
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
820

    
821
    # Default to request data version 0
822
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
823

    
824
    if data_version == 0:
825
      op = self._ParseVersion0CreateRequest()
826
    elif data_version == 1:
827
      op = _ParseInstanceCreateRequestVersion1(self.request_body,
828
                                               self.dryRun())
829
    else:
830
      raise http.HttpBadRequest("Unsupported request data version %s" %
831
                                data_version)
832

    
833
    return baserlib.SubmitJob([op])
834

    
835

    
836
class R_2_instances_name(baserlib.R_Generic):
837
  """/2/instances/[instance_name] resources.
838

839
  """
840
  def GET(self):
841
    """Send information about an instance.
842

843
    """
844
    client = baserlib.GetClient()
845
    instance_name = self.items[0]
846

    
847
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
848
                                            names=[instance_name],
849
                                            fields=I_FIELDS,
850
                                            use_locking=self.useLocking())
851

    
852
    return baserlib.MapFields(I_FIELDS, result[0])
853

    
854
  def DELETE(self):
855
    """Delete an instance.
856

857
    """
858
    op = opcodes.OpRemoveInstance(instance_name=self.items[0],
859
                                  ignore_failures=False,
860
                                  dry_run=bool(self.dryRun()))
861
    return baserlib.SubmitJob([op])
862

    
863

    
864
class R_2_instances_name_info(baserlib.R_Generic):
865
  """/2/instances/[instance_name]/info resource.
866

867
  """
868
  def GET(self):
869
    """Request detailed instance information.
870

871
    """
872
    instance_name = self.items[0]
873
    static = bool(self._checkIntVariable("static", default=0))
874

    
875
    op = opcodes.OpQueryInstanceData(instances=[instance_name],
876
                                     static=static)
877
    return baserlib.SubmitJob([op])
878

    
879

    
880
class R_2_instances_name_reboot(baserlib.R_Generic):
881
  """/2/instances/[instance_name]/reboot resource.
882

883
  Implements an instance reboot.
884

885
  """
886
  def POST(self):
887
    """Reboot an instance.
888

889
    The URI takes type=[hard|soft|full] and
890
    ignore_secondaries=[False|True] parameters.
891

892
    """
893
    instance_name = self.items[0]
894
    reboot_type = self.queryargs.get('type',
895
                                     [constants.INSTANCE_REBOOT_HARD])[0]
896
    ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
897
    op = opcodes.OpRebootInstance(instance_name=instance_name,
898
                                  reboot_type=reboot_type,
899
                                  ignore_secondaries=ignore_secondaries,
900
                                  dry_run=bool(self.dryRun()))
901

    
902
    return baserlib.SubmitJob([op])
903

    
904

    
905
class R_2_instances_name_startup(baserlib.R_Generic):
906
  """/2/instances/[instance_name]/startup resource.
907

908
  Implements an instance startup.
909

910
  """
911
  def PUT(self):
912
    """Startup an instance.
913

914
    The URI takes force=[False|True] parameter to start the instance
915
    if even if secondary disks are failing.
916

917
    """
918
    instance_name = self.items[0]
919
    force_startup = bool(self._checkIntVariable('force'))
920
    op = opcodes.OpStartupInstance(instance_name=instance_name,
921
                                   force=force_startup,
922
                                   dry_run=bool(self.dryRun()))
923

    
924
    return baserlib.SubmitJob([op])
925

    
926

    
927
class R_2_instances_name_shutdown(baserlib.R_Generic):
928
  """/2/instances/[instance_name]/shutdown resource.
929

930
  Implements an instance shutdown.
931

932
  """
933
  def PUT(self):
934
    """Shutdown an instance.
935

936
    """
937
    instance_name = self.items[0]
938
    op = opcodes.OpShutdownInstance(instance_name=instance_name,
939
                                    dry_run=bool(self.dryRun()))
940

    
941
    return baserlib.SubmitJob([op])
942

    
943

    
944
def _ParseInstanceReinstallRequest(name, data):
945
  """Parses a request for reinstalling an instance.
946

947
  """
948
  if not isinstance(data, dict):
949
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
950

    
951
  ostype = baserlib.CheckParameter(data, "os")
952
  start = baserlib.CheckParameter(data, "start", exptype=bool,
953
                                  default=True)
954
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
955

    
956
  ops = [
957
    opcodes.OpShutdownInstance(instance_name=name),
958
    opcodes.OpReinstallInstance(instance_name=name, os_type=ostype,
959
                                osparams=osparams),
960
    ]
961

    
962
  if start:
963
    ops.append(opcodes.OpStartupInstance(instance_name=name, force=False))
964

    
965
  return ops
966

    
967

    
968
class R_2_instances_name_reinstall(baserlib.R_Generic):
969
  """/2/instances/[instance_name]/reinstall resource.
970

971
  Implements an instance reinstall.
972

973
  """
974
  def POST(self):
975
    """Reinstall an instance.
976

977
    The URI takes os=name and nostartup=[0|1] optional
978
    parameters. By default, the instance will be started
979
    automatically.
980

981
    """
982
    if self.request_body:
983
      if self.queryargs:
984
        raise http.HttpBadRequest("Can't combine query and body parameters")
985

    
986
      body = self.request_body
987
    else:
988
      if not self.queryargs:
989
        raise http.HttpBadRequest("Missing query parameters")
990
      # Legacy interface, do not modify/extend
991
      body = {
992
        "os": self._checkStringVariable("os"),
993
        "start": not self._checkIntVariable("nostartup"),
994
        }
995

    
996
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
997

    
998
    return baserlib.SubmitJob(ops)
999

    
1000

    
1001
class R_2_instances_name_replace_disks(baserlib.R_Generic):
1002
  """/2/instances/[instance_name]/replace-disks resource.
1003

1004
  """
1005
  def POST(self):
1006
    """Replaces disks on an instance.
1007

1008
    """
1009
    instance_name = self.items[0]
1010
    remote_node = self._checkStringVariable("remote_node", default=None)
1011
    mode = self._checkStringVariable("mode", default=None)
1012
    raw_disks = self._checkStringVariable("disks", default=None)
1013
    iallocator = self._checkStringVariable("iallocator", default=None)
1014

    
1015
    if raw_disks:
1016
      try:
1017
        disks = [int(part) for part in raw_disks.split(",")]
1018
      except ValueError, err:
1019
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1020
    else:
1021
      disks = []
1022

    
1023
    op = opcodes.OpReplaceDisks(instance_name=instance_name,
1024
                                remote_node=remote_node,
1025
                                mode=mode,
1026
                                disks=disks,
1027
                                iallocator=iallocator)
1028

    
1029
    return baserlib.SubmitJob([op])
1030

    
1031

    
1032
class R_2_instances_name_activate_disks(baserlib.R_Generic):
1033
  """/2/instances/[instance_name]/activate-disks resource.
1034

1035
  """
1036
  def PUT(self):
1037
    """Activate disks for an instance.
1038

1039
    The URI might contain ignore_size to ignore current recorded size.
1040

1041
    """
1042
    instance_name = self.items[0]
1043
    ignore_size = bool(self._checkIntVariable('ignore_size'))
1044

    
1045
    op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
1046
                                         ignore_size=ignore_size)
1047

    
1048
    return baserlib.SubmitJob([op])
1049

    
1050

    
1051
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1052
  """/2/instances/[instance_name]/deactivate-disks resource.
1053

1054
  """
1055
  def PUT(self):
1056
    """Deactivate disks for an instance.
1057

1058
    """
1059
    instance_name = self.items[0]
1060

    
1061
    op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
1062

    
1063
    return baserlib.SubmitJob([op])
1064

    
1065

    
1066
class R_2_instances_name_prepare_export(baserlib.R_Generic):
1067
  """/2/instances/[instance_name]/prepare-export resource.
1068

1069
  """
1070
  def PUT(self):
1071
    """Prepares an export for an instance.
1072

1073
    @return: a job id
1074

1075
    """
1076
    instance_name = self.items[0]
1077
    mode = self._checkStringVariable("mode")
1078

    
1079
    op = opcodes.OpPrepareExport(instance_name=instance_name,
1080
                                 mode=mode)
1081

    
1082
    return baserlib.SubmitJob([op])
1083

    
1084

    
1085
def _ParseExportInstanceRequest(name, data):
1086
  """Parses a request for an instance export.
1087

1088
  @rtype: L{opcodes.OpExportInstance}
1089
  @return: Instance export opcode
1090

1091
  """
1092
  mode = baserlib.CheckParameter(data, "mode",
1093
                                 default=constants.EXPORT_MODE_LOCAL)
1094
  target_node = baserlib.CheckParameter(data, "destination")
1095
  shutdown = baserlib.CheckParameter(data, "shutdown", exptype=bool)
1096
  remove_instance = baserlib.CheckParameter(data, "remove_instance",
1097
                                            exptype=bool, default=False)
1098
  x509_key_name = baserlib.CheckParameter(data, "x509_key_name", default=None)
1099
  destination_x509_ca = baserlib.CheckParameter(data, "destination_x509_ca",
1100
                                                default=None)
1101

    
1102
  return opcodes.OpExportInstance(instance_name=name,
1103
                                  mode=mode,
1104
                                  target_node=target_node,
1105
                                  shutdown=shutdown,
1106
                                  remove_instance=remove_instance,
1107
                                  x509_key_name=x509_key_name,
1108
                                  destination_x509_ca=destination_x509_ca)
1109

    
1110

    
1111
class R_2_instances_name_export(baserlib.R_Generic):
1112
  """/2/instances/[instance_name]/export resource.
1113

1114
  """
1115
  def PUT(self):
1116
    """Exports an instance.
1117

1118
    @return: a job id
1119

1120
    """
1121
    if not isinstance(self.request_body, dict):
1122
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1123

    
1124
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1125

    
1126
    return baserlib.SubmitJob([op])
1127

    
1128

    
1129
def _ParseMigrateInstanceRequest(name, data):
1130
  """Parses a request for an instance migration.
1131

1132
  @rtype: L{opcodes.OpMigrateInstance}
1133
  @return: Instance migration opcode
1134

1135
  """
1136
  mode = baserlib.CheckParameter(data, "mode", default=None)
1137
  cleanup = baserlib.CheckParameter(data, "cleanup", exptype=bool,
1138
                                    default=False)
1139

    
1140
  return opcodes.OpMigrateInstance(instance_name=name, mode=mode,
1141
                                   cleanup=cleanup)
1142

    
1143

    
1144
class R_2_instances_name_migrate(baserlib.R_Generic):
1145
  """/2/instances/[instance_name]/migrate resource.
1146

1147
  """
1148
  def PUT(self):
1149
    """Migrates an instance.
1150

1151
    @return: a job id
1152

1153
    """
1154
    baserlib.CheckType(self.request_body, dict, "Body contents")
1155

    
1156
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1157

    
1158
    return baserlib.SubmitJob([op])
1159

    
1160

    
1161
def _ParseRenameInstanceRequest(name, data):
1162
  """Parses a request for renaming an instance.
1163

1164
  @rtype: L{opcodes.OpRenameInstance}
1165
  @return: Instance rename opcode
1166

1167
  """
1168
  new_name = baserlib.CheckParameter(data, "new_name")
1169
  ip_check = baserlib.CheckParameter(data, "ip_check", default=True)
1170
  name_check = baserlib.CheckParameter(data, "name_check", default=True)
1171

    
1172
  return opcodes.OpRenameInstance(instance_name=name, new_name=new_name,
1173
                                  name_check=name_check, ip_check=ip_check)
1174

    
1175

    
1176
class R_2_instances_name_rename(baserlib.R_Generic):
1177
  """/2/instances/[instance_name]/rename resource.
1178

1179
  """
1180
  def PUT(self):
1181
    """Changes the name of an instance.
1182

1183
    @return: a job id
1184

1185
    """
1186
    baserlib.CheckType(self.request_body, dict, "Body contents")
1187

    
1188
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1189

    
1190
    return baserlib.SubmitJob([op])
1191

    
1192

    
1193
def _ParseModifyInstanceRequest(name, data):
1194
  """Parses a request for modifying an instance.
1195

1196
  @rtype: L{opcodes.OpSetInstanceParams}
1197
  @return: Instance modify opcode
1198

1199
  """
1200
  osparams = baserlib.CheckParameter(data, "osparams", default={})
1201
  force = baserlib.CheckParameter(data, "force", default=False)
1202
  nics = baserlib.CheckParameter(data, "nics", default=[])
1203
  disks = baserlib.CheckParameter(data, "disks", default=[])
1204
  disk_template = baserlib.CheckParameter(data, "disk_template", default=None)
1205
  remote_node = baserlib.CheckParameter(data, "remote_node", default=None)
1206
  os_name = baserlib.CheckParameter(data, "os_name", default=None)
1207
  force_variant = baserlib.CheckParameter(data, "force_variant", default=False)
1208

    
1209
  # HV/BE parameters
1210
  hvparams = baserlib.CheckParameter(data, "hvparams", default={})
1211
  utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES,
1212
                      allowed_values=[constants.VALUE_DEFAULT])
1213

    
1214
  beparams = baserlib.CheckParameter(data, "beparams", default={})
1215
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES,
1216
                      allowed_values=[constants.VALUE_DEFAULT])
1217

    
1218
  return opcodes.OpSetInstanceParams(instance_name=name, hvparams=hvparams,
1219
                                     beparams=beparams, osparams=osparams,
1220
                                     force=force, nics=nics, disks=disks,
1221
                                     disk_template=disk_template,
1222
                                     remote_node=remote_node, os_name=os_name,
1223
                                     force_variant=force_variant)
1224

    
1225

    
1226
class R_2_instances_name_modify(baserlib.R_Generic):
1227
  """/2/instances/[instance_name]/modify resource.
1228

1229
  """
1230
  def PUT(self):
1231
    """Changes some parameters of an instance.
1232

1233
    @return: a job id
1234

1235
    """
1236
    baserlib.CheckType(self.request_body, dict, "Body contents")
1237

    
1238
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1239

    
1240
    return baserlib.SubmitJob([op])
1241

    
1242

    
1243
class _R_Tags(baserlib.R_Generic):
1244
  """ Quasiclass for tagging resources
1245

1246
  Manages tags. When inheriting this class you must define the
1247
  TAG_LEVEL for it.
1248

1249
  """
1250
  TAG_LEVEL = None
1251

    
1252
  def __init__(self, items, queryargs, req):
1253
    """A tag resource constructor.
1254

1255
    We have to override the default to sort out cluster naming case.
1256

1257
    """
1258
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1259

    
1260
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1261
      self.name = None
1262
    else:
1263
      self.name = items[0]
1264

    
1265
  def GET(self):
1266
    """Returns a list of tags.
1267

1268
    Example: ["tag1", "tag2", "tag3"]
1269

1270
    """
1271
    # pylint: disable-msg=W0212
1272
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1273

    
1274
  def PUT(self):
1275
    """Add a set of tags.
1276

1277
    The request as a list of strings should be PUT to this URI. And
1278
    you'll have back a job id.
1279

1280
    """
1281
    # pylint: disable-msg=W0212
1282
    if 'tag' not in self.queryargs:
1283
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1284
                                " the 'tag' parameter")
1285
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1286
                              self.queryargs['tag'], name=self.name,
1287
                              dry_run=bool(self.dryRun()))
1288

    
1289
  def DELETE(self):
1290
    """Delete a tag.
1291

1292
    In order to delete a set of tags, the DELETE
1293
    request should be addressed to URI like:
1294
    /tags?tag=[tag]&tag=[tag]
1295

1296
    """
1297
    # pylint: disable-msg=W0212
1298
    if 'tag' not in self.queryargs:
1299
      # no we not gonna delete all tags
1300
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1301
                                " tag(s) using the 'tag' parameter")
1302
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1303
                                 self.queryargs['tag'],
1304
                                 name=self.name,
1305
                                 dry_run=bool(self.dryRun()))
1306

    
1307

    
1308
class R_2_instances_name_tags(_R_Tags):
1309
  """ /2/instances/[instance_name]/tags resource.
1310

1311
  Manages per-instance tags.
1312

1313
  """
1314
  TAG_LEVEL = constants.TAG_INSTANCE
1315

    
1316

    
1317
class R_2_nodes_name_tags(_R_Tags):
1318
  """ /2/nodes/[node_name]/tags resource.
1319

1320
  Manages per-node tags.
1321

1322
  """
1323
  TAG_LEVEL = constants.TAG_NODE
1324

    
1325

    
1326
class R_2_tags(_R_Tags):
1327
  """ /2/instances/tags resource.
1328

1329
  Manages cluster tags.
1330

1331
  """
1332
  TAG_LEVEL = constants.TAG_CLUSTER