rlib2.py 35.4 KB
Newer Older
1
2
3
#
#

4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


22
"""Remote API resource implementations.
23

24
25
PUT or POST?
============
26

27
28
29
According to RFC2616 the main difference between PUT and POST is that
POST can create new resources but PUT can only create the resource the
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
33
entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
new instance) with a name specified in the request.
34

35
Quoting from RFC2616, section 9.6::
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

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

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

52
53
"""

Iustin Pop's avatar
Iustin Pop committed
54
55
56
57
# pylint: disable-msg=C0103

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

Iustin Pop's avatar
Iustin Pop committed
58
from ganeti import opcodes
59
60
from ganeti import http
from ganeti import constants
Iustin Pop's avatar
Iustin Pop committed
61
from ganeti import cli
Michael Hanselmann's avatar
Michael Hanselmann committed
62
from ganeti import rapi
63
from ganeti import ht
64
from ganeti import compat
65
from ganeti import ssconf
Iustin Pop's avatar
Iustin Pop committed
66
from ganeti.rapi import baserlib
67

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
68

Iustin Pop's avatar
Iustin Pop committed
69
_COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
70
71
72
I_FIELDS = ["name", "admin_state", "os",
            "pnode", "snodes",
            "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",
Iustin Pop's avatar
Iustin Pop committed
79
            ] + _COMMON_FIELDS
80

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
151

152
class R_root(baserlib.ResourceBase):
153
154
155
156
157
158
159
160
161
162
163
  """/ resource.

  """
  @staticmethod
  def GET():
    """Supported for legacy reasons.

    """
    return None


164
class R_version(baserlib.ResourceBase):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
165
166
167
168
169
170
  """/version resource.

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

  """
171
172
  @staticmethod
  def GET():
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
173
174
175
176
177
178
    """Returns the remote API version.

    """
    return constants.RAPI_VERSION


179
class R_2_info(baserlib.ResourceBase):
180
  """/2/info resource.
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
181
182

  """
183
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
184
185
186
    """Returns cluster information.

    """
187
    client = self.GetClient()
188
    return client.QueryClusterInfo()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
189
190


191
class R_2_features(baserlib.ResourceBase):
192
193
194
195
196
197
198
199
  """/2/features resource.

  """
  @staticmethod
  def GET():
    """Returns list of optional RAPI features implemented.

    """
200
    return list(ALL_FEATURES)
201
202


203
class R_2_os(baserlib.ResourceBase):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
204
205
206
  """/2/os resource.

  """
207
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
208
209
210
211
212
213
214
    """Return a list of all OSes.

    Can return error 500 in case of a problem.

    Example: ["debian-etch"]

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

    if not isinstance(diagnose_data, list):
Iustin Pop's avatar
Iustin Pop committed
223
      raise http.HttpBadGateway(message="Can't get OS list")
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
224

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

    return os_names
230

231

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

  """
236
  PUT_OPCODE = opcodes.OpClusterRedistConf
237
238


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

  """
243
  PUT_OPCODE = opcodes.OpClusterSetParams
244
245


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

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

Iustin Pop's avatar
Iustin Pop committed
253
    @return: a dictionary with jobs id and uri.
Iustin Pop's avatar
Iustin Pop committed
254

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

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


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

  """
  def GET(self):
    """Returns a job status.

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

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

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
290
291
292
293
294
  def DELETE(self):
    """Cancel not-yet-started job.

    """
    job_id = self.items[0]
295
    result = self.GetClient().CancelJob(job_id)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
296
297
    return result

298

299
class R_2_jobs_id_wait(baserlib.ResourceBase):
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
  """/2/jobs/[job_id]/wait resource.

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

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

    """
    job_id = self.items[0]

    fields = self.getBodyParameter("fields")
    prev_job_info = self.getBodyParameter("previous_job_info", None)
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)

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

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

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

329
    client = self.GetClient()
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
    result = client.WaitForJobChangeOnce(job_id, fields,
                                         prev_job_info, prev_log_serial,
                                         timeout=_WFJC_TIMEOUT)
    if not result:
      raise http.HttpNotFound()

    if result == constants.JOB_NOTCHANGED:
      # No changes
      return None

    (job_info, log_entries) = result

    return {
      "job_info": job_info,
      "log_entries": log_entries,
      }


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

  """
  def GET(self):
    """Returns a list of all nodes.
Iustin Pop's avatar
Iustin Pop committed
354

355
    """
356
    client = self.GetClient()
Iustin Pop's avatar
Iustin Pop committed
357

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


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

  """
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
372
373
374
375
376
  def GET(self):
    """Send information about a node.

    """
    node_name = self.items[0]
377
    client = self.GetClient()
378
379
380
381

    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
                                            names=[node_name], fields=N_FIELDS,
                                            use_locking=self.useLocking())
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
382
383

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


386
class R_2_nodes_name_role(baserlib.ResourceBase):
387
388
389
390
391
392
393
394
395
396
  """ /2/nodes/[node_name]/role resource.

  """
  def GET(self):
    """Returns the current node role.

    @return: Node role

    """
    node_name = self.items[0]
397
    client = self.GetClient()
398
399
400
401
402
403
404
405
406
407
408
    result = client.QueryNodes(names=[node_name], fields=["role"],
                               use_locking=self.useLocking())

    return _NR_MAP[result[0][0]]

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

    @return: a job id

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

    node_name = self.items[0]
413
    role = self.request_body
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434

    if role == _NR_REGULAR:
      candidate = False
      offline = False
      drained = False

    elif role == _NR_MASTER_CANDIATE:
      candidate = True
      offline = drained = None

    elif role == _NR_DRAINED:
      drained = True
      candidate = offline = None

    elif role == _NR_OFFLINE:
      offline = True
      candidate = drained = None

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

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

441
    return self.SubmitJob([op])
442
443


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

  """
448
449
450
  POST_OPCODE = opcodes.OpNodeEvacuate

  def GetPostOpInput(self):
451
    """Evacuate all instances off a node.
452
453

    """
454
    return (self.request_body, {
455
456
457
      "node_name": self.items[0],
      "dry_run": self.dryRun(),
      })
458

459

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

  """
464
465
466
  POST_OPCODE = opcodes.OpNodeMigrate

  def GetPostOpInput(self):
467
468
469
    """Migrate all primary instances from a node.

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

      if "live" in self.queryargs:
        if self._checkIntVariable("live", default=1):
          mode = constants.HT_MIGRATION_LIVE
        else:
          mode = constants.HT_MIGRATION_NONLIVE
481
      else:
482
483
484
485
486
        mode = self._checkStringVariable("mode", default=None)

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

490
491
    return (data, {
      "node_name": self.items[0],
492
      })
493
494


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

  """
499
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]

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

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

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

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


521
class R_2_nodes_name_storage_modify(baserlib.ResourceBase):
522
  """/2/nodes/[node_name]/storage/modify resource.
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543

  """
  def PUT(self):
    node_name = self.items[0]

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

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

    changes = {}

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

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


551
class R_2_nodes_name_storage_repair(baserlib.ResourceBase):
552
  """/2/nodes/[node_name]/storage/repair resource.
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570

  """
  def PUT(self):
    node_name = self.items[0]

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

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

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


574
575
class R_2_groups(baserlib.OpcodeResource):
  """/2/groups resource.
576
577

  """
578
579
  POST_OPCODE = opcodes.OpGroupAdd
  POST_RENAME = {
580
581
582
    "name": "group_name",
    }

583
584
  def GetPostOpInput(self):
    """Create a node group.
585

586
587
588
589
590
    """
    assert not self.items
    return (self.request_body, {
      "dry_run": self.dryRun(),
      })
591
592
593
594
595

  def GET(self):
    """Returns a list of all node groups.

    """
596
    client = self.GetClient()
597
598
599
600
601
602
603
604
605
606
607

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


608
class R_2_groups_name(baserlib.ResourceBase):
609
  """/2/groups/[group_name] resource.
610
611
612
613
614
615
616

  """
  def GET(self):
    """Send information about a node group.

    """
    group_name = self.items[0]
617
    client = self.GetClient()
618
619
620
621
622
623
624

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

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

625
626
627
628
  def DELETE(self):
    """Delete a node group.

    """
629
    op = opcodes.OpGroupRemove(group_name=self.items[0],
630
631
                               dry_run=bool(self.dryRun()))

632
    return self.SubmitJob([op])
633
634


635
class R_2_groups_name_modify(baserlib.OpcodeResource):
636
637
638
  """/2/groups/[group_name]/modify resource.

  """
639
  PUT_OPCODE = opcodes.OpGroupSetParams
640

641
642
  def GetPutOpInput(self):
    """Changes some parameters of node group.
643
644

    """
645
646
647
648
    assert self.items
    return (self.request_body, {
      "group_name": self.items[0],
      })
649
650


651
class R_2_groups_name_rename(baserlib.OpcodeResource):
652
  """/2/groups/[group_name]/rename resource.
653
654

  """
655
  PUT_OPCODE = opcodes.OpGroupRename
656

657
658
  def GetPutOpInput(self):
    """Changes the name of a node group.
659
660

    """
661
662
663
664
665
    assert len(self.items) == 1
    return (self.request_body, {
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      })
666
667


668
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
669
  """/2/groups/[group_name]/assign-nodes resource.
670
671

  """
672
  PUT_OPCODE = opcodes.OpGroupAssignNodes
673

674
675
  def GetPutOpInput(self):
    """Assigns nodes to a group.
676
677

    """
678
679
    assert len(self.items) == 1
    return (self.request_body, {
680
681
682
683
684
685
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      "force": self.useForce(),
      })


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

689
  @rtype: L{opcodes.OpInstanceCreate}
690
691
692
  @return: Instance creation opcode

  """
693
694
695
  override = {
    "dry_run": dry_run,
    }
696

697
698
699
700
701
702
703
  rename = {
    "os": "os_type",
    "name": "instance_name",
    }

  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
                             rename=rename)
704
705


706
class R_2_instances(baserlib.ResourceBase):
707
708
709
710
711
712
713
  """/2/instances resource.

  """
  def GET(self):
    """Returns a list of all available instances.

    """
714
    client = self.GetClient()
715

716
717
718
    use_locking = self.useLocking()
    if self.useBulk():
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
719
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
720
    else:
721
      instancesdata = client.QueryInstances([], ["name"], use_locking)
722
      instanceslist = [row[0] for row in instancesdata]
723
724
725
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
                                   uri_fields=("id", "uri"))

726
727
728
729
730
731
  def POST(self):
    """Create an instance.

    @return: a job id

    """
Luca Bigliardi's avatar
Luca Bigliardi committed
732
    if not isinstance(self.request_body, dict):
733
734
735
736
737
738
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")

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

    if data_version == 0:
739
740
      raise http.HttpBadRequest("Instance creation request version 0 is no"
                                " longer supported")
741
    elif data_version == 1:
742
743
744
745
      data = self.request_body.copy()
      # Remove "__version__"
      data.pop(_REQ_DATA_VERSION, None)
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
746
747
    else:
      raise http.HttpBadRequest("Unsupported request data version %s" %
748
                                data_version)
749

750
    return self.SubmitJob([op])
751

752

753
class R_2_instances_name(baserlib.OpcodeResource):
754
  """/2/instances/[instance_name] resource.
755
756

  """
757
758
  DELETE_OPCODE = opcodes.OpInstanceRemove

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
759
760
761
762
  def GET(self):
    """Send information about an instance.

    """
763
    client = self.GetClient()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
764
    instance_name = self.items[0]
765
766
767
768
769

    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
                                            names=[instance_name],
                                            fields=I_FIELDS,
                                            use_locking=self.useLocking())
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
770
771

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

773
  def GetDeleteOpInput(self):
Iustin Pop's avatar
Iustin Pop committed
774
775
776
    """Delete an instance.

    """
777
778
779
780
781
782
    assert len(self.items) == 1
    return ({}, {
      "instance_name": self.items[0],
      "ignore_failures": False,
      "dry_run": self.dryRun(),
      })
Iustin Pop's avatar
Iustin Pop committed
783

784

785
class R_2_instances_name_info(baserlib.OpcodeResource):
786
787
788
  """/2/instances/[instance_name]/info resource.

  """
789
790
791
  GET_OPCODE = opcodes.OpInstanceQueryData

  def GetGetOpInput(self):
792
793
794
    """Request detailed instance information.

    """
795
796
797
798
799
    assert len(self.items) == 1
    return ({}, {
      "instances": [self.items[0]],
      "static": bool(self._checkIntVariable("static", default=0)),
      })
800
801


802
class R_2_instances_name_reboot(baserlib.OpcodeResource):
803
804
805
806
807
  """/2/instances/[instance_name]/reboot resource.

  Implements an instance reboot.

  """
808
809
810
  POST_OPCODE = opcodes.OpInstanceReboot

  def GetPostOpInput(self):
811
812
    """Reboot an instance.

813
814
815
    The URI takes type=[hard|soft|full] and
    ignore_secondaries=[False|True] parameters.

816
    """
817
818
819
820
821
822
823
    return ({}, {
      "instance_name": self.items[0],
      "reboot_type":
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
      "dry_run": self.dryRun(),
      })
824
825


826
class R_2_instances_name_startup(baserlib.OpcodeResource):
827
828
829
830
831
  """/2/instances/[instance_name]/startup resource.

  Implements an instance startup.

  """
832
833
834
  PUT_OPCODE = opcodes.OpInstanceStartup

  def GetPutOpInput(self):
835
836
    """Startup an instance.

Iustin Pop's avatar
Iustin Pop committed
837
838
    The URI takes force=[False|True] parameter to start the instance
    if even if secondary disks are failing.
839
840

    """
841
842
843
844
845
846
    return ({}, {
      "instance_name": self.items[0],
      "force": self.useForce(),
      "dry_run": self.dryRun(),
      "no_remember": bool(self._checkIntVariable("no_remember")),
      })
847
848


849
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
850
851
852
853
854
  """/2/instances/[instance_name]/shutdown resource.

  Implements an instance shutdown.

  """
855
  PUT_OPCODE = opcodes.OpInstanceShutdown
856

857
858
  def GetPutOpInput(self):
    """Shutdown an instance.
859

860
    """
861
862
863
864
865
    return (self.request_body, {
      "instance_name": self.items[0],
      "no_remember": bool(self._checkIntVariable("no_remember")),
      "dry_run": self.dryRun(),
      })
866
867


868
869
870
871
872
873
874
def _ParseInstanceReinstallRequest(name, data):
  """Parses a request for reinstalling an instance.

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

875
  ostype = baserlib.CheckParameter(data, "os", default=None)
876
877
878
879
880
  start = baserlib.CheckParameter(data, "start", exptype=bool,
                                  default=True)
  osparams = baserlib.CheckParameter(data, "osparams", default=None)

  ops = [
881
    opcodes.OpInstanceShutdown(instance_name=name),
882
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
883
884
885
886
                                osparams=osparams),
    ]

  if start:
887
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
888
889
890
891

  return ops


892
class R_2_instances_name_reinstall(baserlib.ResourceBase):
893
894
895
896
897
898
899
900
901
902
903
904
905
  """/2/instances/[instance_name]/reinstall resource.

  Implements an instance reinstall.

  """
  def POST(self):
    """Reinstall an instance.

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

    """
906
907
908
909
910
    if self.request_body:
      if self.queryargs:
        raise http.HttpBadRequest("Can't combine query and body parameters")

      body = self.request_body
911
    elif self.queryargs:
912
913
914
915
916
      # Legacy interface, do not modify/extend
      body = {
        "os": self._checkStringVariable("os"),
        "start": not self._checkIntVariable("nostartup"),
        }
917
918
    else:
      body = {}
919
920
921

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

922
    return self.SubmitJob(ops)
923
924


925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
def _ParseInstanceReplaceDisksRequest(name, data):
  """Parses a request for an instance export.

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

  """
  override = {
    "instance_name": name,
    }

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

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


952
class R_2_instances_name_replace_disks(baserlib.ResourceBase):
953
954
955
956
957
958
959
  """/2/instances/[instance_name]/replace-disks resource.

  """
  def POST(self):
    """Replaces disks on an instance.

    """
960
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
961

962
    return self.SubmitJob([op])
963
964


965
class R_2_instances_name_activate_disks(baserlib.ResourceBase):
966
967
968
969
970
971
972
973
974
975
  """/2/instances/[instance_name]/activate-disks resource.

  """
  def PUT(self):
    """Activate disks for an instance.

    The URI might contain ignore_size to ignore current recorded size.

    """
    instance_name = self.items[0]
Iustin Pop's avatar
Iustin Pop committed
976
    ignore_size = bool(self._checkIntVariable("ignore_size"))
977

978
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
979
980
                                         ignore_size=ignore_size)

981
    return self.SubmitJob([op])
982
983


984
class R_2_instances_name_deactivate_disks(baserlib.ResourceBase):
985
986
987
988
989
990
991
992
993
  """/2/instances/[instance_name]/deactivate-disks resource.

  """
  def PUT(self):
    """Deactivate disks for an instance.

    """
    instance_name = self.items[0]

994
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
995

996
    return self.SubmitJob([op])
997
998


999
class R_2_instances_name_prepare_export(baserlib.ResourceBase):
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
  """/2/instances/[instance_name]/prepare-export resource.

  """
  def PUT(self):
    """Prepares an export for an instance.

    @return: a job id

    """
    instance_name = self.items[0]
    mode = self._checkStringVariable("mode")

1012
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1013
1014
                                 mode=mode)

1015
    return self.SubmitJob([op])
1016
1017
1018
1019
1020


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

1021
  @rtype: L{opcodes.OpBackupExport}
1022
1023
1024
  @return: Instance export opcode

  """
1025
1026
1027
1028
1029
1030
1031
1032
1033
  # Rename "destination" to "target_node"
  try:
    data["target_node"] = data.pop("destination")
  except KeyError:
    pass

  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
    "instance_name": name,
    })
1034
1035


1036
class R_2_instances_name_export(baserlib.ResourceBase):
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
  """/2/instances/[instance_name]/export resource.

  """
  def PUT(self):
    """Exports an instance.

    @return: a job id

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

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

1051
    return self.SubmitJob([op])
1052
1053


1054
1055
1056
def _ParseMigrateInstanceRequest(name, data):
  """Parses a request for an instance migration.

1057
  @rtype: L{opcodes.OpInstanceMigrate}
1058
1059
1060
  @return: Instance migration opcode

  """
1061
1062
1063
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
    "instance_name": name,
    })
1064
1065


1066
class R_2_instances_name_migrate(baserlib.ResourceBase):
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
  """/2/instances/[instance_name]/migrate resource.

  """
  def PUT(self):
    """Migrates an instance.

    @return: a job id

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

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

1080
    return self.SubmitJob([op])
1081
1082


1083
class R_2_instances_name_failover(baserlib.ResourceBase):
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
  """/2/instances/[instance_name]/failover resource.

  """
  def PUT(self):
    """Does a failover of an instance.

    @return: a job id

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

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

1099
    return self.SubmitJob([op])
1100
1101


1102
1103
1104
def _ParseRenameInstanceRequest(name, data):
  """Parses a request for renaming an instance.

1105
  @rtype: L{opcodes.OpInstanceRename}
1106
1107
1108
  @return: Instance rename opcode

  """
1109
1110
1111
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
    "instance_name": name,
    })
1112
1113


1114
class R_2_instances_name_rename(baserlib.ResourceBase):
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
  """/2/instances/[instance_name]/rename resource.

  """
  def PUT(self):
    """Changes the name of an instance.

    @return: a job id

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

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

1128
    return self.SubmitJob([op])
1129
1130


1131
1132
1133
def _ParseModifyInstanceRequest(name, data):
  """Parses a request for modifying an instance.

1134
  @rtype: L{opcodes.OpInstanceSetParams}
1135
1136
1137
  @return: Instance modify opcode

  """
1138
1139
1140
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
    "instance_name": name,
    })
1141
1142


1143
class R_2_instances_name_modify(baserlib.ResourceBase):
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
  """/2/instances/[instance_name]/modify resource.

  """
  def PUT(self):
    """Changes some parameters of an instance.

    @return: a job id

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

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