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

4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
"""

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

# 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
            "ndparams",
90
            "group.uuid",
Iustin Pop's avatar
Iustin Pop committed
91
            ] + _COMMON_FIELDS
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
92

93
94
95
96
97
G_FIELDS = [
  "alloc_policy",
  "name",
  "node_cnt",
  "node_list",
98
  "ipolicy",
99
100
101
102
103
  "custom_ipolicy",
  "diskparams",
  "custom_diskparams",
  "ndparams",
  "custom_ndparams",
104
  ] + _COMMON_FIELDS
105

106
J_FIELDS_BULK = [
107
  "id", "ops", "status", "summary",
108
  "opstatus",
109
110
111
  "received_ts", "start_ts", "end_ts",
  ]

112
113
114
115
116
J_FIELDS = J_FIELDS_BULK + [
  "oplog",
  "opresult",
  ]

117
_NR_DRAINED = "drained"
118
_NR_MASTER_CANDIDATE = "master-candidate"
119
120
121
122
123
_NR_MASTER = "master"
_NR_OFFLINE = "offline"
_NR_REGULAR = "regular"

_NR_MAP = {
124
  constants.NR_MASTER: _NR_MASTER,
125
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
126
127
128
  constants.NR_DRAINED: _NR_DRAINED,
  constants.NR_OFFLINE: _NR_OFFLINE,
  constants.NR_REGULAR: _NR_REGULAR,
129
130
  }

131
132
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL

133
134
135
# Request data version field
_REQ_DATA_VERSION = "__version__"

136
137
138
# Feature string for instance creation request data version 1
_INST_CREATE_REQV1 = "instance-create-reqv1"

139
140
141
# Feature string for instance reinstall request version 1
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"

142
143
144
# Feature string for node migration version 1
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"

145
146
147
# Feature string for node evacuation with LU-generated jobs
_NODE_EVAC_RES1 = "node-evac-res1"

148
149
150
151
152
153
154
ALL_FEATURES = frozenset([
  _INST_CREATE_REQV1,
  _INST_REINSTALL_REQV1,
  _NODE_MIGRATE_REQV1,
  _NODE_EVAC_RES1,
  ])

155
156
157
# 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
158

159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# FIXME: For compatibility we update the beparams/memory field. Needs to be
#        removed in Ganeti 2.7
def _UpdateBeparams(inst):
  """Updates the beparams dict of inst to support the memory field.

  @param inst: Inst dict
  @return: Updated inst dict

  """
  beparams = inst["beparams"]
  beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]

  return inst


174
class R_root(baserlib.ResourceBase):
175
176
177
178
179
180
181
182
183
184
185
  """/ resource.

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

    """
    return None


186
187
188
189
190
191
class R_2(R_root):
  """/2 resource.

  """


192
class R_version(baserlib.ResourceBase):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
193
194
195
196
197
198
  """/version resource.

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

  """
199
200
  @staticmethod
  def GET():
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
201
202
203
204
205
206
    """Returns the remote API version.

    """
    return constants.RAPI_VERSION


207
class R_2_info(baserlib.OpcodeResource):
208
  """/2/info resource.
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
209
210

  """
211
212
  GET_OPCODE = opcodes.OpClusterQuery

213
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
214
215
216
    """Returns cluster information.

    """
217
    client = self.GetClient(query=True)
218
    return client.QueryClusterInfo()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
219
220


221
class R_2_features(baserlib.ResourceBase):
222
223
224
225
226
227
228
229
  """/2/features resource.

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

    """
230
    return list(ALL_FEATURES)
231
232


233
class R_2_os(baserlib.OpcodeResource):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
234
235
236
  """/2/os resource.

  """
237
238
  GET_OPCODE = opcodes.OpOsDiagnose

239
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
240
241
242
243
244
245
246
    """Return a list of all OSes.

    Can return error 500 in case of a problem.

    Example: ["debian-etch"]

    """
247
    cl = self.GetClient()
248
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
249
    job_id = self.SubmitJob([op], cl=cl)
Iustin Pop's avatar
Iustin Pop committed
250
251
252
    # 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
253
254

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

257
    os_names = []
258
259
    for (name, variants) in diagnose_data:
      os_names.extend(cli.CalculateOSNames(name, variants))
260
261

    return os_names
262

263

264
class R_2_redist_config(baserlib.OpcodeResource):
265
266
267
  """/2/redistribute-config resource.

  """
268
  PUT_OPCODE = opcodes.OpClusterRedistConf
269
270


271
class R_2_cluster_modify(baserlib.OpcodeResource):
272
273
274
  """/2/modify resource.

  """
275
  PUT_OPCODE = opcodes.OpClusterSetParams
276
277


278
class R_2_jobs(baserlib.ResourceBase):
279
280
281
  """/2/jobs resource.

  """
282
  def GET(self):
283
284
    """Returns a dictionary of jobs.

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

287
    """
288
    client = self.GetClient()
289
290

    if self.useBulk():
291
292
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
293
294
295
296
    else:
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
                                   uri_fields=("id", "uri"))
297
298


299
class R_2_jobs_id(baserlib.ResourceBase):
300
301
302
303
304
305
  """/2/jobs/[job_id] resource.

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

Iustin Pop's avatar
Iustin Pop committed
306
307
308
309
310
311
312
313
    @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
314

315
316
    """
    job_id = self.items[0]
317
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
Iustin Pop's avatar
Iustin Pop committed
318
319
    if result is None:
      raise http.HttpNotFound()
320
    return baserlib.MapFields(J_FIELDS, result)
321

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
322
323
324
325
326
  def DELETE(self):
    """Cancel not-yet-started job.

    """
    job_id = self.items[0]
327
    result = self.GetClient().CancelJob(job_id)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
328
329
    return result

330

331
class R_2_jobs_id_wait(baserlib.ResourceBase):
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
  """/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")

361
    client = self.GetClient()
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
    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,
      }


380
class R_2_nodes(baserlib.OpcodeResource):
381
382
383
  """/2/nodes resource.

  """
384
385
  GET_OPCODE = opcodes.OpNodeQuery

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

389
    """
390
    client = self.GetClient()
Iustin Pop's avatar
Iustin Pop committed
391

392
    if self.useBulk():
393
      bulkdata = client.QueryNodes([], N_FIELDS, False)
394
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
395
396
397
398
399
    else:
      nodesdata = client.QueryNodes([], ["name"], False)
      nodeslist = [row[0] for row in nodesdata]
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
                                   uri_fields=("id", "uri"))
400
401


402
class R_2_nodes_name(baserlib.OpcodeResource):
403
  """/2/nodes/[node_name] resource.
404
405

  """
406
407
  GET_OPCODE = opcodes.OpNodeQuery

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
408
409
410
411
412
  def GET(self):
    """Send information about a node.

    """
    node_name = self.items[0]
413
    client = self.GetClient()
414
415
416
417

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

    return baserlib.MapFields(N_FIELDS, result[0])
420
421


422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
  """/2/nodes/[node_name]/powercycle resource.

  """
  POST_OPCODE = opcodes.OpNodePowercycle

  def GetPostOpInput(self):
    """Tries to powercycle a node.

    """
    return (self.request_body, {
      "node_name": self.items[0],
      "force": self.useForce(),
      })


438
439
class R_2_nodes_name_role(baserlib.OpcodeResource):
  """/2/nodes/[node_name]/role resource.
440
441

  """
442
443
  PUT_OPCODE = opcodes.OpNodeSetParams

444
445
446
447
448
449
450
  def GET(self):
    """Returns the current node role.

    @return: Node role

    """
    node_name = self.items[0]
451
    client = self.GetClient()
452
453
454
455
456
    result = client.QueryNodes(names=[node_name], fields=["role"],
                               use_locking=self.useLocking())

    return _NR_MAP[result[0][0]]

457
  def GetPutOpInput(self):
458
459
460
    """Sets the node role.

    """
461
    baserlib.CheckType(self.request_body, basestring, "Body contents")
462

463
    role = self.request_body
464
465
466
467
468
469

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

470
    elif role == _NR_MASTER_CANDIDATE:
471
472
473
474
475
476
477
478
479
480
481
482
483
484
      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)

485
    assert len(self.items) == 1
486

487
488
489
490
491
492
    return ({}, {
      "node_name": self.items[0],
      "master_candidate": candidate,
      "offline": offline,
      "drained": drained,
      "force": self.useForce(),
Guido Trotter's avatar
Guido Trotter committed
493
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
494
      })
495
496


497
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
498
499
500
  """/2/nodes/[node_name]/evacuate resource.

  """
501
502
503
  POST_OPCODE = opcodes.OpNodeEvacuate

  def GetPostOpInput(self):
504
    """Evacuate all instances off a node.
505
506

    """
507
    return (self.request_body, {
508
509
510
      "node_name": self.items[0],
      "dry_run": self.dryRun(),
      })
511

512

513
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
514
  """/2/nodes/[node_name]/migrate resource.
515
516

  """
517
518
519
  POST_OPCODE = opcodes.OpNodeMigrate

  def GetPostOpInput(self):
520
521
522
    """Migrate all primary instances from a node.

    """
523
524
525
526
527
528
529
530
531
532
533
    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
534
      else:
535
536
537
538
539
        mode = self._checkStringVariable("mode", default=None)

      data = {
        "mode": mode,
        }
540
    else:
541
      data = self.request_body
542

543
544
    return (data, {
      "node_name": self.items[0],
545
      })
546
547


Guido Trotter's avatar
Guido Trotter committed
548
class R_2_nodes_name_modify(baserlib.OpcodeResource):
549
550
551
  """/2/nodes/[node_name]/modify resource.

  """
Guido Trotter's avatar
Guido Trotter committed
552
  POST_OPCODE = opcodes.OpNodeSetParams
553

Guido Trotter's avatar
Guido Trotter committed
554
  def GetPostOpInput(self):
Guido Trotter's avatar
Guido Trotter committed
555
    """Changes parameters of a node.
556
557

    """
Guido Trotter's avatar
Guido Trotter committed
558
    assert len(self.items) == 1
559

Guido Trotter's avatar
Guido Trotter committed
560
    return (self.request_body, {
561
      "node_name": self.items[0],
562
563
564
      })


565
class R_2_nodes_name_storage(baserlib.OpcodeResource):
566
  """/2/nodes/[node_name]/storage resource.
567
568

  """
569
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
570
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
571
  GET_OPCODE = opcodes.OpNodeQueryStorage
572

573
574
  def GetGetOpInput(self):
    """List storage available on a node.
575

576
    """
577
578
    storage_type = self._checkStringVariable("storage_type", None)
    output_fields = self._checkStringVariable("output_fields", None)
579

580
581
582
583
    if not output_fields:
      raise http.HttpBadRequest("Missing the required 'output_fields'"
                                " parameter")

584
585
586
587
588
    return ({}, {
      "nodes": [self.items[0]],
      "storage_type": storage_type,
      "output_fields": output_fields.split(","),
      })
589
590


591
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
592
  """/2/nodes/[node_name]/storage/modify resource.
593
594

  """
595
  PUT_OPCODE = opcodes.OpNodeModifyStorage
596

597
598
  def GetPutOpInput(self):
    """Modifies a storage volume on a node.
599

600
601
    """
    storage_type = self._checkStringVariable("storage_type", None)
602
    name = self._checkStringVariable("name", None)
603

604
605
606
607
608
609
610
611
612
613
    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))

614
615
616
617
618
619
    return ({}, {
      "node_name": self.items[0],
      "storage_type": storage_type,
      "name": name,
      "changes": changes,
      })
620
621


622
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
623
  """/2/nodes/[node_name]/storage/repair resource.
624
625

  """
626
  PUT_OPCODE = opcodes.OpRepairNodeStorage
627

628
629
  def GetPutOpInput(self):
    """Repairs a storage volume on a node.
630

631
632
    """
    storage_type = self._checkStringVariable("storage_type", None)
633
634
635
636
637
    name = self._checkStringVariable("name", None)
    if not name:
      raise http.HttpBadRequest("Missing the required 'name'"
                                " parameter")

638
639
640
641
642
    return ({}, {
      "node_name": self.items[0],
      "storage_type": storage_type,
      "name": name,
      })
643
644


645
646
class R_2_groups(baserlib.OpcodeResource):
  """/2/groups resource.
647
648

  """
649
  GET_OPCODE = opcodes.OpGroupQuery
650
651
  POST_OPCODE = opcodes.OpGroupAdd
  POST_RENAME = {
652
653
654
    "name": "group_name",
    }

655
656
  def GetPostOpInput(self):
    """Create a node group.
657

658
659
660
661
662
    """
    assert not self.items
    return (self.request_body, {
      "dry_run": self.dryRun(),
      })
663
664
665
666
667

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

    """
668
    client = self.GetClient()
669
670
671
672
673
674
675
676
677
678
679

    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"))


680
class R_2_groups_name(baserlib.OpcodeResource):
681
  """/2/groups/[group_name] resource.
682
683

  """
684
685
  DELETE_OPCODE = opcodes.OpGroupRemove

686
687
688
689
690
  def GET(self):
    """Send information about a node group.

    """
    group_name = self.items[0]
691
    client = self.GetClient()
692
693
694
695
696
697
698

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

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

699
  def GetDeleteOpInput(self):
700
701
702
    """Delete a node group.

    """
703
704
705
706
707
    assert len(self.items) == 1
    return ({}, {
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      })
708
709


710
class R_2_groups_name_modify(baserlib.OpcodeResource):
711
712
713
  """/2/groups/[group_name]/modify resource.

  """
714
  PUT_OPCODE = opcodes.OpGroupSetParams
715

716
717
  def GetPutOpInput(self):
    """Changes some parameters of node group.
718
719

    """
720
721
722
723
    assert self.items
    return (self.request_body, {
      "group_name": self.items[0],
      })
724
725


726
class R_2_groups_name_rename(baserlib.OpcodeResource):
727
  """/2/groups/[group_name]/rename resource.
728
729

  """
730
  PUT_OPCODE = opcodes.OpGroupRename
731

732
733
  def GetPutOpInput(self):
    """Changes the name of a node group.
734
735

    """
736
737
738
739
740
    assert len(self.items) == 1
    return (self.request_body, {
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      })
741
742


743
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
744
  """/2/groups/[group_name]/assign-nodes resource.
745
746

  """
747
  PUT_OPCODE = opcodes.OpGroupAssignNodes
748

749
750
  def GetPutOpInput(self):
    """Assigns nodes to a group.
751
752

    """
753
754
    assert len(self.items) == 1
    return (self.request_body, {
755
756
757
758
759
760
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      "force": self.useForce(),
      })


761
762
class R_2_instances(baserlib.OpcodeResource):
  """/2/instances resource.
763
764

  """
765
  GET_OPCODE = opcodes.OpInstanceQuery
766
767
  POST_OPCODE = opcodes.OpInstanceCreate
  POST_RENAME = {
768
769
770
771
    "os": "os_type",
    "name": "instance_name",
    }

772
773
774
775
  def GET(self):
    """Returns a list of all available instances.

    """
776
    client = self.GetClient()
777

778
779
780
    use_locking = self.useLocking()
    if self.useBulk():
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
781
      return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
782
    else:
783
      instancesdata = client.QueryInstances([], ["name"], use_locking)
784
      instanceslist = [row[0] for row in instancesdata]
785
786
787
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
                                   uri_fields=("id", "uri"))

788
  def GetPostOpInput(self):
789
790
791
792
793
    """Create an instance.

    @return: a job id

    """
794
    baserlib.CheckType(self.request_body, dict, "Body contents")
795
796
797
798
799

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

    if data_version == 0:
800
801
      raise http.HttpBadRequest("Instance creation request version 0 is no"
                                " longer supported")
802
    elif data_version != 1:
803
      raise http.HttpBadRequest("Unsupported request data version %s" %
804
                                data_version)
805

806
807
808
809
810
811
812
    data = self.request_body.copy()
    # Remove "__version__"
    data.pop(_REQ_DATA_VERSION, None)

    return (data, {
      "dry_run": self.dryRun(),
      })
813

814

815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
class R_2_instances_multi_alloc(baserlib.OpcodeResource):
  """/2/instances-multi-alloc resource.

  """
  POST_OPCODE = opcodes.OpInstanceMultiAlloc

  def GetPostOpInput(self):
    """Try to allocate multiple instances.

    @return: A dict with submitted jobs, allocatable instances and failed
             allocations

    """
    return (self.request_body, {
      "dry_run": self.dryRun(),
      })


833
class R_2_instances_name(baserlib.OpcodeResource):
834
  """/2/instances/[instance_name] resource.
835
836

  """
837
  GET_OPCODE = opcodes.OpInstanceQuery
838
839
  DELETE_OPCODE = opcodes.OpInstanceRemove

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
840
841
842
843
  def GET(self):
    """Send information about an instance.

    """
844
    client = self.GetClient()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
845
    instance_name = self.items[0]
846
847
848
849
850

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

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

854
  def GetDeleteOpInput(self):
Iustin Pop's avatar
Iustin Pop committed
855
856
857
    """Delete an instance.

    """
858
859
860
861
862
863
    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
864

865

866
class R_2_instances_name_info(baserlib.OpcodeResource):
867
868
869
  """/2/instances/[instance_name]/info resource.

  """
870
871
872
  GET_OPCODE = opcodes.OpInstanceQueryData

  def GetGetOpInput(self):
873
874
875
    """Request detailed instance information.

    """
876
877
878
879
880
    assert len(self.items) == 1
    return ({}, {
      "instances": [self.items[0]],
      "static": bool(self._checkIntVariable("static", default=0)),
      })
881
882


883
class R_2_instances_name_reboot(baserlib.OpcodeResource):
884
885
886
887
888
  """/2/instances/[instance_name]/reboot resource.

  Implements an instance reboot.

  """
889
890
891
  POST_OPCODE = opcodes.OpInstanceReboot

  def GetPostOpInput(self):
892
893
    """Reboot an instance.

894
895
896
    The URI takes type=[hard|soft|full] and
    ignore_secondaries=[False|True] parameters.

897
    """
898
899
900
901
902
903
904
    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(),
      })
905
906


907
class R_2_instances_name_startup(baserlib.OpcodeResource):
908
909
910
911
912
  """/2/instances/[instance_name]/startup resource.

  Implements an instance startup.

  """
913
914
915
  PUT_OPCODE = opcodes.OpInstanceStartup

  def GetPutOpInput(self):
916
917
    """Startup an instance.

Iustin Pop's avatar
Iustin Pop committed
918
919
    The URI takes force=[False|True] parameter to start the instance
    if even if secondary disks are failing.
920
921

    """
922
923
924
925
926
927
    return ({}, {
      "instance_name": self.items[0],
      "force": self.useForce(),
      "dry_run": self.dryRun(),
      "no_remember": bool(self._checkIntVariable("no_remember")),
      })
928
929


930
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
931
932
933
934
935
  """/2/instances/[instance_name]/shutdown resource.

  Implements an instance shutdown.

  """
936
  PUT_OPCODE = opcodes.OpInstanceShutdown
937

938
939
  def GetPutOpInput(self):
    """Shutdown an instance.
940

941
    """
942
943
944
945
946
    return (self.request_body, {
      "instance_name": self.items[0],
      "no_remember": bool(self._checkIntVariable("no_remember")),
      "dry_run": self.dryRun(),
      })
947
948


949
950
951
952
953
954
955
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")

956
  ostype = baserlib.CheckParameter(data, "os", default=None)
957
958
959
960
961
  start = baserlib.CheckParameter(data, "start", exptype=bool,
                                  default=True)
  osparams = baserlib.CheckParameter(data, "osparams", default=None)

  ops = [
962
    opcodes.OpInstanceShutdown(instance_name=name),
963
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
964
965
966
967
                                osparams=osparams),
    ]

  if start:
968
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
969
970
971
972

  return ops


973
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
974
975
976
977
978
  """/2/instances/[instance_name]/reinstall resource.

  Implements an instance reinstall.

  """
979
980
  POST_OPCODE = opcodes.OpInstanceReinstall

981
982
983
984
985
986
987
988
  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.

    """
989
990
991
992
993
    if self.request_body:
      if self.queryargs:
        raise http.HttpBadRequest("Can't combine query and body parameters")

      body = self.request_body
994
    elif self.queryargs:
995
996
997
998
999
      # Legacy interface, do not modify/extend
      body = {
        "os": self._checkStringVariable("os"),
        "start": not self._checkIntVariable("nostartup"),
        }
1000
1001
    else:
      body = {}
1002
1003
1004

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

1005
    return self.SubmitJob(ops)
1006
1007


1008
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1009
1010
1011
  """/2/instances/[instance_name]/replace-disks resource.

  """
1012
1013
1014
  POST_OPCODE = opcodes.OpInstanceReplaceDisks

  def GetPostOpInput(self):
1015
1016
1017
    """Replaces disks on an instance.

    """
1018
1019
1020
    static = {
      "instance_name": self.items[0],
      }
1021

1022
    if self.request_body:
Michael Hanselmann's avatar
Michael Hanselmann committed
1023
      data = self.request_body
1024
1025
    elif self.queryargs:
      # Legacy interface, do not modify/extend
Michael Hanselmann's avatar
Michael Hanselmann committed
1026
      data = {
1027
1028
1029
1030
1031
1032
        "remote_node": self._checkStringVariable("remote_node", default=None),
        "mode": self._checkStringVariable("mode", default=None),
        "disks": self._checkStringVariable("disks", default=None),
        "iallocator": self._checkStringVariable("iallocator", default=None),
        }
    else:
Michael Hanselmann's avatar
Michael Hanselmann committed
1033
      data = {}
1034

1035
1036
    # Parse disks
    try:
Michael Hanselmann's avatar
Michael Hanselmann committed
1037
      raw_disks = data.pop("disks")
1038
1039
1040
    except KeyError:
      pass
    else:
Michael Hanselmann's avatar
Michael Hanselmann committed
1041
1042
1043
1044
1045
1046
1047
1048
1049
      if raw_disks:
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
          data["disks"] = raw_disks
        else:
          # 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" % err)
1050
1051

    return (data, static)
1052
1053


1054
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1055
1056
1057
  """/2/instances/[instance_name]/activate-disks resource.

  """
1058
1059
1060
  PUT_OPCODE = opcodes.OpInstanceActivateDisks

  def GetPutOpInput(self):
1061
1062
1063
1064
1065
    """Activate disks for an instance.

    The URI might contain ignore_size to ignore current recorded size.

    """
1066
1067