Fix multi-allocation RAPI method
[ganeti-local] / lib / rapi / rlib2.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Remote API resource implementations.
23
24 PUT or POST?
25 ============
26
27 According to RFC2616 the main difference between PUT and POST is that
28 POST can create new resources but PUT can only create the resource the
29 URI was pointing to on the PUT request.
30
31 In the context of this module POST on ``/2/instances`` to change an existing
32 entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
33 new instance) with a name specified in the request.
34
35 Quoting from RFC2616, section 9.6::
36
37   The fundamental difference between the POST and PUT requests is reflected in
38   the different meaning of the Request-URI. The URI in a POST request
39   identifies the resource that will handle the enclosed entity. That resource
40   might be a data-accepting process, a gateway to some other protocol, or a
41   separate entity that accepts annotations. In contrast, the URI in a PUT
42   request identifies the entity enclosed with the request -- the user agent
43   knows what URI is intended and the server MUST NOT attempt to apply the
44   request to some other resource. If the server desires that the request be
45   applied to a different URI, it MUST send a 301 (Moved Permanently) response;
46   the user agent MAY then make its own decision regarding whether or not to
47   redirect the request.
48
49 So when adding new methods, if they are operating on the URI entity itself,
50 PUT should be prefered over POST.
51
52 """
53
54 # pylint: disable=C0103
55
56 # C0103: Invalid name, since the R_* names are not conforming
57
58 from ganeti import opcodes
59 from ganeti import objects
60 from ganeti import http
61 from ganeti import constants
62 from ganeti import cli
63 from ganeti import rapi
64 from ganeti import ht
65 from ganeti import compat
66 from ganeti import ssconf
67 from ganeti.rapi import baserlib
68
69
70 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
71 I_FIELDS = ["name", "admin_state", "os",
72             "pnode", "snodes",
73             "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             "disk.sizes", "disk.spindles", "disk_usage", "disk.uuids",
78             "disk.names",
79             "beparams", "hvparams",
80             "oper_state", "oper_ram", "oper_vcpus", "status",
81             "custom_hvparams", "custom_beparams", "custom_nicparams",
82             ] + _COMMON_FIELDS
83
84 N_FIELDS = ["name", "offline", "master_candidate", "drained",
85             "dtotal", "dfree", "sptotal", "spfree",
86             "mtotal", "mnode", "mfree",
87             "pinst_cnt", "sinst_cnt",
88             "ctotal", "cnos", "cnodes", "csockets",
89             "pip", "sip", "role",
90             "pinst_list", "sinst_list",
91             "master_capable", "vm_capable",
92             "ndparams",
93             "group.uuid",
94             ] + _COMMON_FIELDS
95
96 NET_FIELDS = ["name", "network", "gateway",
97               "network6", "gateway6",
98               "mac_prefix",
99               "free_count", "reserved_count",
100               "map", "group_list", "inst_list",
101               "external_reservations",
102               ] + _COMMON_FIELDS
103
104 G_FIELDS = [
105   "alloc_policy",
106   "name",
107   "node_cnt",
108   "node_list",
109   "ipolicy",
110   "custom_ipolicy",
111   "diskparams",
112   "custom_diskparams",
113   "ndparams",
114   "custom_ndparams",
115   ] + _COMMON_FIELDS
116
117 J_FIELDS_BULK = [
118   "id", "ops", "status", "summary",
119   "opstatus",
120   "received_ts", "start_ts", "end_ts",
121   ]
122
123 J_FIELDS = J_FIELDS_BULK + [
124   "oplog",
125   "opresult",
126   ]
127
128 _NR_DRAINED = "drained"
129 _NR_MASTER_CANDIDATE = "master-candidate"
130 _NR_MASTER = "master"
131 _NR_OFFLINE = "offline"
132 _NR_REGULAR = "regular"
133
134 _NR_MAP = {
135   constants.NR_MASTER: _NR_MASTER,
136   constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
137   constants.NR_DRAINED: _NR_DRAINED,
138   constants.NR_OFFLINE: _NR_OFFLINE,
139   constants.NR_REGULAR: _NR_REGULAR,
140   }
141
142 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
143
144 # Request data version field
145 _REQ_DATA_VERSION = "__version__"
146
147 # Feature string for instance creation request data version 1
148 _INST_CREATE_REQV1 = "instance-create-reqv1"
149
150 # Feature string for instance reinstall request version 1
151 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
152
153 # Feature string for node migration version 1
154 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
155
156 # Feature string for node evacuation with LU-generated jobs
157 _NODE_EVAC_RES1 = "node-evac-res1"
158
159 ALL_FEATURES = compat.UniqueFrozenset([
160   _INST_CREATE_REQV1,
161   _INST_REINSTALL_REQV1,
162   _NODE_MIGRATE_REQV1,
163   _NODE_EVAC_RES1,
164   ])
165
166 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
167 _WFJC_TIMEOUT = 10
168
169
170 # FIXME: For compatibility we update the beparams/memory field. Needs to be
171 #        removed in Ganeti 2.8
172 def _UpdateBeparams(inst):
173   """Updates the beparams dict of inst to support the memory field.
174
175   @param inst: Inst dict
176   @return: Updated inst dict
177
178   """
179   beparams = inst["beparams"]
180   beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
181
182   return inst
183
184
185 class R_root(baserlib.ResourceBase):
186   """/ resource.
187
188   """
189   @staticmethod
190   def GET():
191     """Supported for legacy reasons.
192
193     """
194     return None
195
196
197 class R_2(R_root):
198   """/2 resource.
199
200   """
201
202
203 class R_version(baserlib.ResourceBase):
204   """/version resource.
205
206   This resource should be used to determine the remote API version and
207   to adapt clients accordingly.
208
209   """
210   @staticmethod
211   def GET():
212     """Returns the remote API version.
213
214     """
215     return constants.RAPI_VERSION
216
217
218 class R_2_info(baserlib.OpcodeResource):
219   """/2/info resource.
220
221   """
222   GET_OPCODE = opcodes.OpClusterQuery
223
224   def GET(self):
225     """Returns cluster information.
226
227     """
228     client = self.GetClient(query=True)
229     return client.QueryClusterInfo()
230
231
232 class R_2_features(baserlib.ResourceBase):
233   """/2/features resource.
234
235   """
236   @staticmethod
237   def GET():
238     """Returns list of optional RAPI features implemented.
239
240     """
241     return list(ALL_FEATURES)
242
243
244 class R_2_os(baserlib.OpcodeResource):
245   """/2/os resource.
246
247   """
248   GET_OPCODE = opcodes.OpOsDiagnose
249
250   def GET(self):
251     """Return a list of all OSes.
252
253     Can return error 500 in case of a problem.
254
255     Example: ["debian-etch"]
256
257     """
258     cl = self.GetClient()
259     op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
260     job_id = self.SubmitJob([op], cl=cl)
261     # we use custom feedback function, instead of print we log the status
262     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
263     diagnose_data = result[0]
264
265     if not isinstance(diagnose_data, list):
266       raise http.HttpBadGateway(message="Can't get OS list")
267
268     os_names = []
269     for (name, variants) in diagnose_data:
270       os_names.extend(cli.CalculateOSNames(name, variants))
271
272     return os_names
273
274
275 class R_2_redist_config(baserlib.OpcodeResource):
276   """/2/redistribute-config resource.
277
278   """
279   PUT_OPCODE = opcodes.OpClusterRedistConf
280
281
282 class R_2_cluster_modify(baserlib.OpcodeResource):
283   """/2/modify resource.
284
285   """
286   PUT_OPCODE = opcodes.OpClusterSetParams
287
288
289 class R_2_jobs(baserlib.ResourceBase):
290   """/2/jobs resource.
291
292   """
293   def GET(self):
294     """Returns a dictionary of jobs.
295
296     @return: a dictionary with jobs id and uri.
297
298     """
299     client = self.GetClient(query=True)
300
301     if self.useBulk():
302       bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
303       return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
304     else:
305       jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
306       return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
307                                    uri_fields=("id", "uri"))
308
309
310 class R_2_jobs_id(baserlib.ResourceBase):
311   """/2/jobs/[job_id] resource.
312
313   """
314   def GET(self):
315     """Returns a job status.
316
317     @return: a dictionary with job parameters.
318         The result includes:
319             - id: job ID as a number
320             - status: current job status as a string
321             - ops: involved OpCodes as a list of dictionaries for each
322               opcodes in the job
323             - opstatus: OpCodes status as a list
324             - opresult: OpCodes results as a list of lists
325
326     """
327     job_id = self.items[0]
328     result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
329     if result is None:
330       raise http.HttpNotFound()
331     return baserlib.MapFields(J_FIELDS, result)
332
333   def DELETE(self):
334     """Cancel not-yet-started job.
335
336     """
337     job_id = self.items[0]
338     result = self.GetClient().CancelJob(job_id)
339     return result
340
341
342 class R_2_jobs_id_wait(baserlib.ResourceBase):
343   """/2/jobs/[job_id]/wait resource.
344
345   """
346   # WaitForJobChange provides access to sensitive information and blocks
347   # machine resources (it's a blocking RAPI call), hence restricting access.
348   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
349
350   def GET(self):
351     """Waits for job changes.
352
353     """
354     job_id = self.items[0]
355
356     fields = self.getBodyParameter("fields")
357     prev_job_info = self.getBodyParameter("previous_job_info", None)
358     prev_log_serial = self.getBodyParameter("previous_log_serial", None)
359
360     if not isinstance(fields, list):
361       raise http.HttpBadRequest("The 'fields' parameter should be a list")
362
363     if not (prev_job_info is None or isinstance(prev_job_info, list)):
364       raise http.HttpBadRequest("The 'previous_job_info' parameter should"
365                                 " be a list")
366
367     if not (prev_log_serial is None or
368             isinstance(prev_log_serial, (int, long))):
369       raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
370                                 " be a number")
371
372     client = self.GetClient()
373     result = client.WaitForJobChangeOnce(job_id, fields,
374                                          prev_job_info, prev_log_serial,
375                                          timeout=_WFJC_TIMEOUT)
376     if not result:
377       raise http.HttpNotFound()
378
379     if result == constants.JOB_NOTCHANGED:
380       # No changes
381       return None
382
383     (job_info, log_entries) = result
384
385     return {
386       "job_info": job_info,
387       "log_entries": log_entries,
388       }
389
390
391 class R_2_nodes(baserlib.OpcodeResource):
392   """/2/nodes resource.
393
394   """
395   GET_OPCODE = opcodes.OpNodeQuery
396
397   def GET(self):
398     """Returns a list of all nodes.
399
400     """
401     client = self.GetClient(query=True)
402
403     if self.useBulk():
404       bulkdata = client.QueryNodes([], N_FIELDS, False)
405       return baserlib.MapBulkFields(bulkdata, N_FIELDS)
406     else:
407       nodesdata = client.QueryNodes([], ["name"], False)
408       nodeslist = [row[0] for row in nodesdata]
409       return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
410                                    uri_fields=("id", "uri"))
411
412
413 class R_2_nodes_name(baserlib.OpcodeResource):
414   """/2/nodes/[node_name] resource.
415
416   """
417   GET_OPCODE = opcodes.OpNodeQuery
418
419   def GET(self):
420     """Send information about a node.
421
422     """
423     node_name = self.items[0]
424     client = self.GetClient(query=True)
425
426     result = baserlib.HandleItemQueryErrors(client.QueryNodes,
427                                             names=[node_name], fields=N_FIELDS,
428                                             use_locking=self.useLocking())
429
430     return baserlib.MapFields(N_FIELDS, result[0])
431
432
433 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
434   """/2/nodes/[node_name]/powercycle resource.
435
436   """
437   POST_OPCODE = opcodes.OpNodePowercycle
438
439   def GetPostOpInput(self):
440     """Tries to powercycle a node.
441
442     """
443     return (self.request_body, {
444       "node_name": self.items[0],
445       "force": self.useForce(),
446       })
447
448
449 class R_2_nodes_name_role(baserlib.OpcodeResource):
450   """/2/nodes/[node_name]/role resource.
451
452   """
453   PUT_OPCODE = opcodes.OpNodeSetParams
454
455   def GET(self):
456     """Returns the current node role.
457
458     @return: Node role
459
460     """
461     node_name = self.items[0]
462     client = self.GetClient(query=True)
463     result = client.QueryNodes(names=[node_name], fields=["role"],
464                                use_locking=self.useLocking())
465
466     return _NR_MAP[result[0][0]]
467
468   def GetPutOpInput(self):
469     """Sets the node role.
470
471     """
472     baserlib.CheckType(self.request_body, basestring, "Body contents")
473
474     role = self.request_body
475
476     if role == _NR_REGULAR:
477       candidate = False
478       offline = False
479       drained = False
480
481     elif role == _NR_MASTER_CANDIDATE:
482       candidate = True
483       offline = drained = None
484
485     elif role == _NR_DRAINED:
486       drained = True
487       candidate = offline = None
488
489     elif role == _NR_OFFLINE:
490       offline = True
491       candidate = drained = None
492
493     else:
494       raise http.HttpBadRequest("Can't set '%s' role" % role)
495
496     assert len(self.items) == 1
497
498     return ({}, {
499       "node_name": self.items[0],
500       "master_candidate": candidate,
501       "offline": offline,
502       "drained": drained,
503       "force": self.useForce(),
504       "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
505       })
506
507
508 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
509   """/2/nodes/[node_name]/evacuate resource.
510
511   """
512   POST_OPCODE = opcodes.OpNodeEvacuate
513
514   def GetPostOpInput(self):
515     """Evacuate all instances off a node.
516
517     """
518     return (self.request_body, {
519       "node_name": self.items[0],
520       "dry_run": self.dryRun(),
521       })
522
523
524 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
525   """/2/nodes/[node_name]/migrate resource.
526
527   """
528   POST_OPCODE = opcodes.OpNodeMigrate
529
530   def GetPostOpInput(self):
531     """Migrate all primary instances from a node.
532
533     """
534     if self.queryargs:
535       # Support old-style requests
536       if "live" in self.queryargs and "mode" in self.queryargs:
537         raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
538                                   " be passed")
539
540       if "live" in self.queryargs:
541         if self._checkIntVariable("live", default=1):
542           mode = constants.HT_MIGRATION_LIVE
543         else:
544           mode = constants.HT_MIGRATION_NONLIVE
545       else:
546         mode = self._checkStringVariable("mode", default=None)
547
548       data = {
549         "mode": mode,
550         }
551     else:
552       data = self.request_body
553
554     return (data, {
555       "node_name": self.items[0],
556       })
557
558
559 class R_2_nodes_name_modify(baserlib.OpcodeResource):
560   """/2/nodes/[node_name]/modify resource.
561
562   """
563   POST_OPCODE = opcodes.OpNodeSetParams
564
565   def GetPostOpInput(self):
566     """Changes parameters of a node.
567
568     """
569     assert len(self.items) == 1
570
571     return (self.request_body, {
572       "node_name": self.items[0],
573       })
574
575
576 class R_2_nodes_name_storage(baserlib.OpcodeResource):
577   """/2/nodes/[node_name]/storage resource.
578
579   """
580   # LUNodeQueryStorage acquires locks, hence restricting access to GET
581   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
582   GET_OPCODE = opcodes.OpNodeQueryStorage
583
584   def GetGetOpInput(self):
585     """List storage available on a node.
586
587     """
588     storage_type = self._checkStringVariable("storage_type", None)
589     output_fields = self._checkStringVariable("output_fields", None)
590
591     if not output_fields:
592       raise http.HttpBadRequest("Missing the required 'output_fields'"
593                                 " parameter")
594
595     return ({}, {
596       "nodes": [self.items[0]],
597       "storage_type": storage_type,
598       "output_fields": output_fields.split(","),
599       })
600
601
602 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
603   """/2/nodes/[node_name]/storage/modify resource.
604
605   """
606   PUT_OPCODE = opcodes.OpNodeModifyStorage
607
608   def GetPutOpInput(self):
609     """Modifies a storage volume on a node.
610
611     """
612     storage_type = self._checkStringVariable("storage_type", None)
613     name = self._checkStringVariable("name", None)
614
615     if not name:
616       raise http.HttpBadRequest("Missing the required 'name'"
617                                 " parameter")
618
619     changes = {}
620
621     if "allocatable" in self.queryargs:
622       changes[constants.SF_ALLOCATABLE] = \
623         bool(self._checkIntVariable("allocatable", default=1))
624
625     return ({}, {
626       "node_name": self.items[0],
627       "storage_type": storage_type,
628       "name": name,
629       "changes": changes,
630       })
631
632
633 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
634   """/2/nodes/[node_name]/storage/repair resource.
635
636   """
637   PUT_OPCODE = opcodes.OpRepairNodeStorage
638
639   def GetPutOpInput(self):
640     """Repairs a storage volume on a node.
641
642     """
643     storage_type = self._checkStringVariable("storage_type", None)
644     name = self._checkStringVariable("name", None)
645     if not name:
646       raise http.HttpBadRequest("Missing the required 'name'"
647                                 " parameter")
648
649     return ({}, {
650       "node_name": self.items[0],
651       "storage_type": storage_type,
652       "name": name,
653       })
654
655
656 class R_2_networks(baserlib.OpcodeResource):
657   """/2/networks resource.
658
659   """
660   GET_OPCODE = opcodes.OpNetworkQuery
661   POST_OPCODE = opcodes.OpNetworkAdd
662   POST_RENAME = {
663     "name": "network_name",
664     }
665
666   def GetPostOpInput(self):
667     """Create a network.
668
669     """
670     assert not self.items
671     return (self.request_body, {
672       "dry_run": self.dryRun(),
673       })
674
675   def GET(self):
676     """Returns a list of all networks.
677
678     """
679     client = self.GetClient(query=True)
680
681     if self.useBulk():
682       bulkdata = client.QueryNetworks([], NET_FIELDS, False)
683       return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
684     else:
685       data = client.QueryNetworks([], ["name"], False)
686       networknames = [row[0] for row in data]
687       return baserlib.BuildUriList(networknames, "/2/networks/%s",
688                                    uri_fields=("name", "uri"))
689
690
691 class R_2_networks_name(baserlib.OpcodeResource):
692   """/2/networks/[network_name] resource.
693
694   """
695   DELETE_OPCODE = opcodes.OpNetworkRemove
696
697   def GET(self):
698     """Send information about a network.
699
700     """
701     network_name = self.items[0]
702     client = self.GetClient(query=True)
703
704     result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
705                                             names=[network_name],
706                                             fields=NET_FIELDS,
707                                             use_locking=self.useLocking())
708
709     return baserlib.MapFields(NET_FIELDS, result[0])
710
711   def GetDeleteOpInput(self):
712     """Delete a network.
713
714     """
715     assert len(self.items) == 1
716     return (self.request_body, {
717       "network_name": self.items[0],
718       "dry_run": self.dryRun(),
719       })
720
721
722 class R_2_networks_name_connect(baserlib.OpcodeResource):
723   """/2/networks/[network_name]/connect resource.
724
725   """
726   PUT_OPCODE = opcodes.OpNetworkConnect
727
728   def GetPutOpInput(self):
729     """Changes some parameters of node group.
730
731     """
732     assert self.items
733     return (self.request_body, {
734       "network_name": self.items[0],
735       "dry_run": self.dryRun(),
736       })
737
738
739 class R_2_networks_name_disconnect(baserlib.OpcodeResource):
740   """/2/networks/[network_name]/disconnect resource.
741
742   """
743   PUT_OPCODE = opcodes.OpNetworkDisconnect
744
745   def GetPutOpInput(self):
746     """Changes some parameters of node group.
747
748     """
749     assert self.items
750     return (self.request_body, {
751       "network_name": self.items[0],
752       "dry_run": self.dryRun(),
753       })
754
755
756 class R_2_networks_name_modify(baserlib.OpcodeResource):
757   """/2/networks/[network_name]/modify resource.
758
759   """
760   PUT_OPCODE = opcodes.OpNetworkSetParams
761
762   def GetPutOpInput(self):
763     """Changes some parameters of network.
764
765     """
766     assert self.items
767     return (self.request_body, {
768       "network_name": self.items[0],
769       })
770
771
772 class R_2_groups(baserlib.OpcodeResource):
773   """/2/groups resource.
774
775   """
776   GET_OPCODE = opcodes.OpGroupQuery
777   POST_OPCODE = opcodes.OpGroupAdd
778   POST_RENAME = {
779     "name": "group_name",
780     }
781
782   def GetPostOpInput(self):
783     """Create a node group.
784
785
786     """
787     assert not self.items
788     return (self.request_body, {
789       "dry_run": self.dryRun(),
790       })
791
792   def GET(self):
793     """Returns a list of all node groups.
794
795     """
796     client = self.GetClient(query=True)
797
798     if self.useBulk():
799       bulkdata = client.QueryGroups([], G_FIELDS, False)
800       return baserlib.MapBulkFields(bulkdata, G_FIELDS)
801     else:
802       data = client.QueryGroups([], ["name"], False)
803       groupnames = [row[0] for row in data]
804       return baserlib.BuildUriList(groupnames, "/2/groups/%s",
805                                    uri_fields=("name", "uri"))
806
807
808 class R_2_groups_name(baserlib.OpcodeResource):
809   """/2/groups/[group_name] resource.
810
811   """
812   DELETE_OPCODE = opcodes.OpGroupRemove
813
814   def GET(self):
815     """Send information about a node group.
816
817     """
818     group_name = self.items[0]
819     client = self.GetClient(query=True)
820
821     result = baserlib.HandleItemQueryErrors(client.QueryGroups,
822                                             names=[group_name], fields=G_FIELDS,
823                                             use_locking=self.useLocking())
824
825     return baserlib.MapFields(G_FIELDS, result[0])
826
827   def GetDeleteOpInput(self):
828     """Delete a node group.
829
830     """
831     assert len(self.items) == 1
832     return ({}, {
833       "group_name": self.items[0],
834       "dry_run": self.dryRun(),
835       })
836
837
838 class R_2_groups_name_modify(baserlib.OpcodeResource):
839   """/2/groups/[group_name]/modify resource.
840
841   """
842   PUT_OPCODE = opcodes.OpGroupSetParams
843
844   def GetPutOpInput(self):
845     """Changes some parameters of node group.
846
847     """
848     assert self.items
849     return (self.request_body, {
850       "group_name": self.items[0],
851       })
852
853
854 class R_2_groups_name_rename(baserlib.OpcodeResource):
855   """/2/groups/[group_name]/rename resource.
856
857   """
858   PUT_OPCODE = opcodes.OpGroupRename
859
860   def GetPutOpInput(self):
861     """Changes the name of a node group.
862
863     """
864     assert len(self.items) == 1
865     return (self.request_body, {
866       "group_name": self.items[0],
867       "dry_run": self.dryRun(),
868       })
869
870
871 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
872   """/2/groups/[group_name]/assign-nodes resource.
873
874   """
875   PUT_OPCODE = opcodes.OpGroupAssignNodes
876
877   def GetPutOpInput(self):
878     """Assigns nodes to a group.
879
880     """
881     assert len(self.items) == 1
882     return (self.request_body, {
883       "group_name": self.items[0],
884       "dry_run": self.dryRun(),
885       "force": self.useForce(),
886       })
887
888
889 def _ConvertUsbDevices(data):
890   """Convert in place the usb_devices string to the proper format.
891
892   In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
893   comma to space because commas cannot be accepted on the command line
894   (they already act as the separator between different hvparams). RAPI
895   should be able to accept commas for backwards compatibility, but we want
896   it to also accept the new space separator. Therefore, we convert
897   spaces into commas here and keep the old parsing logic elsewhere.
898
899   """
900   try:
901     hvparams = data["hvparams"]
902     usb_devices = hvparams[constants.HV_USB_DEVICES]
903     hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
904     data["hvparams"] = hvparams
905   except KeyError:
906     #No usb_devices, no modification required
907     pass
908
909
910 class R_2_instances(baserlib.OpcodeResource):
911   """/2/instances resource.
912
913   """
914   GET_OPCODE = opcodes.OpInstanceQuery
915   POST_OPCODE = opcodes.OpInstanceCreate
916   POST_RENAME = {
917     "os": "os_type",
918     "name": "instance_name",
919     }
920
921   def GET(self):
922     """Returns a list of all available instances.
923
924     """
925     client = self.GetClient()
926
927     use_locking = self.useLocking()
928     if self.useBulk():
929       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
930       return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
931     else:
932       instancesdata = client.QueryInstances([], ["name"], use_locking)
933       instanceslist = [row[0] for row in instancesdata]
934       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
935                                    uri_fields=("id", "uri"))
936
937   def GetPostOpInput(self):
938     """Create an instance.
939
940     @return: a job id
941
942     """
943     baserlib.CheckType(self.request_body, dict, "Body contents")
944
945     # Default to request data version 0
946     data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
947
948     if data_version == 0:
949       raise http.HttpBadRequest("Instance creation request version 0 is no"
950                                 " longer supported")
951     elif data_version != 1:
952       raise http.HttpBadRequest("Unsupported request data version %s" %
953                                 data_version)
954
955     data = self.request_body.copy()
956     # Remove "__version__"
957     data.pop(_REQ_DATA_VERSION, None)
958
959     _ConvertUsbDevices(data)
960
961     return (data, {
962       "dry_run": self.dryRun(),
963       })
964
965
966 class R_2_instances_multi_alloc(baserlib.OpcodeResource):
967   """/2/instances-multi-alloc resource.
968
969   """
970   POST_OPCODE = opcodes.OpInstanceMultiAlloc
971
972   def GetPostOpInput(self):
973     """Try to allocate multiple instances.
974
975     @return: A dict with submitted jobs, allocatable instances and failed
976              allocations
977
978     """
979     if "instances" not in self.request_body:
980       raise http.HttpBadRequest("Request is missing required 'instances' field"
981                                 " in body")
982
983     # Unlike most other RAPI calls, this one is composed of individual opcodes,
984     # and we have to do the filling ourselves
985     OPCODE_RENAME = {
986       "os": "os_type",
987       "name": "instance_name",
988     }
989
990     body = objects.FillDict(self.request_body, {
991       "instances": [
992         baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {}, OPCODE_RENAME)
993         for inst in self.request_body["instances"]
994         ],
995       })
996
997     return (body, {
998       "dry_run": self.dryRun(),
999       })
1000
1001
1002 class R_2_instances_name(baserlib.OpcodeResource):
1003   """/2/instances/[instance_name] resource.
1004
1005   """
1006   GET_OPCODE = opcodes.OpInstanceQuery
1007   DELETE_OPCODE = opcodes.OpInstanceRemove
1008
1009   def GET(self):
1010     """Send information about an instance.
1011
1012     """
1013     client = self.GetClient()
1014     instance_name = self.items[0]
1015
1016     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1017                                             names=[instance_name],
1018                                             fields=I_FIELDS,
1019                                             use_locking=self.useLocking())
1020
1021     return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1022
1023   def GetDeleteOpInput(self):
1024     """Delete an instance.
1025
1026     """
1027     assert len(self.items) == 1
1028     return ({}, {
1029       "instance_name": self.items[0],
1030       "ignore_failures": False,
1031       "dry_run": self.dryRun(),
1032       })
1033
1034
1035 class R_2_instances_name_info(baserlib.OpcodeResource):
1036   """/2/instances/[instance_name]/info resource.
1037
1038   """
1039   GET_OPCODE = opcodes.OpInstanceQueryData
1040
1041   def GetGetOpInput(self):
1042     """Request detailed instance information.
1043
1044     """
1045     assert len(self.items) == 1
1046     return ({}, {
1047       "instances": [self.items[0]],
1048       "static": bool(self._checkIntVariable("static", default=0)),
1049       })
1050
1051
1052 class R_2_instances_name_reboot(baserlib.OpcodeResource):
1053   """/2/instances/[instance_name]/reboot resource.
1054
1055   Implements an instance reboot.
1056
1057   """
1058   POST_OPCODE = opcodes.OpInstanceReboot
1059
1060   def GetPostOpInput(self):
1061     """Reboot an instance.
1062
1063     The URI takes type=[hard|soft|full] and
1064     ignore_secondaries=[False|True] parameters.
1065
1066     """
1067     return ({}, {
1068       "instance_name": self.items[0],
1069       "reboot_type":
1070         self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1071       "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1072       "dry_run": self.dryRun(),
1073       })
1074
1075
1076 class R_2_instances_name_startup(baserlib.OpcodeResource):
1077   """/2/instances/[instance_name]/startup resource.
1078
1079   Implements an instance startup.
1080
1081   """
1082   PUT_OPCODE = opcodes.OpInstanceStartup
1083
1084   def GetPutOpInput(self):
1085     """Startup an instance.
1086
1087     The URI takes force=[False|True] parameter to start the instance
1088     if even if secondary disks are failing.
1089
1090     """
1091     return ({}, {
1092       "instance_name": self.items[0],
1093       "force": self.useForce(),
1094       "dry_run": self.dryRun(),
1095       "no_remember": bool(self._checkIntVariable("no_remember")),
1096       })
1097
1098
1099 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1100   """/2/instances/[instance_name]/shutdown resource.
1101
1102   Implements an instance shutdown.
1103
1104   """
1105   PUT_OPCODE = opcodes.OpInstanceShutdown
1106
1107   def GetPutOpInput(self):
1108     """Shutdown an instance.
1109
1110     """
1111     return (self.request_body, {
1112       "instance_name": self.items[0],
1113       "no_remember": bool(self._checkIntVariable("no_remember")),
1114       "dry_run": self.dryRun(),
1115       })
1116
1117
1118 def _ParseInstanceReinstallRequest(name, data):
1119   """Parses a request for reinstalling an instance.
1120
1121   """
1122   if not isinstance(data, dict):
1123     raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1124
1125   ostype = baserlib.CheckParameter(data, "os", default=None)
1126   start = baserlib.CheckParameter(data, "start", exptype=bool,
1127                                   default=True)
1128   osparams = baserlib.CheckParameter(data, "osparams", default=None)
1129
1130   ops = [
1131     opcodes.OpInstanceShutdown(instance_name=name),
1132     opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1133                                 osparams=osparams),
1134     ]
1135
1136   if start:
1137     ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1138
1139   return ops
1140
1141
1142 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1143   """/2/instances/[instance_name]/reinstall resource.
1144
1145   Implements an instance reinstall.
1146
1147   """
1148   POST_OPCODE = opcodes.OpInstanceReinstall
1149
1150   def POST(self):
1151     """Reinstall an instance.
1152
1153     The URI takes os=name and nostartup=[0|1] optional
1154     parameters. By default, the instance will be started
1155     automatically.
1156
1157     """
1158     if self.request_body:
1159       if self.queryargs:
1160         raise http.HttpBadRequest("Can't combine query and body parameters")
1161
1162       body = self.request_body
1163     elif self.queryargs:
1164       # Legacy interface, do not modify/extend
1165       body = {
1166         "os": self._checkStringVariable("os"),
1167         "start": not self._checkIntVariable("nostartup"),
1168         }
1169     else:
1170       body = {}
1171
1172     ops = _ParseInstanceReinstallRequest(self.items[0], body)
1173
1174     return self.SubmitJob(ops)
1175
1176
1177 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1178   """/2/instances/[instance_name]/replace-disks resource.
1179
1180   """
1181   POST_OPCODE = opcodes.OpInstanceReplaceDisks
1182
1183   def GetPostOpInput(self):
1184     """Replaces disks on an instance.
1185
1186     """
1187     static = {
1188       "instance_name": self.items[0],
1189       }
1190
1191     if self.request_body:
1192       data = self.request_body
1193     elif self.queryargs:
1194       # Legacy interface, do not modify/extend
1195       data = {
1196         "remote_node": self._checkStringVariable("remote_node", default=None),
1197         "mode": self._checkStringVariable("mode", default=None),
1198         "disks": self._checkStringVariable("disks", default=None),
1199         "iallocator": self._checkStringVariable("iallocator", default=None),
1200         }
1201     else:
1202       data = {}
1203
1204     # Parse disks
1205     try:
1206       raw_disks = data.pop("disks")
1207     except KeyError:
1208       pass
1209     else:
1210       if raw_disks:
1211         if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1212           data["disks"] = raw_disks
1213         else:
1214           # Backwards compatibility for strings of the format "1, 2, 3"
1215           try:
1216             data["disks"] = [int(part) for part in raw_disks.split(",")]
1217           except (TypeError, ValueError), err:
1218             raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1219
1220     return (data, static)
1221
1222
1223 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1224   """/2/instances/[instance_name]/activate-disks resource.
1225
1226   """
1227   PUT_OPCODE = opcodes.OpInstanceActivateDisks
1228
1229   def GetPutOpInput(self):
1230     """Activate disks for an instance.
1231
1232     The URI might contain ignore_size to ignore current recorded size.
1233
1234     """
1235     return ({}, {
1236       "instance_name": self.items[0],
1237       "ignore_size": bool(self._checkIntVariable("ignore_size")),
1238       })
1239
1240
1241 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1242   """/2/instances/[instance_name]/deactivate-disks resource.
1243
1244   """
1245   PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1246
1247   def GetPutOpInput(self):
1248     """Deactivate disks for an instance.
1249
1250     """
1251     return ({}, {
1252       "instance_name": self.items[0],
1253       })
1254
1255
1256 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1257   """/2/instances/[instance_name]/recreate-disks resource.
1258
1259   """
1260   POST_OPCODE = opcodes.OpInstanceRecreateDisks
1261
1262   def GetPostOpInput(self):
1263     """Recreate disks for an instance.
1264
1265     """
1266     return ({}, {
1267       "instance_name": self.items[0],
1268       })
1269
1270
1271 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1272   """/2/instances/[instance_name]/prepare-export resource.
1273
1274   """
1275   PUT_OPCODE = opcodes.OpBackupPrepare
1276
1277   def GetPutOpInput(self):
1278     """Prepares an export for an instance.
1279
1280     """
1281     return ({}, {
1282       "instance_name": self.items[0],
1283       "mode": self._checkStringVariable("mode"),
1284       })
1285
1286
1287 class R_2_instances_name_export(baserlib.OpcodeResource):
1288   """/2/instances/[instance_name]/export resource.
1289
1290   """
1291   PUT_OPCODE = opcodes.OpBackupExport
1292   PUT_RENAME = {
1293     "destination": "target_node",
1294     }
1295
1296   def GetPutOpInput(self):
1297     """Exports an instance.
1298
1299     """
1300     return (self.request_body, {
1301       "instance_name": self.items[0],
1302       })
1303
1304
1305 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1306   """/2/instances/[instance_name]/migrate resource.
1307
1308   """
1309   PUT_OPCODE = opcodes.OpInstanceMigrate
1310
1311   def GetPutOpInput(self):
1312     """Migrates an instance.
1313
1314     """
1315     return (self.request_body, {
1316       "instance_name": self.items[0],
1317       })
1318
1319
1320 class R_2_instances_name_failover(baserlib.OpcodeResource):
1321   """/2/instances/[instance_name]/failover resource.
1322
1323   """
1324   PUT_OPCODE = opcodes.OpInstanceFailover
1325
1326   def GetPutOpInput(self):
1327     """Does a failover of an instance.
1328
1329     """
1330     return (self.request_body, {
1331       "instance_name": self.items[0],
1332       })
1333
1334
1335 class R_2_instances_name_rename(baserlib.OpcodeResource):
1336   """/2/instances/[instance_name]/rename resource.
1337
1338   """
1339   PUT_OPCODE = opcodes.OpInstanceRename
1340
1341   def GetPutOpInput(self):
1342     """Changes the name of an instance.
1343
1344     """
1345     return (self.request_body, {
1346       "instance_name": self.items[0],
1347       })
1348
1349
1350 class R_2_instances_name_modify(baserlib.OpcodeResource):
1351   """/2/instances/[instance_name]/modify resource.
1352
1353   """
1354   PUT_OPCODE = opcodes.OpInstanceSetParams
1355
1356   def GetPutOpInput(self):
1357     """Changes parameters of an instance.
1358
1359     """
1360     data = self.request_body.copy()
1361     _ConvertUsbDevices(data)
1362
1363     return (data, {
1364       "instance_name": self.items[0],
1365       })
1366
1367
1368 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1369   """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1370
1371   """
1372   POST_OPCODE = opcodes.OpInstanceGrowDisk
1373
1374   def GetPostOpInput(self):
1375     """Increases the size of an instance disk.
1376
1377     """
1378     return (self.request_body, {
1379       "instance_name": self.items[0],
1380       "disk": int(self.items[1]),
1381       })
1382
1383
1384 class R_2_instances_name_console(baserlib.ResourceBase):
1385   """/2/instances/[instance_name]/console resource.
1386
1387   """
1388   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1389   GET_OPCODE = opcodes.OpInstanceConsole
1390
1391   def GET(self):
1392     """Request information for connecting to instance's console.
1393
1394     @return: Serialized instance console description, see
1395              L{objects.InstanceConsole}
1396
1397     """
1398     client = self.GetClient()
1399
1400     ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1401
1402     if console is None:
1403       raise http.HttpServiceUnavailable("Instance console unavailable")
1404
1405     assert isinstance(console, dict)
1406     return console
1407
1408
1409 def _GetQueryFields(args):
1410   """Tries to extract C{fields} query parameter.
1411
1412   @type args: dictionary
1413   @rtype: list of string
1414   @raise http.HttpBadRequest: When parameter can't be found
1415
1416   """
1417   try:
1418     fields = args["fields"]
1419   except KeyError:
1420     raise http.HttpBadRequest("Missing 'fields' query argument")
1421
1422   return _SplitQueryFields(fields[0])
1423
1424
1425 def _SplitQueryFields(fields):
1426   """Splits fields as given for a query request.
1427
1428   @type fields: string
1429   @rtype: list of string
1430
1431   """
1432   return [i.strip() for i in fields.split(",")]
1433
1434
1435 class R_2_query(baserlib.ResourceBase):
1436   """/2/query/[resource] resource.
1437
1438   """
1439   # Results might contain sensitive information
1440   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1441   PUT_ACCESS = GET_ACCESS
1442   GET_OPCODE = opcodes.OpQuery
1443   PUT_OPCODE = opcodes.OpQuery
1444
1445   def _Query(self, fields, qfilter):
1446     return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1447
1448   def GET(self):
1449     """Returns resource information.
1450
1451     @return: Query result, see L{objects.QueryResponse}
1452
1453     """
1454     return self._Query(_GetQueryFields(self.queryargs), None)
1455
1456   def PUT(self):
1457     """Submits job querying for resources.
1458
1459     @return: Query result, see L{objects.QueryResponse}
1460
1461     """
1462     body = self.request_body
1463
1464     baserlib.CheckType(body, dict, "Body contents")
1465
1466     try:
1467       fields = body["fields"]
1468     except KeyError:
1469       fields = _GetQueryFields(self.queryargs)
1470
1471     qfilter = body.get("qfilter", None)
1472     # TODO: remove this after 2.7
1473     if qfilter is None:
1474       qfilter = body.get("filter", None)
1475
1476     return self._Query(fields, qfilter)
1477
1478
1479 class R_2_query_fields(baserlib.ResourceBase):
1480   """/2/query/[resource]/fields resource.
1481
1482   """
1483   GET_OPCODE = opcodes.OpQueryFields
1484
1485   def GET(self):
1486     """Retrieves list of available fields for a resource.
1487
1488     @return: List of serialized L{objects.QueryFieldDefinition}
1489
1490     """
1491     try:
1492       raw_fields = self.queryargs["fields"]
1493     except KeyError:
1494       fields = None
1495     else:
1496       fields = _SplitQueryFields(raw_fields[0])
1497
1498     return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1499
1500
1501 class _R_Tags(baserlib.OpcodeResource):
1502   """Quasiclass for tagging resources.
1503
1504   Manages tags. When inheriting this class you must define the
1505   TAG_LEVEL for it.
1506
1507   """
1508   TAG_LEVEL = None
1509   GET_OPCODE = opcodes.OpTagsGet
1510   PUT_OPCODE = opcodes.OpTagsSet
1511   DELETE_OPCODE = opcodes.OpTagsDel
1512
1513   def __init__(self, items, queryargs, req, **kwargs):
1514     """A tag resource constructor.
1515
1516     We have to override the default to sort out cluster naming case.
1517
1518     """
1519     baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1520
1521     if self.TAG_LEVEL == constants.TAG_CLUSTER:
1522       self.name = None
1523     else:
1524       self.name = items[0]
1525
1526   def GET(self):
1527     """Returns a list of tags.
1528
1529     Example: ["tag1", "tag2", "tag3"]
1530
1531     """
1532     kind = self.TAG_LEVEL
1533
1534     if kind in (constants.TAG_INSTANCE,
1535                 constants.TAG_NODEGROUP,
1536                 constants.TAG_NODE,
1537                 constants.TAG_NETWORK):
1538       if not self.name:
1539         raise http.HttpBadRequest("Missing name on tag request")
1540
1541       cl = self.GetClient(query=True)
1542       tags = list(cl.QueryTags(kind, self.name))
1543
1544     elif kind == constants.TAG_CLUSTER:
1545       assert not self.name
1546       # TODO: Use query API?
1547       ssc = ssconf.SimpleStore()
1548       tags = ssc.GetClusterTags()
1549
1550     else:
1551       raise http.HttpBadRequest("Unhandled tag type!")
1552
1553     return list(tags)
1554
1555   def GetPutOpInput(self):
1556     """Add a set of tags.
1557
1558     The request as a list of strings should be PUT to this URI. And
1559     you'll have back a job id.
1560
1561     """
1562     return ({}, {
1563       "kind": self.TAG_LEVEL,
1564       "name": self.name,
1565       "tags": self.queryargs.get("tag", []),
1566       "dry_run": self.dryRun(),
1567       })
1568
1569   def GetDeleteOpInput(self):
1570     """Delete a tag.
1571
1572     In order to delete a set of tags, the DELETE
1573     request should be addressed to URI like:
1574     /tags?tag=[tag]&tag=[tag]
1575
1576     """
1577     # Re-use code
1578     return self.GetPutOpInput()
1579
1580
1581 class R_2_instances_name_tags(_R_Tags):
1582   """ /2/instances/[instance_name]/tags resource.
1583
1584   Manages per-instance tags.
1585
1586   """
1587   TAG_LEVEL = constants.TAG_INSTANCE
1588
1589
1590 class R_2_nodes_name_tags(_R_Tags):
1591   """ /2/nodes/[node_name]/tags resource.
1592
1593   Manages per-node tags.
1594
1595   """
1596   TAG_LEVEL = constants.TAG_NODE
1597
1598
1599 class R_2_groups_name_tags(_R_Tags):
1600   """ /2/groups/[group_name]/tags resource.
1601
1602   Manages per-nodegroup tags.
1603
1604   """
1605   TAG_LEVEL = constants.TAG_NODEGROUP
1606
1607
1608 class R_2_networks_name_tags(_R_Tags):
1609   """ /2/networks/[network_name]/tags resource.
1610
1611   Manages per-network tags.
1612
1613   """
1614   TAG_LEVEL = constants.TAG_NETWORK
1615
1616
1617 class R_2_tags(_R_Tags):
1618   """ /2/tags resource.
1619
1620   Manages cluster tags.
1621
1622   """
1623   TAG_LEVEL = constants.TAG_CLUSTER