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

4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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
from ganeti import objects
60
61
from ganeti import http
from ganeti import constants
Iustin Pop's avatar
Iustin Pop committed
62
from ganeti import cli
Michael Hanselmann's avatar
Michael Hanselmann committed
63
from ganeti import rapi
64
from ganeti import ht
65
from ganeti import compat
66
from ganeti import ssconf
Iustin Pop's avatar
Iustin Pop committed
67
from ganeti.rapi import baserlib
68

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
69

Iustin Pop's avatar
Iustin Pop committed
70
_COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
71
72
73
I_FIELDS = ["name", "admin_state", "os",
            "pnode", "snodes",
            "disk_template",
74
            "nic.ips", "nic.macs", "nic.modes", "nic.uuids", "nic.names",
75
            "nic.links", "nic.networks", "nic.networks.names", "nic.bridges",
76
            "network_port",
77
78
            "disk.sizes", "disk.spindles", "disk_usage", "disk.uuids",
            "disk.names",
79
            "beparams", "hvparams",
80
            "oper_state", "oper_ram", "oper_vcpus", "status",
81
            "custom_hvparams", "custom_beparams", "custom_nicparams",
Iustin Pop's avatar
Iustin Pop committed
82
            ] + _COMMON_FIELDS
83

84
N_FIELDS = ["name", "offline", "master_candidate", "drained",
Bernardo Dal Seno's avatar
Bernardo Dal Seno committed
85
            "dtotal", "dfree", "sptotal", "spfree",
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
86
            "mtotal", "mnode", "mfree",
Iustin Pop's avatar
Iustin Pop committed
87
            "pinst_cnt", "sinst_cnt",
88
            "ctotal", "cnos", "cnodes", "csockets",
Iustin Pop's avatar
Iustin Pop committed
89
            "pip", "sip", "role",
90
            "pinst_list", "sinst_list",
91
            "master_capable", "vm_capable",
92
            "ndparams",
93
            "group.uuid",
Iustin Pop's avatar
Iustin Pop committed
94
            ] + _COMMON_FIELDS
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
95

96
97
NET_FIELDS = ["name", "network", "gateway",
              "network6", "gateway6",
98
              "mac_prefix",
99
100
              "free_count", "reserved_count",
              "map", "group_list", "inst_list",
101
102
              "external_reservations",
              ] + _COMMON_FIELDS
103

104
105
106
107
108
G_FIELDS = [
  "alloc_policy",
  "name",
  "node_cnt",
  "node_list",
109
  "ipolicy",
110
111
112
113
114
  "custom_ipolicy",
  "diskparams",
  "custom_diskparams",
  "ndparams",
  "custom_ndparams",
115
  ] + _COMMON_FIELDS
116

117
J_FIELDS_BULK = [
118
  "id", "ops", "status", "summary",
119
  "opstatus",
120
121
122
  "received_ts", "start_ts", "end_ts",
  ]

123
124
125
126
127
J_FIELDS = J_FIELDS_BULK + [
  "oplog",
  "opresult",
  ]

128
_NR_DRAINED = "drained"
129
_NR_MASTER_CANDIDATE = "master-candidate"
130
131
132
133
134
_NR_MASTER = "master"
_NR_OFFLINE = "offline"
_NR_REGULAR = "regular"

_NR_MAP = {
135
  constants.NR_MASTER: _NR_MASTER,
136
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
137
138
139
  constants.NR_DRAINED: _NR_DRAINED,
  constants.NR_OFFLINE: _NR_OFFLINE,
  constants.NR_REGULAR: _NR_REGULAR,
140
141
  }

142
143
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL

144
145
146
# Request data version field
_REQ_DATA_VERSION = "__version__"

147
148
149
# Feature string for instance creation request data version 1
_INST_CREATE_REQV1 = "instance-create-reqv1"

150
151
152
# Feature string for instance reinstall request version 1
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"

153
154
155
# Feature string for node migration version 1
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"

156
157
158
# Feature string for node evacuation with LU-generated jobs
_NODE_EVAC_RES1 = "node-evac-res1"

159
ALL_FEATURES = compat.UniqueFrozenset([
160
161
162
163
164
165
  _INST_CREATE_REQV1,
  _INST_REINSTALL_REQV1,
  _NODE_MIGRATE_REQV1,
  _NODE_EVAC_RES1,
  ])

166
167
168
# 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
169

170
# FIXME: For compatibility we update the beparams/memory field. Needs to be
171
#        removed in Ganeti 2.8
172
173
174
175
176
177
178
179
180
181
182
183
184
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


185
class R_root(baserlib.ResourceBase):
186
187
188
189
190
191
192
193
194
195
196
  """/ resource.

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

    """
    return None


197
198
199
200
201
202
class R_2(R_root):
  """/2 resource.

  """


203
class R_version(baserlib.ResourceBase):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
204
205
206
207
208
209
  """/version resource.

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

  """
210
211
  @staticmethod
  def GET():
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
212
213
214
215
216
217
    """Returns the remote API version.

    """
    return constants.RAPI_VERSION


218
class R_2_info(baserlib.OpcodeResource):
219
  """/2/info resource.
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
220
221

  """
222
  GET_OPCODE = opcodes.OpClusterQuery
223
224
225
226
  GET_ALIASES = {
    "volume_group_name": "vg_name",
    "drbd_usermode_helper": "drbd_helper",
    }
227

228
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
229
230
231
    """Returns cluster information.

    """
232
    client = self.GetClient(query=True)
233
    return client.QueryClusterInfo()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
234
235


236
class R_2_features(baserlib.ResourceBase):
237
238
239
240
241
242
243
244
  """/2/features resource.

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

    """
245
    return list(ALL_FEATURES)
246
247


248
class R_2_os(baserlib.OpcodeResource):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
249
250
251
  """/2/os resource.

  """
252
253
  GET_OPCODE = opcodes.OpOsDiagnose

254
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
255
256
257
258
259
260
261
    """Return a list of all OSes.

    Can return error 500 in case of a problem.

    Example: ["debian-etch"]

    """
262
    cl = self.GetClient()
263
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
264
    job_id = self.SubmitJob([op], cl=cl)
Iustin Pop's avatar
Iustin Pop committed
265
266
267
    # 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
268
269

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

272
    os_names = []
273
274
    for (name, variants) in diagnose_data:
      os_names.extend(cli.CalculateOSNames(name, variants))
275
276

    return os_names
277

278

279
class R_2_redist_config(baserlib.OpcodeResource):
280
281
282
  """/2/redistribute-config resource.

  """
283
  PUT_OPCODE = opcodes.OpClusterRedistConf
284
285


286
class R_2_cluster_modify(baserlib.OpcodeResource):
287
288
289
  """/2/modify resource.

  """
290
  PUT_OPCODE = opcodes.OpClusterSetParams
291
292


293
class R_2_jobs(baserlib.ResourceBase):
294
295
296
  """/2/jobs resource.

  """
297
  def GET(self):
298
299
    """Returns a dictionary of jobs.

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

302
    """
303
    client = self.GetClient(query=True)
304
305

    if self.useBulk():
306
307
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
308
309
310
311
    else:
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
                                   uri_fields=("id", "uri"))
312
313


314
class R_2_jobs_id(baserlib.ResourceBase):
315
316
317
318
319
320
  """/2/jobs/[job_id] resource.

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

Iustin Pop's avatar
Iustin Pop committed
321
322
323
324
325
326
327
328
    @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
329

330
331
    """
    job_id = self.items[0]
332
    result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
Iustin Pop's avatar
Iustin Pop committed
333
334
    if result is None:
      raise http.HttpNotFound()
335
    return baserlib.MapFields(J_FIELDS, result)
336

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
337
338
339
340
341
  def DELETE(self):
    """Cancel not-yet-started job.

    """
    job_id = self.items[0]
342
    result = self.GetClient().CancelJob(job_id)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
343
344
    return result

345

346
class R_2_jobs_id_wait(baserlib.ResourceBase):
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
  """/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")

376
    client = self.GetClient()
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
    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,
      }


395
class R_2_nodes(baserlib.OpcodeResource):
396
397
398
  """/2/nodes resource.

  """
399
400
  GET_OPCODE = opcodes.OpNodeQuery

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

404
    """
405
    client = self.GetClient(query=True)
Iustin Pop's avatar
Iustin Pop committed
406

407
    if self.useBulk():
408
      bulkdata = client.QueryNodes([], N_FIELDS, False)
409
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
410
411
412
413
414
    else:
      nodesdata = client.QueryNodes([], ["name"], False)
      nodeslist = [row[0] for row in nodesdata]
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
                                   uri_fields=("id", "uri"))
415
416


417
class R_2_nodes_name(baserlib.OpcodeResource):
418
  """/2/nodes/[node_name] resource.
419
420

  """
421
  GET_OPCODE = opcodes.OpNodeQuery
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
422
423
424
  GET_ALIASES = {
    "sip": "secondary_ip",
    }
425

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
426
427
428
429
430
  def GET(self):
    """Send information about a node.

    """
    node_name = self.items[0]
431
    client = self.GetClient(query=True)
432
433
434
435

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

    return baserlib.MapFields(N_FIELDS, result[0])
438
439


440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
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(),
      })


456
457
class R_2_nodes_name_role(baserlib.OpcodeResource):
  """/2/nodes/[node_name]/role resource.
458
459

  """
460
461
  PUT_OPCODE = opcodes.OpNodeSetParams

462
463
464
465
466
467
468
  def GET(self):
    """Returns the current node role.

    @return: Node role

    """
    node_name = self.items[0]
469
    client = self.GetClient(query=True)
470
471
472
473
474
    result = client.QueryNodes(names=[node_name], fields=["role"],
                               use_locking=self.useLocking())

    return _NR_MAP[result[0][0]]

475
  def GetPutOpInput(self):
476
477
478
    """Sets the node role.

    """
479
    baserlib.CheckType(self.request_body, basestring, "Body contents")
480

481
    role = self.request_body
482
483
484
485
486
487

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

488
    elif role == _NR_MASTER_CANDIDATE:
489
490
491
492
493
494
495
496
497
498
499
500
501
502
      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)

503
    assert len(self.items) == 1
504

505
506
507
508
509
510
    return ({}, {
      "node_name": self.items[0],
      "master_candidate": candidate,
      "offline": offline,
      "drained": drained,
      "force": self.useForce(),
Guido Trotter's avatar
Guido Trotter committed
511
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
512
      })
513
514


515
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
516
517
518
  """/2/nodes/[node_name]/evacuate resource.

  """
519
520
521
  POST_OPCODE = opcodes.OpNodeEvacuate

  def GetPostOpInput(self):
522
    """Evacuate all instances off a node.
523
524

    """
525
    return (self.request_body, {
526
527
528
      "node_name": self.items[0],
      "dry_run": self.dryRun(),
      })
529

530

531
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
532
  """/2/nodes/[node_name]/migrate resource.
533
534

  """
535
536
537
  POST_OPCODE = opcodes.OpNodeMigrate

  def GetPostOpInput(self):
538
539
540
    """Migrate all primary instances from a node.

    """
541
542
543
544
545
546
547
548
549
550
551
    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
552
      else:
553
554
555
556
557
        mode = self._checkStringVariable("mode", default=None)

      data = {
        "mode": mode,
        }
558
    else:
559
      data = self.request_body
560

561
562
    return (data, {
      "node_name": self.items[0],
563
      })
564
565


Guido Trotter's avatar
Guido Trotter committed
566
class R_2_nodes_name_modify(baserlib.OpcodeResource):
567
568
569
  """/2/nodes/[node_name]/modify resource.

  """
Guido Trotter's avatar
Guido Trotter committed
570
  POST_OPCODE = opcodes.OpNodeSetParams
571

Guido Trotter's avatar
Guido Trotter committed
572
  def GetPostOpInput(self):
Guido Trotter's avatar
Guido Trotter committed
573
    """Changes parameters of a node.
574
575

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

Guido Trotter's avatar
Guido Trotter committed
578
    return (self.request_body, {
579
      "node_name": self.items[0],
580
581
582
      })


583
class R_2_nodes_name_storage(baserlib.OpcodeResource):
584
  """/2/nodes/[node_name]/storage resource.
585
586

  """
587
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
588
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
589
  GET_OPCODE = opcodes.OpNodeQueryStorage
590

591
592
  def GetGetOpInput(self):
    """List storage available on a node.
593

594
    """
595
596
    storage_type = self._checkStringVariable("storage_type", None)
    output_fields = self._checkStringVariable("output_fields", None)
597

598
599
600
601
    if not output_fields:
      raise http.HttpBadRequest("Missing the required 'output_fields'"
                                " parameter")

602
603
604
605
606
    return ({}, {
      "nodes": [self.items[0]],
      "storage_type": storage_type,
      "output_fields": output_fields.split(","),
      })
607
608


609
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
610
  """/2/nodes/[node_name]/storage/modify resource.
611
612

  """
613
  PUT_OPCODE = opcodes.OpNodeModifyStorage
614

615
616
  def GetPutOpInput(self):
    """Modifies a storage volume on a node.
617

618
619
    """
    storage_type = self._checkStringVariable("storage_type", None)
620
    name = self._checkStringVariable("name", None)
621

622
623
624
625
626
627
628
629
630
631
    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))

632
633
634
635
636
637
    return ({}, {
      "node_name": self.items[0],
      "storage_type": storage_type,
      "name": name,
      "changes": changes,
      })
638
639


640
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
641
  """/2/nodes/[node_name]/storage/repair resource.
642
643

  """
644
  PUT_OPCODE = opcodes.OpRepairNodeStorage
645

646
647
  def GetPutOpInput(self):
    """Repairs a storage volume on a node.
648

649
650
    """
    storage_type = self._checkStringVariable("storage_type", None)
651
652
653
654
655
    name = self._checkStringVariable("name", None)
    if not name:
      raise http.HttpBadRequest("Missing the required 'name'"
                                " parameter")

656
657
658
659
660
    return ({}, {
      "node_name": self.items[0],
      "storage_type": storage_type,
      "name": name,
      })
661
662


663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
class R_2_networks(baserlib.OpcodeResource):
  """/2/networks resource.

  """
  GET_OPCODE = opcodes.OpNetworkQuery
  POST_OPCODE = opcodes.OpNetworkAdd
  POST_RENAME = {
    "name": "network_name",
    }

  def GetPostOpInput(self):
    """Create a network.

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

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

    """
686
    client = self.GetClient(query=True)
687
688
689
690
691
692
693
694
695
696
697
698

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


class R_2_networks_name(baserlib.OpcodeResource):
699
  """/2/networks/[network_name] resource.
700
701
702
703
704
705
706
707
708

  """
  DELETE_OPCODE = opcodes.OpNetworkRemove

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

    """
    network_name = self.items[0]
709
    client = self.GetClient(query=True)
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727

    result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
                                            names=[network_name],
                                            fields=NET_FIELDS,
                                            use_locking=self.useLocking())

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

  def GetDeleteOpInput(self):
    """Delete a network.

    """
    assert len(self.items) == 1
    return (self.request_body, {
      "network_name": self.items[0],
      "dry_run": self.dryRun(),
      })

728

729
class R_2_networks_name_connect(baserlib.OpcodeResource):
730
  """/2/networks/[network_name]/connect resource.
731
732
733
734
735
736
737
738
739
740
741

  """
  PUT_OPCODE = opcodes.OpNetworkConnect

  def GetPutOpInput(self):
    """Changes some parameters of node group.

    """
    assert self.items
    return (self.request_body, {
      "network_name": self.items[0],
742
      "dry_run": self.dryRun(),
743
744
      })

745

746
class R_2_networks_name_disconnect(baserlib.OpcodeResource):
747
  """/2/networks/[network_name]/disconnect resource.
748
749
750
751
752
753
754

  """
  PUT_OPCODE = opcodes.OpNetworkDisconnect

  def GetPutOpInput(self):
    """Changes some parameters of node group.

755
756
757
758
759
760
761
    """
    assert self.items
    return (self.request_body, {
      "network_name": self.items[0],
      "dry_run": self.dryRun(),
      })

762

763
764
765
766
767
768
769
770
771
class R_2_networks_name_modify(baserlib.OpcodeResource):
  """/2/networks/[network_name]/modify resource.

  """
  PUT_OPCODE = opcodes.OpNetworkSetParams

  def GetPutOpInput(self):
    """Changes some parameters of network.

772
773
774
775
776
777
    """
    assert self.items
    return (self.request_body, {
      "network_name": self.items[0],
      })

778

779
780
class R_2_groups(baserlib.OpcodeResource):
  """/2/groups resource.
781
782

  """
783
  GET_OPCODE = opcodes.OpGroupQuery
784
785
  POST_OPCODE = opcodes.OpGroupAdd
  POST_RENAME = {
786
787
788
    "name": "group_name",
    }

789
790
  def GetPostOpInput(self):
    """Create a node group.
791

792

793
794
795
796
797
    """
    assert not self.items
    return (self.request_body, {
      "dry_run": self.dryRun(),
      })
798
799
800
801
802

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

    """
803
    client = self.GetClient(query=True)
804
805
806
807
808
809
810
811
812
813
814

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


815
class R_2_groups_name(baserlib.OpcodeResource):
816
  """/2/groups/[group_name] resource.
817
818

  """
819
820
  DELETE_OPCODE = opcodes.OpGroupRemove

821
822
823
824
825
  def GET(self):
    """Send information about a node group.

    """
    group_name = self.items[0]
826
    client = self.GetClient(query=True)
827
828
829
830
831
832
833

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

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

834
  def GetDeleteOpInput(self):
835
836
837
    """Delete a node group.

    """
838
839
840
841
842
    assert len(self.items) == 1
    return ({}, {
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      })
843
844


845
class R_2_groups_name_modify(baserlib.OpcodeResource):
846
847
848
  """/2/groups/[group_name]/modify resource.

  """
849
  PUT_OPCODE = opcodes.OpGroupSetParams
850
851
852
853
854
  PUT_RENAME = {
    "custom_ndparams": "ndparams",
    "custom_ipolicy": "ipolicy",
    "custom_diskparams": "diskparams",
    }
855

856
857
  def GetPutOpInput(self):
    """Changes some parameters of node group.
858
859

    """
860
861
862
863
    assert self.items
    return (self.request_body, {
      "group_name": self.items[0],
      })
864
865


866
class R_2_groups_name_rename(baserlib.OpcodeResource):
867
  """/2/groups/[group_name]/rename resource.
868
869

  """
870
  PUT_OPCODE = opcodes.OpGroupRename
871

872
873
  def GetPutOpInput(self):
    """Changes the name of a node group.
874
875

    """
876
877
878
879
880
    assert len(self.items) == 1
    return (self.request_body, {
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      })
881
882


883
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
884
  """/2/groups/[group_name]/assign-nodes resource.
885
886

  """
887
  PUT_OPCODE = opcodes.OpGroupAssignNodes
888

889
890
  def GetPutOpInput(self):
    """Assigns nodes to a group.
891
892

    """
893
894
    assert len(self.items) == 1
    return (self.request_body, {
895
896
897
898
899
900
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      "force": self.useForce(),
      })


901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
def _ConvertUsbDevices(data):
  """Convert in place the usb_devices string to the proper format.

  In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
  comma to space because commas cannot be accepted on the command line
  (they already act as the separator between different hvparams). RAPI
  should be able to accept commas for backwards compatibility, but we want
  it to also accept the new space separator. Therefore, we convert
  spaces into commas here and keep the old parsing logic elsewhere.

  """
  try:
    hvparams = data["hvparams"]
    usb_devices = hvparams[constants.HV_USB_DEVICES]
    hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
    data["hvparams"] = hvparams
  except KeyError:
    #No usb_devices, no modification required
    pass


922
923
class R_2_instances(baserlib.OpcodeResource):
  """/2/instances resource.
924
925

  """
926
  GET_OPCODE = opcodes.OpInstanceQuery
927
928
  POST_OPCODE = opcodes.OpInstanceCreate
  POST_RENAME = {
929
930
931
932
    "os": "os_type",
    "name": "instance_name",
    }

933
934
935
936
  def GET(self):
    """Returns a list of all available instances.

    """
937
    client = self.GetClient()
938

939
940
941
    use_locking = self.useLocking()
    if self.useBulk():
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
942
      return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
943
    else:
944
      instancesdata = client.QueryInstances([], ["name"], use_locking)
945
      instanceslist = [row[0] for row in instancesdata]
946
947
948
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
                                   uri_fields=("id", "uri"))

949
  def GetPostOpInput(self):
950
951
952
953
954
    """Create an instance.

    @return: a job id

    """
955
    baserlib.CheckType(self.request_body, dict, "Body contents")
956
957
958
959
960

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

    if data_version == 0:
961
962
      raise http.HttpBadRequest("Instance creation request version 0 is no"
                                " longer supported")
963
    elif data_version != 1:
964
      raise http.HttpBadRequest("Unsupported request data version %s" %
965
                                data_version)
966

967
968
969
970
    data = self.request_body.copy()
    # Remove "__version__"
    data.pop(_REQ_DATA_VERSION, None)

971
972
    _ConvertUsbDevices(data)

973
974
975
    return (data, {
      "dry_run": self.dryRun(),
      })
976

977

978
979
980
981
982
983
984
985
986
987
988
989
990
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

    """
991
992
993
994
    if "instances" not in self.request_body:
      raise http.HttpBadRequest("Request is missing required 'instances' field"
                                " in body")

995
996
997
998
999
1000
1001
    # Unlike most other RAPI calls, this one is composed of individual opcodes,
    # and we have to do the filling ourselves
    OPCODE_RENAME = {
      "os": "os_type",
      "name": "instance_name",
    }

1002
    body = objects.FillDict(self.request_body, {
1003
      "instances": [
1004
1005
        baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {},
                            rename=OPCODE_RENAME)
1006
1007
        for inst in self.request_body["instances"]
        ],
1008
1009
1010
      })

    return (body, {
1011
1012
1013
1014
      "dry_run": self.dryRun(),
      })


1015
class R_2_instances_name(baserlib.OpcodeResource):
1016
  """/2/instances/[instance_name] resource.
1017
1018

  """
1019
  GET_OPCODE = opcodes.OpInstanceQuery
1020
1021
  DELETE_OPCODE = opcodes.OpInstanceRemove

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
1022
1023
1024
1025
  def GET(self):
    """Send information about an instance.

    """
1026
    client = self.GetClient()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
1027
    instance_name = self.items[0]
1028
1029
1030
1031
1032

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

1034
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1035

1036
  def GetDeleteOpInput(self):
Iustin Pop's avatar
Iustin Pop committed
1037
1038
1039
    """Delete an instance.

    """
1040
    assert len(self.items) == 1
1041
    return (self.request_body, {
1042
1043
1044
1045
      "instance_name": self.items[0],
      "ignore_failures": False,
      "dry_run": self.dryRun(),
      })
Iustin Pop's avatar
Iustin Pop committed
1046

1047

1048
class R_2_instances_name_info(baserlib.OpcodeResource):
1049
1050
1051
  """/2/instances/[instance_name]/info resource.

  """
1052
1053
1054
  GET_OPCODE = opcodes.OpInstanceQueryData

  def GetGetOpInput(self):
1055
1056
1057
    """Request detailed instance information.

    """
1058
1059
1060
1061
1062
    assert len(self.items) == 1
    return ({}, {
      "instances": [self.items[0]],
      "static": bool(self._checkIntVariable("static", default=0)),
      })
1063
1064


1065
class R_2_instances_name_reboot(baserlib.OpcodeResource):
1066
1067
1068
1069
1070
  """/2/instances/[instance_name]/reboot resource.

  Implements an instance reboot.

  """
1071
1072
1073
  POST_OPCODE = opcodes.OpInstanceReboot

  def GetPostOpInput(self):
1074
1075
    """Reboot an instance.

1076
1077
1078
    The URI takes type=[hard|soft|full] and
    ignore_secondaries=[False|True] parameters.

1079
    """
1080
    return (self.request_body, {
1081
1082
1083
1084
1085
1086
      "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(),
      })
1087
1088


1089
class R_2_instances_name_startup(baserlib.OpcodeResource):
1090
1091
1092
1093
1094
  """/2/instances/[instance_name]/startup resource.

  Implements an instance startup.

  """