Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 8fd625fc

History | View | Annotate | Download (37.5 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 resource implementations.
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
In the context of this module POST on ``/2/instances`` to change an existing
32
entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
33
new instance) with a name specified in the request.
34

35
Quoting from RFC2616, section 9.6::
36

37
  The fundamental difference between the POST and PUT requests is reflected in
38
  the different meaning of the Request-URI. The URI in a POST request
39
  identifies the resource that will handle the enclosed entity. That resource
40
  might be a data-accepting process, a gateway to some other protocol, or a
41
  separate entity that accepts annotations. In contrast, the URI in a PUT
42
  request identifies the entity enclosed with the request -- the user agent
43
  knows what URI is intended and the server MUST NOT attempt to apply the
44
  request to some other resource. If the server desires that the request be
45
  applied to a different URI, it MUST send a 301 (Moved Permanently) response;
46
  the user agent MAY then make its own decision regarding whether or not to
47
  redirect the request.
48

49
So when adding new methods, if they are operating on the URI entity itself,
50
PUT should be prefered over POST.
51

52
"""
53

    
54
# pylint: disable-msg=C0103
55

    
56
# C0103: Invalid name, since the R_* names are not conforming
57

    
58
from ganeti import opcodes
59
from ganeti import http
60
from ganeti import constants
61
from ganeti import cli
62
from ganeti import rapi
63
from ganeti import ht
64
from ganeti import compat
65
from ganeti import ssconf
66
from ganeti.rapi import baserlib
67

    
68

    
69
_COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
70
I_FIELDS = ["name", "admin_state", "os",
71
            "pnode", "snodes",
72
            "disk_template",
73
            "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
74
            "network_port",
75
            "disk.sizes", "disk_usage",
76
            "beparams", "hvparams",
77
            "oper_state", "oper_ram", "oper_vcpus", "status",
78
            "custom_hvparams", "custom_beparams", "custom_nicparams",
79
            ] + _COMMON_FIELDS
80

    
81
N_FIELDS = ["name", "offline", "master_candidate", "drained",
82
            "dtotal", "dfree",
83
            "mtotal", "mnode", "mfree",
84
            "pinst_cnt", "sinst_cnt",
85
            "ctotal", "cnodes", "csockets",
86
            "pip", "sip", "role",
87
            "pinst_list", "sinst_list",
88
            "master_capable", "vm_capable",
89
            "group.uuid",
90
            ] + _COMMON_FIELDS
91

    
92
G_FIELDS = [
93
  "alloc_policy",
94
  "name",
95
  "node_cnt",
96
  "node_list",
97
  ] + _COMMON_FIELDS
98

    
99
J_FIELDS_BULK = [
100
  "id", "ops", "status", "summary",
101
  "opstatus",
102
  "received_ts", "start_ts", "end_ts",
103
  ]
104

    
105
J_FIELDS = J_FIELDS_BULK + [
106
  "oplog",
107
  "opresult",
108
  ]
109

    
110
_NR_DRAINED = "drained"
111
_NR_MASTER_CANDIATE = "master-candidate"
112
_NR_MASTER = "master"
113
_NR_OFFLINE = "offline"
114
_NR_REGULAR = "regular"
115

    
116
_NR_MAP = {
117
  constants.NR_MASTER: _NR_MASTER,
118
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIATE,
119
  constants.NR_DRAINED: _NR_DRAINED,
120
  constants.NR_OFFLINE: _NR_OFFLINE,
121
  constants.NR_REGULAR: _NR_REGULAR,
122
  }
123

    
124
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
125

    
126
# Request data version field
127
_REQ_DATA_VERSION = "__version__"
128

    
129
# Feature string for instance creation request data version 1
130
_INST_CREATE_REQV1 = "instance-create-reqv1"
131

    
132
# Feature string for instance reinstall request version 1
133
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
134

    
135
# Feature string for node migration version 1
136
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
137

    
138
# Feature string for node evacuation with LU-generated jobs
139
_NODE_EVAC_RES1 = "node-evac-res1"
140

    
141
ALL_FEATURES = frozenset([
142
  _INST_CREATE_REQV1,
143
  _INST_REINSTALL_REQV1,
144
  _NODE_MIGRATE_REQV1,
145
  _NODE_EVAC_RES1,
146
  ])
147

    
148
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
149
_WFJC_TIMEOUT = 10
150

    
151

    
152
class R_root(baserlib.ResourceBase):
153
  """/ resource.
154

155
  """
156
  @staticmethod
157
  def GET():
158
    """Supported for legacy reasons.
159

160
    """
161
    return None
162

    
163

    
164
class R_version(baserlib.ResourceBase):
165
  """/version resource.
166

167
  This resource should be used to determine the remote API version and
168
  to adapt clients accordingly.
169

170
  """
171
  @staticmethod
172
  def GET():
173
    """Returns the remote API version.
174

175
    """
176
    return constants.RAPI_VERSION
177

    
178

    
179
class R_2_info(baserlib.ResourceBase):
180
  """/2/info resource.
181

182
  """
183
  def GET(self):
184
    """Returns cluster information.
185

186
    """
187
    client = self.GetClient()
188
    return client.QueryClusterInfo()
189

    
190

    
191
class R_2_features(baserlib.ResourceBase):
192
  """/2/features resource.
193

194
  """
195
  @staticmethod
196
  def GET():
197
    """Returns list of optional RAPI features implemented.
198

199
    """
200
    return list(ALL_FEATURES)
201

    
202

    
203
class R_2_os(baserlib.ResourceBase):
204
  """/2/os resource.
205

206
  """
207
  def GET(self):
208
    """Return a list of all OSes.
209

210
    Can return error 500 in case of a problem.
211

212
    Example: ["debian-etch"]
213

214
    """
215
    cl = self.GetClient()
216
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
217
    job_id = self.SubmitJob([op], cl=cl)
218
    # we use custom feedback function, instead of print we log the status
219
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
220
    diagnose_data = result[0]
221

    
222
    if not isinstance(diagnose_data, list):
223
      raise http.HttpBadGateway(message="Can't get OS list")
224

    
225
    os_names = []
226
    for (name, variants) in diagnose_data:
227
      os_names.extend(cli.CalculateOSNames(name, variants))
228

    
229
    return os_names
230

    
231

    
232
class R_2_redist_config(baserlib.OpcodeResource):
233
  """/2/redistribute-config resource.
234

235
  """
236
  PUT_OPCODE = opcodes.OpClusterRedistConf
237

    
238

    
239
class R_2_cluster_modify(baserlib.OpcodeResource):
240
  """/2/modify resource.
241

242
  """
243
  PUT_OPCODE = opcodes.OpClusterSetParams
244

    
245

    
246
class R_2_jobs(baserlib.ResourceBase):
247
  """/2/jobs resource.
248

249
  """
250
  def GET(self):
251
    """Returns a dictionary of jobs.
252

253
    @return: a dictionary with jobs id and uri.
254

255
    """
256
    client = self.GetClient()
257

    
258
    if self.useBulk():
259
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
260
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
261
    else:
262
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
263
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
264
                                   uri_fields=("id", "uri"))
265

    
266

    
267
class R_2_jobs_id(baserlib.ResourceBase):
268
  """/2/jobs/[job_id] resource.
269

270
  """
271
  def GET(self):
272
    """Returns a job status.
273

274
    @return: a dictionary with job parameters.
275
        The result includes:
276
            - id: job ID as a number
277
            - status: current job status as a string
278
            - ops: involved OpCodes as a list of dictionaries for each
279
              opcodes in the job
280
            - opstatus: OpCodes status as a list
281
            - opresult: OpCodes results as a list of lists
282

283
    """
284
    job_id = self.items[0]
285
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
286
    if result is None:
287
      raise http.HttpNotFound()
288
    return baserlib.MapFields(J_FIELDS, result)
289

    
290
  def DELETE(self):
291
    """Cancel not-yet-started job.
292

293
    """
294
    job_id = self.items[0]
295
    result = self.GetClient().CancelJob(job_id)
296
    return result
297

    
298

    
299
class R_2_jobs_id_wait(baserlib.ResourceBase):
300
  """/2/jobs/[job_id]/wait resource.
301

302
  """
303
  # WaitForJobChange provides access to sensitive information and blocks
304
  # machine resources (it's a blocking RAPI call), hence restricting access.
305
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
306

    
307
  def GET(self):
308
    """Waits for job changes.
309

310
    """
311
    job_id = self.items[0]
312

    
313
    fields = self.getBodyParameter("fields")
314
    prev_job_info = self.getBodyParameter("previous_job_info", None)
315
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
316

    
317
    if not isinstance(fields, list):
318
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
319

    
320
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
321
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
322
                                " be a list")
323

    
324
    if not (prev_log_serial is None or
325
            isinstance(prev_log_serial, (int, long))):
326
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
327
                                " be a number")
328

    
329
    client = self.GetClient()
330
    result = client.WaitForJobChangeOnce(job_id, fields,
331
                                         prev_job_info, prev_log_serial,
332
                                         timeout=_WFJC_TIMEOUT)
333
    if not result:
334
      raise http.HttpNotFound()
335

    
336
    if result == constants.JOB_NOTCHANGED:
337
      # No changes
338
      return None
339

    
340
    (job_info, log_entries) = result
341

    
342
    return {
343
      "job_info": job_info,
344
      "log_entries": log_entries,
345
      }
346

    
347

    
348
class R_2_nodes(baserlib.ResourceBase):
349
  """/2/nodes resource.
350

351
  """
352
  def GET(self):
353
    """Returns a list of all nodes.
354

355
    """
356
    client = self.GetClient()
357

    
358
    if self.useBulk():
359
      bulkdata = client.QueryNodes([], N_FIELDS, False)
360
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
361
    else:
362
      nodesdata = client.QueryNodes([], ["name"], False)
363
      nodeslist = [row[0] for row in nodesdata]
364
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
365
                                   uri_fields=("id", "uri"))
366

    
367

    
368
class R_2_nodes_name(baserlib.ResourceBase):
369
  """/2/nodes/[node_name] resource.
370

371
  """
372
  def GET(self):
373
    """Send information about a node.
374

375
    """
376
    node_name = self.items[0]
377
    client = self.GetClient()
378

    
379
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
380
                                            names=[node_name], fields=N_FIELDS,
381
                                            use_locking=self.useLocking())
382

    
383
    return baserlib.MapFields(N_FIELDS, result[0])
384

    
385

    
386
class R_2_nodes_name_role(baserlib.ResourceBase):
387
  """ /2/nodes/[node_name]/role resource.
388

389
  """
390
  def GET(self):
391
    """Returns the current node role.
392

393
    @return: Node role
394

395
    """
396
    node_name = self.items[0]
397
    client = self.GetClient()
398
    result = client.QueryNodes(names=[node_name], fields=["role"],
399
                               use_locking=self.useLocking())
400

    
401
    return _NR_MAP[result[0][0]]
402

    
403
  def PUT(self):
404
    """Sets the node role.
405

406
    @return: a job id
407

408
    """
409
    if not isinstance(self.request_body, basestring):
410
      raise http.HttpBadRequest("Invalid body contents, not a string")
411

    
412
    node_name = self.items[0]
413
    role = self.request_body
414

    
415
    if role == _NR_REGULAR:
416
      candidate = False
417
      offline = False
418
      drained = False
419

    
420
    elif role == _NR_MASTER_CANDIATE:
421
      candidate = True
422
      offline = drained = None
423

    
424
    elif role == _NR_DRAINED:
425
      drained = True
426
      candidate = offline = None
427

    
428
    elif role == _NR_OFFLINE:
429
      offline = True
430
      candidate = drained = None
431

    
432
    else:
433
      raise http.HttpBadRequest("Can't set '%s' role" % role)
434

    
435
    op = opcodes.OpNodeSetParams(node_name=node_name,
436
                                 master_candidate=candidate,
437
                                 offline=offline,
438
                                 drained=drained,
439
                                 force=bool(self.useForce()))
440

    
441
    return self.SubmitJob([op])
442

    
443

    
444
class R_2_nodes_name_evacuate(baserlib.ResourceBase):
445
  """/2/nodes/[node_name]/evacuate resource.
446

447
  """
448
  def POST(self):
449
    """Evacuate all instances off a node.
450

451
    """
452
    op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, {
453
      "node_name": self.items[0],
454
      "dry_run": self.dryRun(),
455
      })
456

    
457
    return self.SubmitJob([op])
458

    
459

    
460
class R_2_nodes_name_migrate(baserlib.ResourceBase):
461
  """/2/nodes/[node_name]/migrate resource.
462

463
  """
464
  def POST(self):
465
    """Migrate all primary instances from a node.
466

467
    """
468
    node_name = self.items[0]
469

    
470
    if self.queryargs:
471
      # Support old-style requests
472
      if "live" in self.queryargs and "mode" in self.queryargs:
473
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
474
                                  " be passed")
475

    
476
      if "live" in self.queryargs:
477
        if self._checkIntVariable("live", default=1):
478
          mode = constants.HT_MIGRATION_LIVE
479
        else:
480
          mode = constants.HT_MIGRATION_NONLIVE
481
      else:
482
        mode = self._checkStringVariable("mode", default=None)
483

    
484
      data = {
485
        "mode": mode,
486
        }
487
    else:
488
      data = self.request_body
489

    
490
    op = baserlib.FillOpcode(opcodes.OpNodeMigrate, data, {
491
      "node_name": node_name,
492
      })
493

    
494
    return self.SubmitJob([op])
495

    
496

    
497
class R_2_nodes_name_storage(baserlib.ResourceBase):
498
  """/2/nodes/[node_name]/storage resource.
499

500
  """
501
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
502
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
503

    
504
  def GET(self):
505
    node_name = self.items[0]
506

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

    
512
    output_fields = self._checkStringVariable("output_fields", None)
513
    if not output_fields:
514
      raise http.HttpBadRequest("Missing the required 'output_fields'"
515
                                " parameter")
516

    
517
    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
518
                                    storage_type=storage_type,
519
                                    output_fields=output_fields.split(","))
520
    return self.SubmitJob([op])
521

    
522

    
523
class R_2_nodes_name_storage_modify(baserlib.ResourceBase):
524
  """/2/nodes/[node_name]/storage/modify resource.
525

526
  """
527
  def PUT(self):
528
    node_name = self.items[0]
529

    
530
    storage_type = self._checkStringVariable("storage_type", None)
531
    if not storage_type:
532
      raise http.HttpBadRequest("Missing the required 'storage_type'"
533
                                " parameter")
534

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

    
540
    changes = {}
541

    
542
    if "allocatable" in self.queryargs:
543
      changes[constants.SF_ALLOCATABLE] = \
544
        bool(self._checkIntVariable("allocatable", default=1))
545

    
546
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
547
                                     storage_type=storage_type,
548
                                     name=name,
549
                                     changes=changes)
550
    return self.SubmitJob([op])
551

    
552

    
553
class R_2_nodes_name_storage_repair(baserlib.ResourceBase):
554
  """/2/nodes/[node_name]/storage/repair resource.
555

556
  """
557
  def PUT(self):
558
    node_name = self.items[0]
559

    
560
    storage_type = self._checkStringVariable("storage_type", None)
561
    if not storage_type:
562
      raise http.HttpBadRequest("Missing the required 'storage_type'"
563
                                " parameter")
564

    
565
    name = self._checkStringVariable("name", None)
566
    if not name:
567
      raise http.HttpBadRequest("Missing the required 'name'"
568
                                " parameter")
569

    
570
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
571
                                     storage_type=storage_type,
572
                                     name=name)
573
    return self.SubmitJob([op])
574

    
575

    
576
def _ParseCreateGroupRequest(data, dry_run):
577
  """Parses a request for creating a node group.
578

579
  @rtype: L{opcodes.OpGroupAdd}
580
  @return: Group creation opcode
581

582
  """
583
  override = {
584
    "dry_run": dry_run,
585
    }
586

    
587
  rename = {
588
    "name": "group_name",
589
    }
590

    
591
  return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
592
                             rename=rename)
593

    
594

    
595
class R_2_groups(baserlib.ResourceBase):
596
  """/2/groups resource.
597

598
  """
599
  def GET(self):
600
    """Returns a list of all node groups.
601

602
    """
603
    client = self.GetClient()
604

    
605
    if self.useBulk():
606
      bulkdata = client.QueryGroups([], G_FIELDS, False)
607
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
608
    else:
609
      data = client.QueryGroups([], ["name"], False)
610
      groupnames = [row[0] for row in data]
611
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
612
                                   uri_fields=("name", "uri"))
613

    
614
  def POST(self):
615
    """Create a node group.
616

617
    @return: a job id
618

619
    """
620
    baserlib.CheckType(self.request_body, dict, "Body contents")
621
    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
622
    return self.SubmitJob([op])
623

    
624

    
625
class R_2_groups_name(baserlib.ResourceBase):
626
  """/2/groups/[group_name] resource.
627

628
  """
629
  def GET(self):
630
    """Send information about a node group.
631

632
    """
633
    group_name = self.items[0]
634
    client = self.GetClient()
635

    
636
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
637
                                            names=[group_name], fields=G_FIELDS,
638
                                            use_locking=self.useLocking())
639

    
640
    return baserlib.MapFields(G_FIELDS, result[0])
641

    
642
  def DELETE(self):
643
    """Delete a node group.
644

645
    """
646
    op = opcodes.OpGroupRemove(group_name=self.items[0],
647
                               dry_run=bool(self.dryRun()))
648

    
649
    return self.SubmitJob([op])
650

    
651

    
652
def _ParseModifyGroupRequest(name, data):
653
  """Parses a request for modifying a node group.
654

655
  @rtype: L{opcodes.OpGroupSetParams}
656
  @return: Group modify opcode
657

658
  """
659
  return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
660
    "group_name": name,
661
    })
662

    
663

    
664
class R_2_groups_name_modify(baserlib.ResourceBase):
665
  """/2/groups/[group_name]/modify resource.
666

667
  """
668
  def PUT(self):
669
    """Changes some parameters of node group.
670

671
    @return: a job id
672

673
    """
674
    baserlib.CheckType(self.request_body, dict, "Body contents")
675

    
676
    op = _ParseModifyGroupRequest(self.items[0], self.request_body)
677

    
678
    return self.SubmitJob([op])
679

    
680

    
681
def _ParseRenameGroupRequest(name, data, dry_run):
682
  """Parses a request for renaming a node group.
683

684
  @type name: string
685
  @param name: name of the node group to rename
686
  @type data: dict
687
  @param data: the body received by the rename request
688
  @type dry_run: bool
689
  @param dry_run: whether to perform a dry run
690

691
  @rtype: L{opcodes.OpGroupRename}
692
  @return: Node group rename opcode
693

694
  """
695
  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
696
    "group_name": name,
697
    "dry_run": dry_run,
698
    })
699

    
700

    
701
class R_2_groups_name_rename(baserlib.ResourceBase):
702
  """/2/groups/[group_name]/rename resource.
703

704
  """
705
  def PUT(self):
706
    """Changes the name of a node group.
707

708
    @return: a job id
709

710
    """
711
    baserlib.CheckType(self.request_body, dict, "Body contents")
712
    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
713
                                  self.dryRun())
714
    return self.SubmitJob([op])
715

    
716

    
717
class R_2_groups_name_assign_nodes(baserlib.ResourceBase):
718
  """/2/groups/[group_name]/assign-nodes resource.
719

720
  """
721
  def PUT(self):
722
    """Assigns nodes to a group.
723

724
    @return: a job id
725

726
    """
727
    op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
728
      "group_name": self.items[0],
729
      "dry_run": self.dryRun(),
730
      "force": self.useForce(),
731
      })
732

    
733
    return self.SubmitJob([op])
734

    
735

    
736
def _ParseInstanceCreateRequestVersion1(data, dry_run):
737
  """Parses an instance creation request version 1.
738

739
  @rtype: L{opcodes.OpInstanceCreate}
740
  @return: Instance creation opcode
741

742
  """
743
  override = {
744
    "dry_run": dry_run,
745
    }
746

    
747
  rename = {
748
    "os": "os_type",
749
    "name": "instance_name",
750
    }
751

    
752
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
753
                             rename=rename)
754

    
755

    
756
class R_2_instances(baserlib.ResourceBase):
757
  """/2/instances resource.
758

759
  """
760
  def GET(self):
761
    """Returns a list of all available instances.
762

763
    """
764
    client = self.GetClient()
765

    
766
    use_locking = self.useLocking()
767
    if self.useBulk():
768
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
769
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
770
    else:
771
      instancesdata = client.QueryInstances([], ["name"], use_locking)
772
      instanceslist = [row[0] for row in instancesdata]
773
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
774
                                   uri_fields=("id", "uri"))
775

    
776
  def POST(self):
777
    """Create an instance.
778

779
    @return: a job id
780

781
    """
782
    if not isinstance(self.request_body, dict):
783
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
784

    
785
    # Default to request data version 0
786
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
787

    
788
    if data_version == 0:
789
      raise http.HttpBadRequest("Instance creation request version 0 is no"
790
                                " longer supported")
791
    elif data_version == 1:
792
      data = self.request_body.copy()
793
      # Remove "__version__"
794
      data.pop(_REQ_DATA_VERSION, None)
795
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
796
    else:
797
      raise http.HttpBadRequest("Unsupported request data version %s" %
798
                                data_version)
799

    
800
    return self.SubmitJob([op])
801

    
802

    
803
class R_2_instances_name(baserlib.ResourceBase):
804
  """/2/instances/[instance_name] resource.
805

806
  """
807
  def GET(self):
808
    """Send information about an instance.
809

810
    """
811
    client = self.GetClient()
812
    instance_name = self.items[0]
813

    
814
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
815
                                            names=[instance_name],
816
                                            fields=I_FIELDS,
817
                                            use_locking=self.useLocking())
818

    
819
    return baserlib.MapFields(I_FIELDS, result[0])
820

    
821
  def DELETE(self):
822
    """Delete an instance.
823

824
    """
825
    op = opcodes.OpInstanceRemove(instance_name=self.items[0],
826
                                  ignore_failures=False,
827
                                  dry_run=bool(self.dryRun()))
828
    return self.SubmitJob([op])
829

    
830

    
831
class R_2_instances_name_info(baserlib.ResourceBase):
832
  """/2/instances/[instance_name]/info resource.
833

834
  """
835
  def GET(self):
836
    """Request detailed instance information.
837

838
    """
839
    instance_name = self.items[0]
840
    static = bool(self._checkIntVariable("static", default=0))
841

    
842
    op = opcodes.OpInstanceQueryData(instances=[instance_name],
843
                                     static=static)
844
    return self.SubmitJob([op])
845

    
846

    
847
class R_2_instances_name_reboot(baserlib.ResourceBase):
848
  """/2/instances/[instance_name]/reboot resource.
849

850
  Implements an instance reboot.
851

852
  """
853
  def POST(self):
854
    """Reboot an instance.
855

856
    The URI takes type=[hard|soft|full] and
857
    ignore_secondaries=[False|True] parameters.
858

859
    """
860
    instance_name = self.items[0]
861
    reboot_type = self.queryargs.get("type",
862
                                     [constants.INSTANCE_REBOOT_HARD])[0]
863
    ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
864
    op = opcodes.OpInstanceReboot(instance_name=instance_name,
865
                                  reboot_type=reboot_type,
866
                                  ignore_secondaries=ignore_secondaries,
867
                                  dry_run=bool(self.dryRun()))
868

    
869
    return self.SubmitJob([op])
870

    
871

    
872
class R_2_instances_name_startup(baserlib.ResourceBase):
873
  """/2/instances/[instance_name]/startup resource.
874

875
  Implements an instance startup.
876

877
  """
878
  def PUT(self):
879
    """Startup an instance.
880

881
    The URI takes force=[False|True] parameter to start the instance
882
    if even if secondary disks are failing.
883

884
    """
885
    instance_name = self.items[0]
886
    force_startup = bool(self._checkIntVariable("force"))
887
    no_remember = bool(self._checkIntVariable("no_remember"))
888
    op = opcodes.OpInstanceStartup(instance_name=instance_name,
889
                                   force=force_startup,
890
                                   dry_run=bool(self.dryRun()),
891
                                   no_remember=no_remember)
892

    
893
    return self.SubmitJob([op])
894

    
895

    
896
def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
897
  """Parses a request for an instance shutdown.
898

899
  @rtype: L{opcodes.OpInstanceShutdown}
900
  @return: Instance shutdown opcode
901

902
  """
903
  return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
904
    "instance_name": name,
905
    "dry_run": dry_run,
906
    "no_remember": no_remember,
907
    })
908

    
909

    
910
class R_2_instances_name_shutdown(baserlib.ResourceBase):
911
  """/2/instances/[instance_name]/shutdown resource.
912

913
  Implements an instance shutdown.
914

915
  """
916
  def PUT(self):
917
    """Shutdown an instance.
918

919
    @return: a job id
920

921
    """
922
    baserlib.CheckType(self.request_body, dict, "Body contents")
923

    
924
    no_remember = bool(self._checkIntVariable("no_remember"))
925
    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
926
                                       bool(self.dryRun()), no_remember)
927

    
928
    return self.SubmitJob([op])
929

    
930

    
931
def _ParseInstanceReinstallRequest(name, data):
932
  """Parses a request for reinstalling an instance.
933

934
  """
935
  if not isinstance(data, dict):
936
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
937

    
938
  ostype = baserlib.CheckParameter(data, "os", default=None)
939
  start = baserlib.CheckParameter(data, "start", exptype=bool,
940
                                  default=True)
941
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
942

    
943
  ops = [
944
    opcodes.OpInstanceShutdown(instance_name=name),
945
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
946
                                osparams=osparams),
947
    ]
948

    
949
  if start:
950
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
951

    
952
  return ops
953

    
954

    
955
class R_2_instances_name_reinstall(baserlib.ResourceBase):
956
  """/2/instances/[instance_name]/reinstall resource.
957

958
  Implements an instance reinstall.
959

960
  """
961
  def POST(self):
962
    """Reinstall an instance.
963

964
    The URI takes os=name and nostartup=[0|1] optional
965
    parameters. By default, the instance will be started
966
    automatically.
967

968
    """
969
    if self.request_body:
970
      if self.queryargs:
971
        raise http.HttpBadRequest("Can't combine query and body parameters")
972

    
973
      body = self.request_body
974
    elif self.queryargs:
975
      # Legacy interface, do not modify/extend
976
      body = {
977
        "os": self._checkStringVariable("os"),
978
        "start": not self._checkIntVariable("nostartup"),
979
        }
980
    else:
981
      body = {}
982

    
983
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
984

    
985
    return self.SubmitJob(ops)
986

    
987

    
988
def _ParseInstanceReplaceDisksRequest(name, data):
989
  """Parses a request for an instance export.
990

991
  @rtype: L{opcodes.OpInstanceReplaceDisks}
992
  @return: Instance export opcode
993

994
  """
995
  override = {
996
    "instance_name": name,
997
    }
998

    
999
  # Parse disks
1000
  try:
1001
    raw_disks = data["disks"]
1002
  except KeyError:
1003
    pass
1004
  else:
1005
    if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
1006
      # Backwards compatibility for strings of the format "1, 2, 3"
1007
      try:
1008
        data["disks"] = [int(part) for part in raw_disks.split(",")]
1009
      except (TypeError, ValueError), err:
1010
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1011

    
1012
  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1013

    
1014

    
1015
class R_2_instances_name_replace_disks(baserlib.ResourceBase):
1016
  """/2/instances/[instance_name]/replace-disks resource.
1017

1018
  """
1019
  def POST(self):
1020
    """Replaces disks on an instance.
1021

1022
    """
1023
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1024

    
1025
    return self.SubmitJob([op])
1026

    
1027

    
1028
class R_2_instances_name_activate_disks(baserlib.ResourceBase):
1029
  """/2/instances/[instance_name]/activate-disks resource.
1030

1031
  """
1032
  def PUT(self):
1033
    """Activate disks for an instance.
1034

1035
    The URI might contain ignore_size to ignore current recorded size.
1036

1037
    """
1038
    instance_name = self.items[0]
1039
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1040

    
1041
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1042
                                         ignore_size=ignore_size)
1043

    
1044
    return self.SubmitJob([op])
1045

    
1046

    
1047
class R_2_instances_name_deactivate_disks(baserlib.ResourceBase):
1048
  """/2/instances/[instance_name]/deactivate-disks resource.
1049

1050
  """
1051
  def PUT(self):
1052
    """Deactivate disks for an instance.
1053

1054
    """
1055
    instance_name = self.items[0]
1056

    
1057
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1058

    
1059
    return self.SubmitJob([op])
1060

    
1061

    
1062
class R_2_instances_name_prepare_export(baserlib.ResourceBase):
1063
  """/2/instances/[instance_name]/prepare-export resource.
1064

1065
  """
1066
  def PUT(self):
1067
    """Prepares an export for an instance.
1068

1069
    @return: a job id
1070

1071
    """
1072
    instance_name = self.items[0]
1073
    mode = self._checkStringVariable("mode")
1074

    
1075
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1076
                                 mode=mode)
1077

    
1078
    return self.SubmitJob([op])
1079

    
1080

    
1081
def _ParseExportInstanceRequest(name, data):
1082
  """Parses a request for an instance export.
1083

1084
  @rtype: L{opcodes.OpBackupExport}
1085
  @return: Instance export opcode
1086

1087
  """
1088
  # Rename "destination" to "target_node"
1089
  try:
1090
    data["target_node"] = data.pop("destination")
1091
  except KeyError:
1092
    pass
1093

    
1094
  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1095
    "instance_name": name,
1096
    })
1097

    
1098

    
1099
class R_2_instances_name_export(baserlib.ResourceBase):
1100
  """/2/instances/[instance_name]/export resource.
1101

1102
  """
1103
  def PUT(self):
1104
    """Exports an instance.
1105

1106
    @return: a job id
1107

1108
    """
1109
    if not isinstance(self.request_body, dict):
1110
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1111

    
1112
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1113

    
1114
    return self.SubmitJob([op])
1115

    
1116

    
1117
def _ParseMigrateInstanceRequest(name, data):
1118
  """Parses a request for an instance migration.
1119

1120
  @rtype: L{opcodes.OpInstanceMigrate}
1121
  @return: Instance migration opcode
1122

1123
  """
1124
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1125
    "instance_name": name,
1126
    })
1127

    
1128

    
1129
class R_2_instances_name_migrate(baserlib.ResourceBase):
1130
  """/2/instances/[instance_name]/migrate resource.
1131

1132
  """
1133
  def PUT(self):
1134
    """Migrates an instance.
1135

1136
    @return: a job id
1137

1138
    """
1139
    baserlib.CheckType(self.request_body, dict, "Body contents")
1140

    
1141
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1142

    
1143
    return self.SubmitJob([op])
1144

    
1145

    
1146
class R_2_instances_name_failover(baserlib.ResourceBase):
1147
  """/2/instances/[instance_name]/failover resource.
1148

1149
  """
1150
  def PUT(self):
1151
    """Does a failover of an instance.
1152

1153
    @return: a job id
1154

1155
    """
1156
    baserlib.CheckType(self.request_body, dict, "Body contents")
1157

    
1158
    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1159
      "instance_name": self.items[0],
1160
      })
1161

    
1162
    return self.SubmitJob([op])
1163

    
1164

    
1165
def _ParseRenameInstanceRequest(name, data):
1166
  """Parses a request for renaming an instance.
1167

1168
  @rtype: L{opcodes.OpInstanceRename}
1169
  @return: Instance rename opcode
1170

1171
  """
1172
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1173
    "instance_name": name,
1174
    })
1175

    
1176

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

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

1184
    @return: a job id
1185

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

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

    
1191
    return self.SubmitJob([op])
1192

    
1193

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

1197
  @rtype: L{opcodes.OpInstanceSetParams}
1198
  @return: Instance modify opcode
1199

1200
  """
1201
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1202
    "instance_name": name,
1203
    })
1204

    
1205

    
1206
class R_2_instances_name_modify(baserlib.ResourceBase):
1207
  """/2/instances/[instance_name]/modify resource.
1208

1209
  """
1210
  def PUT(self):
1211
    """Changes some parameters of an instance.
1212

1213
    @return: a job id
1214

1215
    """
1216
    baserlib.CheckType(self.request_body, dict, "Body contents")
1217

    
1218
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1219

    
1220
    return self.SubmitJob([op])
1221

    
1222

    
1223
class R_2_instances_name_disk_grow(baserlib.ResourceBase):
1224
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1225

1226
  """
1227
  def POST(self):
1228
    """Increases the size of an instance disk.
1229

1230
    @return: a job id
1231

1232
    """
1233
    op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1234
      "instance_name": self.items[0],
1235
      "disk": int(self.items[1]),
1236
      })
1237

    
1238
    return self.SubmitJob([op])
1239

    
1240

    
1241
class R_2_instances_name_console(baserlib.ResourceBase):
1242
  """/2/instances/[instance_name]/console resource.
1243

1244
  """
1245
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1246

    
1247
  def GET(self):
1248
    """Request information for connecting to instance's console.
1249

1250
    @return: Serialized instance console description, see
1251
             L{objects.InstanceConsole}
1252

1253
    """
1254
    client = self.GetClient()
1255

    
1256
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1257

    
1258
    if console is None:
1259
      raise http.HttpServiceUnavailable("Instance console unavailable")
1260

    
1261
    assert isinstance(console, dict)
1262
    return console
1263

    
1264

    
1265
def _GetQueryFields(args):
1266
  """
1267

1268
  """
1269
  try:
1270
    fields = args["fields"]
1271
  except KeyError:
1272
    raise http.HttpBadRequest("Missing 'fields' query argument")
1273

    
1274
  return _SplitQueryFields(fields[0])
1275

    
1276

    
1277
def _SplitQueryFields(fields):
1278
  """
1279

1280
  """
1281
  return [i.strip() for i in fields.split(",")]
1282

    
1283

    
1284
class R_2_query(baserlib.ResourceBase):
1285
  """/2/query/[resource] resource.
1286

1287
  """
1288
  # Results might contain sensitive information
1289
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1290

    
1291
  def _Query(self, fields, filter_):
1292
    return self.GetClient().Query(self.items[0], fields, filter_).ToDict()
1293

    
1294
  def GET(self):
1295
    """Returns resource information.
1296

1297
    @return: Query result, see L{objects.QueryResponse}
1298

1299
    """
1300
    return self._Query(_GetQueryFields(self.queryargs), None)
1301

    
1302
  def PUT(self):
1303
    """Submits job querying for resources.
1304

1305
    @return: Query result, see L{objects.QueryResponse}
1306

1307
    """
1308
    body = self.request_body
1309

    
1310
    baserlib.CheckType(body, dict, "Body contents")
1311

    
1312
    try:
1313
      fields = body["fields"]
1314
    except KeyError:
1315
      fields = _GetQueryFields(self.queryargs)
1316

    
1317
    return self._Query(fields, self.request_body.get("filter", None))
1318

    
1319

    
1320
class R_2_query_fields(baserlib.ResourceBase):
1321
  """/2/query/[resource]/fields resource.
1322

1323
  """
1324
  def GET(self):
1325
    """Retrieves list of available fields for a resource.
1326

1327
    @return: List of serialized L{objects.QueryFieldDefinition}
1328

1329
    """
1330
    try:
1331
      raw_fields = self.queryargs["fields"]
1332
    except KeyError:
1333
      fields = None
1334
    else:
1335
      fields = _SplitQueryFields(raw_fields[0])
1336

    
1337
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1338

    
1339

    
1340
class _R_Tags(baserlib.ResourceBase):
1341
  """ Quasiclass for tagging resources
1342

1343
  Manages tags. When inheriting this class you must define the
1344
  TAG_LEVEL for it.
1345

1346
  """
1347
  TAG_LEVEL = None
1348

    
1349
  def __init__(self, items, queryargs, req):
1350
    """A tag resource constructor.
1351

1352
    We have to override the default to sort out cluster naming case.
1353

1354
    """
1355
    baserlib.ResourceBase.__init__(self, items, queryargs, req)
1356

    
1357
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1358
      self.name = None
1359
    else:
1360
      self.name = items[0]
1361

    
1362
  def GET(self):
1363
    """Returns a list of tags.
1364

1365
    Example: ["tag1", "tag2", "tag3"]
1366

1367
    """
1368
    kind = self.TAG_LEVEL
1369

    
1370
    if kind in (constants.TAG_INSTANCE,
1371
                constants.TAG_NODEGROUP,
1372
                constants.TAG_NODE):
1373
      if not self.name:
1374
        raise http.HttpBadRequest("Missing name on tag request")
1375

    
1376
      cl = self.GetClient()
1377
      if kind == constants.TAG_INSTANCE:
1378
        fn = cl.QueryInstances
1379
      elif kind == constants.TAG_NODEGROUP:
1380
        fn = cl.QueryGroups
1381
      else:
1382
        fn = cl.QueryNodes
1383
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1384
      if not result or not result[0]:
1385
        raise http.HttpBadGateway("Invalid response from tag query")
1386
      tags = result[0][0]
1387

    
1388
    elif kind == constants.TAG_CLUSTER:
1389
      assert not self.name
1390
      # TODO: Use query API?
1391
      ssc = ssconf.SimpleStore()
1392
      tags = ssc.GetClusterTags()
1393

    
1394
    return list(tags)
1395

    
1396
  def PUT(self):
1397
    """Add a set of tags.
1398

1399
    The request as a list of strings should be PUT to this URI. And
1400
    you'll have back a job id.
1401

1402
    """
1403
    # pylint: disable-msg=W0212
1404
    if "tag" not in self.queryargs:
1405
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1406
                                " the 'tag' parameter")
1407
    op = opcodes.OpTagsSet(kind=self.TAG_LEVEL, name=self.name,
1408
                           tags=self.queryargs["tag"], dry_run=self.dryRun())
1409
    return self.SubmitJob([op])
1410

    
1411
  def DELETE(self):
1412
    """Delete a tag.
1413

1414
    In order to delete a set of tags, the DELETE
1415
    request should be addressed to URI like:
1416
    /tags?tag=[tag]&tag=[tag]
1417

1418
    """
1419
    # pylint: disable-msg=W0212
1420
    if "tag" not in self.queryargs:
1421
      # no we not gonna delete all tags
1422
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1423
                                " tag(s) using the 'tag' parameter")
1424
    op = opcodes.OpTagsDel(kind=self.TAG_LEVEL, name=self.name,
1425
                           tags=self.queryargs["tag"], dry_run=self.dryRun())
1426
    return self.SubmitJob([op])
1427

    
1428

    
1429
class R_2_instances_name_tags(_R_Tags):
1430
  """ /2/instances/[instance_name]/tags resource.
1431

1432
  Manages per-instance tags.
1433

1434
  """
1435
  TAG_LEVEL = constants.TAG_INSTANCE
1436

    
1437

    
1438
class R_2_nodes_name_tags(_R_Tags):
1439
  """ /2/nodes/[node_name]/tags resource.
1440

1441
  Manages per-node tags.
1442

1443
  """
1444
  TAG_LEVEL = constants.TAG_NODE
1445

    
1446

    
1447
class R_2_groups_name_tags(_R_Tags):
1448
  """ /2/groups/[group_name]/tags resource.
1449

1450
  Manages per-nodegroup tags.
1451

1452
  """
1453
  TAG_LEVEL = constants.TAG_NODEGROUP
1454

    
1455

    
1456
class R_2_tags(_R_Tags):
1457
  """ /2/tags resource.
1458

1459
  Manages cluster tags.
1460

1461
  """
1462
  TAG_LEVEL = constants.TAG_CLUSTER