Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ bd7b2070

History | View | Annotate | Download (36.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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 rapi
49
from ganeti import ht
50
from ganeti import compat
51
from ganeti.rapi import baserlib
52

    
53

    
54
_COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
55
I_FIELDS = ["name", "admin_state", "os",
56
            "pnode", "snodes",
57
            "disk_template",
58
            "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
59
            "network_port",
60
            "disk.sizes", "disk_usage",
61
            "beparams", "hvparams",
62
            "oper_state", "oper_ram", "oper_vcpus", "status",
63
            "custom_hvparams", "custom_beparams", "custom_nicparams",
64
            ] + _COMMON_FIELDS
65

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

    
77
G_FIELDS = [
78
  "alloc_policy",
79
  "name",
80
  "node_cnt",
81
  "node_list",
82
  ] + _COMMON_FIELDS
83

    
84
J_FIELDS_BULK = [
85
  "id", "ops", "status", "summary",
86
  "opstatus",
87
  "received_ts", "start_ts", "end_ts",
88
  ]
89

    
90
J_FIELDS = J_FIELDS_BULK + [
91
  "oplog",
92
  "opresult",
93
  ]
94

    
95
_NR_DRAINED = "drained"
96
_NR_MASTER_CANDIATE = "master-candidate"
97
_NR_MASTER = "master"
98
_NR_OFFLINE = "offline"
99
_NR_REGULAR = "regular"
100

    
101
_NR_MAP = {
102
  constants.NR_MASTER: _NR_MASTER,
103
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIATE,
104
  constants.NR_DRAINED: _NR_DRAINED,
105
  constants.NR_OFFLINE: _NR_OFFLINE,
106
  constants.NR_REGULAR: _NR_REGULAR,
107
  }
108

    
109
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
110

    
111
# Request data version field
112
_REQ_DATA_VERSION = "__version__"
113

    
114
# Feature string for instance creation request data version 1
115
_INST_CREATE_REQV1 = "instance-create-reqv1"
116

    
117
# Feature string for instance reinstall request version 1
118
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
119

    
120
# Feature string for node migration version 1
121
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
122

    
123
# Feature string for node evacuation with LU-generated jobs
124
_NODE_EVAC_RES1 = "node-evac-res1"
125

    
126
ALL_FEATURES = frozenset([
127
  _INST_CREATE_REQV1,
128
  _INST_REINSTALL_REQV1,
129
  _NODE_MIGRATE_REQV1,
130
  _NODE_EVAC_RES1,
131
  ])
132

    
133
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
134
_WFJC_TIMEOUT = 10
135

    
136

    
137
class R_version(baserlib.R_Generic):
138
  """/version resource.
139

140
  This resource should be used to determine the remote API version and
141
  to adapt clients accordingly.
142

143
  """
144
  @staticmethod
145
  def GET():
146
    """Returns the remote API version.
147

148
    """
149
    return constants.RAPI_VERSION
150

    
151

    
152
class R_2_info(baserlib.R_Generic):
153
  """/2/info resource.
154

155
  """
156
  @staticmethod
157
  def GET():
158
    """Returns cluster information.
159

160
    """
161
    client = baserlib.GetClient()
162
    return client.QueryClusterInfo()
163

    
164

    
165
class R_2_features(baserlib.R_Generic):
166
  """/2/features resource.
167

168
  """
169
  @staticmethod
170
  def GET():
171
    """Returns list of optional RAPI features implemented.
172

173
    """
174
    return list(ALL_FEATURES)
175

    
176

    
177
class R_2_os(baserlib.R_Generic):
178
  """/2/os resource.
179

180
  """
181
  @staticmethod
182
  def GET():
183
    """Return a list of all OSes.
184

185
    Can return error 500 in case of a problem.
186

187
    Example: ["debian-etch"]
188

189
    """
190
    cl = baserlib.GetClient()
191
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
192
    job_id = baserlib.SubmitJob([op], cl)
193
    # we use custom feedback function, instead of print we log the status
194
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
195
    diagnose_data = result[0]
196

    
197
    if not isinstance(diagnose_data, list):
198
      raise http.HttpBadGateway(message="Can't get OS list")
199

    
200
    os_names = []
201
    for (name, variants) in diagnose_data:
202
      os_names.extend(cli.CalculateOSNames(name, variants))
203

    
204
    return os_names
205

    
206

    
207
class R_2_redist_config(baserlib.R_Generic):
208
  """/2/redistribute-config resource.
209

210
  """
211
  @staticmethod
212
  def PUT():
213
    """Redistribute configuration to all nodes.
214

215
    """
216
    return baserlib.SubmitJob([opcodes.OpClusterRedistConf()])
217

    
218

    
219
class R_2_cluster_modify(baserlib.R_Generic):
220
  """/2/modify resource.
221

222
  """
223
  def PUT(self):
224
    """Modifies cluster parameters.
225

226
    @return: a job id
227

228
    """
229
    op = baserlib.FillOpcode(opcodes.OpClusterSetParams, self.request_body,
230
                             None)
231

    
232
    return baserlib.SubmitJob([op])
233

    
234

    
235
class R_2_jobs(baserlib.R_Generic):
236
  """/2/jobs resource.
237

238
  """
239
  def GET(self):
240
    """Returns a dictionary of jobs.
241

242
    @return: a dictionary with jobs id and uri.
243

244
    """
245
    client = baserlib.GetClient()
246

    
247
    if self.useBulk():
248
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
249
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
250
    else:
251
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
252
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
253
                                   uri_fields=("id", "uri"))
254

    
255

    
256
class R_2_jobs_id(baserlib.R_Generic):
257
  """/2/jobs/[job_id] resource.
258

259
  """
260
  def GET(self):
261
    """Returns a job status.
262

263
    @return: a dictionary with job parameters.
264
        The result includes:
265
            - id: job ID as a number
266
            - status: current job status as a string
267
            - ops: involved OpCodes as a list of dictionaries for each
268
              opcodes in the job
269
            - opstatus: OpCodes status as a list
270
            - opresult: OpCodes results as a list of lists
271

272
    """
273
    job_id = self.items[0]
274
    result = baserlib.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
275
    if result is None:
276
      raise http.HttpNotFound()
277
    return baserlib.MapFields(J_FIELDS, result)
278

    
279
  def DELETE(self):
280
    """Cancel not-yet-started job.
281

282
    """
283
    job_id = self.items[0]
284
    result = baserlib.GetClient().CancelJob(job_id)
285
    return result
286

    
287

    
288
class R_2_jobs_id_wait(baserlib.R_Generic):
289
  """/2/jobs/[job_id]/wait resource.
290

291
  """
292
  # WaitForJobChange provides access to sensitive information and blocks
293
  # machine resources (it's a blocking RAPI call), hence restricting access.
294
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
295

    
296
  def GET(self):
297
    """Waits for job changes.
298

299
    """
300
    job_id = self.items[0]
301

    
302
    fields = self.getBodyParameter("fields")
303
    prev_job_info = self.getBodyParameter("previous_job_info", None)
304
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
305

    
306
    if not isinstance(fields, list):
307
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
308

    
309
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
310
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
311
                                " be a list")
312

    
313
    if not (prev_log_serial is None or
314
            isinstance(prev_log_serial, (int, long))):
315
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
316
                                " be a number")
317

    
318
    client = baserlib.GetClient()
319
    result = client.WaitForJobChangeOnce(job_id, fields,
320
                                         prev_job_info, prev_log_serial,
321
                                         timeout=_WFJC_TIMEOUT)
322
    if not result:
323
      raise http.HttpNotFound()
324

    
325
    if result == constants.JOB_NOTCHANGED:
326
      # No changes
327
      return None
328

    
329
    (job_info, log_entries) = result
330

    
331
    return {
332
      "job_info": job_info,
333
      "log_entries": log_entries,
334
      }
335

    
336

    
337
class R_2_nodes(baserlib.R_Generic):
338
  """/2/nodes resource.
339

340
  """
341
  def GET(self):
342
    """Returns a list of all nodes.
343

344
    """
345
    client = baserlib.GetClient()
346

    
347
    if self.useBulk():
348
      bulkdata = client.QueryNodes([], N_FIELDS, False)
349
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
350
    else:
351
      nodesdata = client.QueryNodes([], ["name"], False)
352
      nodeslist = [row[0] for row in nodesdata]
353
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
354
                                   uri_fields=("id", "uri"))
355

    
356

    
357
class R_2_nodes_name(baserlib.R_Generic):
358
  """/2/nodes/[node_name] resource.
359

360
  """
361
  def GET(self):
362
    """Send information about a node.
363

364
    """
365
    node_name = self.items[0]
366
    client = baserlib.GetClient()
367

    
368
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
369
                                            names=[node_name], fields=N_FIELDS,
370
                                            use_locking=self.useLocking())
371

    
372
    return baserlib.MapFields(N_FIELDS, result[0])
373

    
374

    
375
class R_2_nodes_name_role(baserlib.R_Generic):
376
  """ /2/nodes/[node_name]/role resource.
377

378
  """
379
  def GET(self):
380
    """Returns the current node role.
381

382
    @return: Node role
383

384
    """
385
    node_name = self.items[0]
386
    client = baserlib.GetClient()
387
    result = client.QueryNodes(names=[node_name], fields=["role"],
388
                               use_locking=self.useLocking())
389

    
390
    return _NR_MAP[result[0][0]]
391

    
392
  def PUT(self):
393
    """Sets the node role.
394

395
    @return: a job id
396

397
    """
398
    if not isinstance(self.request_body, basestring):
399
      raise http.HttpBadRequest("Invalid body contents, not a string")
400

    
401
    node_name = self.items[0]
402
    role = self.request_body
403

    
404
    if role == _NR_REGULAR:
405
      candidate = False
406
      offline = False
407
      drained = False
408

    
409
    elif role == _NR_MASTER_CANDIATE:
410
      candidate = True
411
      offline = drained = None
412

    
413
    elif role == _NR_DRAINED:
414
      drained = True
415
      candidate = offline = None
416

    
417
    elif role == _NR_OFFLINE:
418
      offline = True
419
      candidate = drained = None
420

    
421
    else:
422
      raise http.HttpBadRequest("Can't set '%s' role" % role)
423

    
424
    op = opcodes.OpNodeSetParams(node_name=node_name,
425
                                 master_candidate=candidate,
426
                                 offline=offline,
427
                                 drained=drained,
428
                                 force=bool(self.useForce()))
429

    
430
    return baserlib.SubmitJob([op])
431

    
432

    
433
class R_2_nodes_name_evacuate(baserlib.R_Generic):
434
  """/2/nodes/[node_name]/evacuate resource.
435

436
  """
437
  def POST(self):
438
    """Evacuate all instances off a node.
439

440
    """
441
    op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, {
442
      "node_name": self.items[0],
443
      "dry_run": self.dryRun(),
444
      })
445

    
446
    return baserlib.SubmitJob([op])
447

    
448

    
449
class R_2_nodes_name_migrate(baserlib.R_Generic):
450
  """/2/nodes/[node_name]/migrate resource.
451

452
  """
453
  def POST(self):
454
    """Migrate all primary instances from a node.
455

456
    """
457
    node_name = self.items[0]
458

    
459
    if self.queryargs:
460
      # Support old-style requests
461
      if "live" in self.queryargs and "mode" in self.queryargs:
462
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
463
                                  " be passed")
464

    
465
      if "live" in self.queryargs:
466
        if self._checkIntVariable("live", default=1):
467
          mode = constants.HT_MIGRATION_LIVE
468
        else:
469
          mode = constants.HT_MIGRATION_NONLIVE
470
      else:
471
        mode = self._checkStringVariable("mode", default=None)
472

    
473
      data = {
474
        "mode": mode,
475
        }
476
    else:
477
      data = self.request_body
478

    
479
    op = baserlib.FillOpcode(opcodes.OpNodeMigrate, data, {
480
      "node_name": node_name,
481
      })
482

    
483
    return baserlib.SubmitJob([op])
484

    
485

    
486
class R_2_nodes_name_storage(baserlib.R_Generic):
487
  """/2/nodes/[node_name]/storage resource.
488

489
  """
490
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
491
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
492

    
493
  def GET(self):
494
    node_name = self.items[0]
495

    
496
    storage_type = self._checkStringVariable("storage_type", None)
497
    if not storage_type:
498
      raise http.HttpBadRequest("Missing the required 'storage_type'"
499
                                " parameter")
500

    
501
    output_fields = self._checkStringVariable("output_fields", None)
502
    if not output_fields:
503
      raise http.HttpBadRequest("Missing the required 'output_fields'"
504
                                " parameter")
505

    
506
    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
507
                                    storage_type=storage_type,
508
                                    output_fields=output_fields.split(","))
509
    return baserlib.SubmitJob([op])
510

    
511

    
512
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
513
  """/2/nodes/[node_name]/storage/modify resource.
514

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

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

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

    
529
    changes = {}
530

    
531
    if "allocatable" in self.queryargs:
532
      changes[constants.SF_ALLOCATABLE] = \
533
        bool(self._checkIntVariable("allocatable", default=1))
534

    
535
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
536
                                     storage_type=storage_type,
537
                                     name=name,
538
                                     changes=changes)
539
    return baserlib.SubmitJob([op])
540

    
541

    
542
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
543
  """/2/nodes/[node_name]/storage/repair resource.
544

545
  """
546
  def PUT(self):
547
    node_name = self.items[0]
548

    
549
    storage_type = self._checkStringVariable("storage_type", None)
550
    if not storage_type:
551
      raise http.HttpBadRequest("Missing the required 'storage_type'"
552
                                " parameter")
553

    
554
    name = self._checkStringVariable("name", None)
555
    if not name:
556
      raise http.HttpBadRequest("Missing the required 'name'"
557
                                " parameter")
558

    
559
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
560
                                     storage_type=storage_type,
561
                                     name=name)
562
    return baserlib.SubmitJob([op])
563

    
564

    
565
def _ParseCreateGroupRequest(data, dry_run):
566
  """Parses a request for creating a node group.
567

568
  @rtype: L{opcodes.OpGroupAdd}
569
  @return: Group creation opcode
570

571
  """
572
  override = {
573
    "dry_run": dry_run,
574
    }
575

    
576
  rename = {
577
    "name": "group_name",
578
    }
579

    
580
  return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
581
                             rename=rename)
582

    
583

    
584
class R_2_groups(baserlib.R_Generic):
585
  """/2/groups resource.
586

587
  """
588
  def GET(self):
589
    """Returns a list of all node groups.
590

591
    """
592
    client = baserlib.GetClient()
593

    
594
    if self.useBulk():
595
      bulkdata = client.QueryGroups([], G_FIELDS, False)
596
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
597
    else:
598
      data = client.QueryGroups([], ["name"], False)
599
      groupnames = [row[0] for row in data]
600
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
601
                                   uri_fields=("name", "uri"))
602

    
603
  def POST(self):
604
    """Create a node group.
605

606
    @return: a job id
607

608
    """
609
    baserlib.CheckType(self.request_body, dict, "Body contents")
610
    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
611
    return baserlib.SubmitJob([op])
612

    
613

    
614
class R_2_groups_name(baserlib.R_Generic):
615
  """/2/groups/[group_name] resource.
616

617
  """
618
  def GET(self):
619
    """Send information about a node group.
620

621
    """
622
    group_name = self.items[0]
623
    client = baserlib.GetClient()
624

    
625
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
626
                                            names=[group_name], fields=G_FIELDS,
627
                                            use_locking=self.useLocking())
628

    
629
    return baserlib.MapFields(G_FIELDS, result[0])
630

    
631
  def DELETE(self):
632
    """Delete a node group.
633

634
    """
635
    op = opcodes.OpGroupRemove(group_name=self.items[0],
636
                               dry_run=bool(self.dryRun()))
637

    
638
    return baserlib.SubmitJob([op])
639

    
640

    
641
def _ParseModifyGroupRequest(name, data):
642
  """Parses a request for modifying a node group.
643

644
  @rtype: L{opcodes.OpGroupSetParams}
645
  @return: Group modify opcode
646

647
  """
648
  return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
649
    "group_name": name,
650
    })
651

    
652

    
653

    
654
class R_2_groups_name_modify(baserlib.R_Generic):
655
  """/2/groups/[group_name]/modify resource.
656

657
  """
658
  def PUT(self):
659
    """Changes some parameters of node group.
660

661
    @return: a job id
662

663
    """
664
    baserlib.CheckType(self.request_body, dict, "Body contents")
665

    
666
    op = _ParseModifyGroupRequest(self.items[0], self.request_body)
667

    
668
    return baserlib.SubmitJob([op])
669

    
670

    
671
def _ParseRenameGroupRequest(name, data, dry_run):
672
  """Parses a request for renaming a node group.
673

674
  @type name: string
675
  @param name: name of the node group to rename
676
  @type data: dict
677
  @param data: the body received by the rename request
678
  @type dry_run: bool
679
  @param dry_run: whether to perform a dry run
680

681
  @rtype: L{opcodes.OpGroupRename}
682
  @return: Node group rename opcode
683

684
  """
685
  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
686
    "group_name": name,
687
    "dry_run": dry_run,
688
    })
689

    
690

    
691
class R_2_groups_name_rename(baserlib.R_Generic):
692
  """/2/groups/[group_name]/rename resource.
693

694
  """
695
  def PUT(self):
696
    """Changes the name of a node group.
697

698
    @return: a job id
699

700
    """
701
    baserlib.CheckType(self.request_body, dict, "Body contents")
702
    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
703
                                  self.dryRun())
704
    return baserlib.SubmitJob([op])
705

    
706

    
707
class R_2_groups_name_assign_nodes(baserlib.R_Generic):
708
  """/2/groups/[group_name]/assign-nodes resource.
709

710
  """
711
  def PUT(self):
712
    """Assigns nodes to a group.
713

714
    @return: a job id
715

716
    """
717
    op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
718
      "group_name": self.items[0],
719
      "dry_run": self.dryRun(),
720
      "force": self.useForce(),
721
      })
722

    
723
    return baserlib.SubmitJob([op])
724

    
725

    
726
def _ParseInstanceCreateRequestVersion1(data, dry_run):
727
  """Parses an instance creation request version 1.
728

729
  @rtype: L{opcodes.OpInstanceCreate}
730
  @return: Instance creation opcode
731

732
  """
733
  override = {
734
    "dry_run": dry_run,
735
    }
736

    
737
  rename = {
738
    "os": "os_type",
739
    "name": "instance_name",
740
    }
741

    
742
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
743
                             rename=rename)
744

    
745

    
746
class R_2_instances(baserlib.R_Generic):
747
  """/2/instances resource.
748

749
  """
750
  def GET(self):
751
    """Returns a list of all available instances.
752

753
    """
754
    client = baserlib.GetClient()
755

    
756
    use_locking = self.useLocking()
757
    if self.useBulk():
758
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
759
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
760
    else:
761
      instancesdata = client.QueryInstances([], ["name"], use_locking)
762
      instanceslist = [row[0] for row in instancesdata]
763
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
764
                                   uri_fields=("id", "uri"))
765

    
766
  def POST(self):
767
    """Create an instance.
768

769
    @return: a job id
770

771
    """
772
    if not isinstance(self.request_body, dict):
773
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
774

    
775
    # Default to request data version 0
776
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
777

    
778
    if data_version == 0:
779
      raise http.HttpBadRequest("Instance creation request version 0 is no"
780
                                " longer supported")
781
    elif data_version == 1:
782
      data = self.request_body.copy()
783
      # Remove "__version__"
784
      data.pop(_REQ_DATA_VERSION, None)
785
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
786
    else:
787
      raise http.HttpBadRequest("Unsupported request data version %s" %
788
                                data_version)
789

    
790
    return baserlib.SubmitJob([op])
791

    
792

    
793
class R_2_instances_name(baserlib.R_Generic):
794
  """/2/instances/[instance_name] resource.
795

796
  """
797
  def GET(self):
798
    """Send information about an instance.
799

800
    """
801
    client = baserlib.GetClient()
802
    instance_name = self.items[0]
803

    
804
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
805
                                            names=[instance_name],
806
                                            fields=I_FIELDS,
807
                                            use_locking=self.useLocking())
808

    
809
    return baserlib.MapFields(I_FIELDS, result[0])
810

    
811
  def DELETE(self):
812
    """Delete an instance.
813

814
    """
815
    op = opcodes.OpInstanceRemove(instance_name=self.items[0],
816
                                  ignore_failures=False,
817
                                  dry_run=bool(self.dryRun()))
818
    return baserlib.SubmitJob([op])
819

    
820

    
821
class R_2_instances_name_info(baserlib.R_Generic):
822
  """/2/instances/[instance_name]/info resource.
823

824
  """
825
  def GET(self):
826
    """Request detailed instance information.
827

828
    """
829
    instance_name = self.items[0]
830
    static = bool(self._checkIntVariable("static", default=0))
831

    
832
    op = opcodes.OpInstanceQueryData(instances=[instance_name],
833
                                     static=static)
834
    return baserlib.SubmitJob([op])
835

    
836

    
837
class R_2_instances_name_reboot(baserlib.R_Generic):
838
  """/2/instances/[instance_name]/reboot resource.
839

840
  Implements an instance reboot.
841

842
  """
843
  def POST(self):
844
    """Reboot an instance.
845

846
    The URI takes type=[hard|soft|full] and
847
    ignore_secondaries=[False|True] parameters.
848

849
    """
850
    instance_name = self.items[0]
851
    reboot_type = self.queryargs.get("type",
852
                                     [constants.INSTANCE_REBOOT_HARD])[0]
853
    ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
854
    op = opcodes.OpInstanceReboot(instance_name=instance_name,
855
                                  reboot_type=reboot_type,
856
                                  ignore_secondaries=ignore_secondaries,
857
                                  dry_run=bool(self.dryRun()))
858

    
859
    return baserlib.SubmitJob([op])
860

    
861

    
862
class R_2_instances_name_startup(baserlib.R_Generic):
863
  """/2/instances/[instance_name]/startup resource.
864

865
  Implements an instance startup.
866

867
  """
868
  def PUT(self):
869
    """Startup an instance.
870

871
    The URI takes force=[False|True] parameter to start the instance
872
    if even if secondary disks are failing.
873

874
    """
875
    instance_name = self.items[0]
876
    force_startup = bool(self._checkIntVariable("force"))
877
    no_remember = bool(self._checkIntVariable("no_remember"))
878
    op = opcodes.OpInstanceStartup(instance_name=instance_name,
879
                                   force=force_startup,
880
                                   dry_run=bool(self.dryRun()),
881
                                   no_remember=no_remember)
882

    
883
    return baserlib.SubmitJob([op])
884

    
885

    
886
def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
887
  """Parses a request for an instance shutdown.
888

889
  @rtype: L{opcodes.OpInstanceShutdown}
890
  @return: Instance shutdown opcode
891

892
  """
893
  return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
894
    "instance_name": name,
895
    "dry_run": dry_run,
896
    "no_remember": no_remember,
897
    })
898

    
899

    
900
class R_2_instances_name_shutdown(baserlib.R_Generic):
901
  """/2/instances/[instance_name]/shutdown resource.
902

903
  Implements an instance shutdown.
904

905
  """
906
  def PUT(self):
907
    """Shutdown an instance.
908

909
    @return: a job id
910

911
    """
912
    baserlib.CheckType(self.request_body, dict, "Body contents")
913

    
914
    no_remember = bool(self._checkIntVariable("no_remember"))
915
    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
916
                                       bool(self.dryRun()), no_remember)
917

    
918
    return baserlib.SubmitJob([op])
919

    
920

    
921
def _ParseInstanceReinstallRequest(name, data):
922
  """Parses a request for reinstalling an instance.
923

924
  """
925
  if not isinstance(data, dict):
926
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
927

    
928
  ostype = baserlib.CheckParameter(data, "os", default=None)
929
  start = baserlib.CheckParameter(data, "start", exptype=bool,
930
                                  default=True)
931
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
932

    
933
  ops = [
934
    opcodes.OpInstanceShutdown(instance_name=name),
935
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
936
                                osparams=osparams),
937
    ]
938

    
939
  if start:
940
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
941

    
942
  return ops
943

    
944

    
945
class R_2_instances_name_reinstall(baserlib.R_Generic):
946
  """/2/instances/[instance_name]/reinstall resource.
947

948
  Implements an instance reinstall.
949

950
  """
951
  def POST(self):
952
    """Reinstall an instance.
953

954
    The URI takes os=name and nostartup=[0|1] optional
955
    parameters. By default, the instance will be started
956
    automatically.
957

958
    """
959
    if self.request_body:
960
      if self.queryargs:
961
        raise http.HttpBadRequest("Can't combine query and body parameters")
962

    
963
      body = self.request_body
964
    elif self.queryargs:
965
      # Legacy interface, do not modify/extend
966
      body = {
967
        "os": self._checkStringVariable("os"),
968
        "start": not self._checkIntVariable("nostartup"),
969
        }
970
    else:
971
      body = {}
972

    
973
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
974

    
975
    return baserlib.SubmitJob(ops)
976

    
977

    
978
def _ParseInstanceReplaceDisksRequest(name, data):
979
  """Parses a request for an instance export.
980

981
  @rtype: L{opcodes.OpInstanceReplaceDisks}
982
  @return: Instance export opcode
983

984
  """
985
  override = {
986
    "instance_name": name,
987
    }
988

    
989
  # Parse disks
990
  try:
991
    raw_disks = data["disks"]
992
  except KeyError:
993
    pass
994
  else:
995
    if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
996
      # Backwards compatibility for strings of the format "1, 2, 3"
997
      try:
998
        data["disks"] = [int(part) for part in raw_disks.split(",")]
999
      except (TypeError, ValueError), err:
1000
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1001

    
1002
  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1003

    
1004

    
1005
class R_2_instances_name_replace_disks(baserlib.R_Generic):
1006
  """/2/instances/[instance_name]/replace-disks resource.
1007

1008
  """
1009
  def POST(self):
1010
    """Replaces disks on an instance.
1011

1012
    """
1013
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1014

    
1015
    return baserlib.SubmitJob([op])
1016

    
1017

    
1018
class R_2_instances_name_activate_disks(baserlib.R_Generic):
1019
  """/2/instances/[instance_name]/activate-disks resource.
1020

1021
  """
1022
  def PUT(self):
1023
    """Activate disks for an instance.
1024

1025
    The URI might contain ignore_size to ignore current recorded size.
1026

1027
    """
1028
    instance_name = self.items[0]
1029
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1030

    
1031
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1032
                                         ignore_size=ignore_size)
1033

    
1034
    return baserlib.SubmitJob([op])
1035

    
1036

    
1037
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1038
  """/2/instances/[instance_name]/deactivate-disks resource.
1039

1040
  """
1041
  def PUT(self):
1042
    """Deactivate disks for an instance.
1043

1044
    """
1045
    instance_name = self.items[0]
1046

    
1047
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1048

    
1049
    return baserlib.SubmitJob([op])
1050

    
1051

    
1052
class R_2_instances_name_prepare_export(baserlib.R_Generic):
1053
  """/2/instances/[instance_name]/prepare-export resource.
1054

1055
  """
1056
  def PUT(self):
1057
    """Prepares an export for an instance.
1058

1059
    @return: a job id
1060

1061
    """
1062
    instance_name = self.items[0]
1063
    mode = self._checkStringVariable("mode")
1064

    
1065
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1066
                                 mode=mode)
1067

    
1068
    return baserlib.SubmitJob([op])
1069

    
1070

    
1071
def _ParseExportInstanceRequest(name, data):
1072
  """Parses a request for an instance export.
1073

1074
  @rtype: L{opcodes.OpBackupExport}
1075
  @return: Instance export opcode
1076

1077
  """
1078
  # Rename "destination" to "target_node"
1079
  try:
1080
    data["target_node"] = data.pop("destination")
1081
  except KeyError:
1082
    pass
1083

    
1084
  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1085
    "instance_name": name,
1086
    })
1087

    
1088

    
1089
class R_2_instances_name_export(baserlib.R_Generic):
1090
  """/2/instances/[instance_name]/export resource.
1091

1092
  """
1093
  def PUT(self):
1094
    """Exports an instance.
1095

1096
    @return: a job id
1097

1098
    """
1099
    if not isinstance(self.request_body, dict):
1100
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1101

    
1102
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1103

    
1104
    return baserlib.SubmitJob([op])
1105

    
1106

    
1107
def _ParseMigrateInstanceRequest(name, data):
1108
  """Parses a request for an instance migration.
1109

1110
  @rtype: L{opcodes.OpInstanceMigrate}
1111
  @return: Instance migration opcode
1112

1113
  """
1114
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1115
    "instance_name": name,
1116
    })
1117

    
1118

    
1119
class R_2_instances_name_migrate(baserlib.R_Generic):
1120
  """/2/instances/[instance_name]/migrate resource.
1121

1122
  """
1123
  def PUT(self):
1124
    """Migrates an instance.
1125

1126
    @return: a job id
1127

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

    
1131
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1132

    
1133
    return baserlib.SubmitJob([op])
1134

    
1135

    
1136
class R_2_instances_name_failover(baserlib.R_Generic):
1137
  """/2/instances/[instance_name]/failover resource.
1138

1139
  """
1140
  def PUT(self):
1141
    """Does a failover of an instance.
1142

1143
    @return: a job id
1144

1145
    """
1146
    baserlib.CheckType(self.request_body, dict, "Body contents")
1147

    
1148
    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1149
      "instance_name": self.items[0],
1150
      })
1151

    
1152
    return baserlib.SubmitJob([op])
1153

    
1154

    
1155
def _ParseRenameInstanceRequest(name, data):
1156
  """Parses a request for renaming an instance.
1157

1158
  @rtype: L{opcodes.OpInstanceRename}
1159
  @return: Instance rename opcode
1160

1161
  """
1162
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1163
    "instance_name": name,
1164
    })
1165

    
1166

    
1167
class R_2_instances_name_rename(baserlib.R_Generic):
1168
  """/2/instances/[instance_name]/rename resource.
1169

1170
  """
1171
  def PUT(self):
1172
    """Changes the name of an instance.
1173

1174
    @return: a job id
1175

1176
    """
1177
    baserlib.CheckType(self.request_body, dict, "Body contents")
1178

    
1179
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1180

    
1181
    return baserlib.SubmitJob([op])
1182

    
1183

    
1184
def _ParseModifyInstanceRequest(name, data):
1185
  """Parses a request for modifying an instance.
1186

1187
  @rtype: L{opcodes.OpInstanceSetParams}
1188
  @return: Instance modify opcode
1189

1190
  """
1191
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1192
    "instance_name": name,
1193
    })
1194

    
1195

    
1196
class R_2_instances_name_modify(baserlib.R_Generic):
1197
  """/2/instances/[instance_name]/modify resource.
1198

1199
  """
1200
  def PUT(self):
1201
    """Changes some parameters of an instance.
1202

1203
    @return: a job id
1204

1205
    """
1206
    baserlib.CheckType(self.request_body, dict, "Body contents")
1207

    
1208
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1209

    
1210
    return baserlib.SubmitJob([op])
1211

    
1212

    
1213
class R_2_instances_name_disk_grow(baserlib.R_Generic):
1214
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1215

1216
  """
1217
  def POST(self):
1218
    """Increases the size of an instance disk.
1219

1220
    @return: a job id
1221

1222
    """
1223
    op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1224
      "instance_name": self.items[0],
1225
      "disk": int(self.items[1]),
1226
      })
1227

    
1228
    return baserlib.SubmitJob([op])
1229

    
1230

    
1231
class R_2_instances_name_console(baserlib.R_Generic):
1232
  """/2/instances/[instance_name]/console resource.
1233

1234
  """
1235
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1236

    
1237
  def GET(self):
1238
    """Request information for connecting to instance's console.
1239

1240
    @return: Serialized instance console description, see
1241
             L{objects.InstanceConsole}
1242

1243
    """
1244
    client = baserlib.GetClient()
1245

    
1246
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1247

    
1248
    if console is None:
1249
      raise http.HttpServiceUnavailable("Instance console unavailable")
1250

    
1251
    assert isinstance(console, dict)
1252
    return console
1253

    
1254

    
1255
def _GetQueryFields(args):
1256
  """
1257

1258
  """
1259
  try:
1260
    fields = args["fields"]
1261
  except KeyError:
1262
    raise http.HttpBadRequest("Missing 'fields' query argument")
1263

    
1264
  return _SplitQueryFields(fields[0])
1265

    
1266

    
1267
def _SplitQueryFields(fields):
1268
  """
1269

1270
  """
1271
  return [i.strip() for i in fields.split(",")]
1272

    
1273

    
1274
class R_2_query(baserlib.R_Generic):
1275
  """/2/query/[resource] resource.
1276

1277
  """
1278
  # Results might contain sensitive information
1279
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1280

    
1281
  def _Query(self, fields, filter_):
1282
    return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1283

    
1284
  def GET(self):
1285
    """Returns resource information.
1286

1287
    @return: Query result, see L{objects.QueryResponse}
1288

1289
    """
1290
    return self._Query(_GetQueryFields(self.queryargs), None)
1291

    
1292
  def PUT(self):
1293
    """Submits job querying for resources.
1294

1295
    @return: Query result, see L{objects.QueryResponse}
1296

1297
    """
1298
    body = self.request_body
1299

    
1300
    baserlib.CheckType(body, dict, "Body contents")
1301

    
1302
    try:
1303
      fields = body["fields"]
1304
    except KeyError:
1305
      fields = _GetQueryFields(self.queryargs)
1306

    
1307
    return self._Query(fields, self.request_body.get("filter", None))
1308

    
1309

    
1310
class R_2_query_fields(baserlib.R_Generic):
1311
  """/2/query/[resource]/fields resource.
1312

1313
  """
1314
  def GET(self):
1315
    """Retrieves list of available fields for a resource.
1316

1317
    @return: List of serialized L{objects.QueryFieldDefinition}
1318

1319
    """
1320
    try:
1321
      raw_fields = self.queryargs["fields"]
1322
    except KeyError:
1323
      fields = None
1324
    else:
1325
      fields = _SplitQueryFields(raw_fields[0])
1326

    
1327
    return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1328

    
1329

    
1330
class _R_Tags(baserlib.R_Generic):
1331
  """ Quasiclass for tagging resources
1332

1333
  Manages tags. When inheriting this class you must define the
1334
  TAG_LEVEL for it.
1335

1336
  """
1337
  TAG_LEVEL = None
1338

    
1339
  def __init__(self, items, queryargs, req):
1340
    """A tag resource constructor.
1341

1342
    We have to override the default to sort out cluster naming case.
1343

1344
    """
1345
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1346

    
1347
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1348
      self.name = None
1349
    else:
1350
      self.name = items[0]
1351

    
1352
  def GET(self):
1353
    """Returns a list of tags.
1354

1355
    Example: ["tag1", "tag2", "tag3"]
1356

1357
    """
1358
    # pylint: disable-msg=W0212
1359
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1360

    
1361
  def PUT(self):
1362
    """Add a set of tags.
1363

1364
    The request as a list of strings should be PUT to this URI. And
1365
    you'll have back a job id.
1366

1367
    """
1368
    # pylint: disable-msg=W0212
1369
    if "tag" not in self.queryargs:
1370
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1371
                                " the 'tag' parameter")
1372
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1373
                              self.queryargs["tag"], name=self.name,
1374
                              dry_run=bool(self.dryRun()))
1375

    
1376
  def DELETE(self):
1377
    """Delete a tag.
1378

1379
    In order to delete a set of tags, the DELETE
1380
    request should be addressed to URI like:
1381
    /tags?tag=[tag]&tag=[tag]
1382

1383
    """
1384
    # pylint: disable-msg=W0212
1385
    if "tag" not in self.queryargs:
1386
      # no we not gonna delete all tags
1387
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1388
                                " tag(s) using the 'tag' parameter")
1389
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1390
                                 self.queryargs["tag"],
1391
                                 name=self.name,
1392
                                 dry_run=bool(self.dryRun()))
1393

    
1394

    
1395
class R_2_instances_name_tags(_R_Tags):
1396
  """ /2/instances/[instance_name]/tags resource.
1397

1398
  Manages per-instance tags.
1399

1400
  """
1401
  TAG_LEVEL = constants.TAG_INSTANCE
1402

    
1403

    
1404
class R_2_nodes_name_tags(_R_Tags):
1405
  """ /2/nodes/[node_name]/tags resource.
1406

1407
  Manages per-node tags.
1408

1409
  """
1410
  TAG_LEVEL = constants.TAG_NODE
1411

    
1412

    
1413
class R_2_groups_name_tags(_R_Tags):
1414
  """ /2/groups/[group_name]/tags resource.
1415

1416
  Manages per-nodegroup tags.
1417

1418
  """
1419
  TAG_LEVEL = constants.TAG_NODEGROUP
1420

    
1421

    
1422
class R_2_tags(_R_Tags):
1423
  """ /2/tags resource.
1424

1425
  Manages cluster tags.
1426

1427
  """
1428
  TAG_LEVEL = constants.TAG_CLUSTER