Merge branch 'stable-2.9' into stable-2.10
[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     op_id = {
984       "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
985       }
986     body = objects.FillDict(self.request_body, {
987       "instances": [objects.FillDict(inst, op_id)
988                     for inst in self.request_body["instances"]],
989       })
990
991     return (body, {
992       "dry_run": self.dryRun(),
993       })
994
995
996 class R_2_instances_name(baserlib.OpcodeResource):
997   """/2/instances/[instance_name] resource.
998
999   """
1000   GET_OPCODE = opcodes.OpInstanceQuery
1001   DELETE_OPCODE = opcodes.OpInstanceRemove
1002
1003   def GET(self):
1004     """Send information about an instance.
1005
1006     """
1007     client = self.GetClient()
1008     instance_name = self.items[0]
1009
1010     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1011                                             names=[instance_name],
1012                                             fields=I_FIELDS,
1013                                             use_locking=self.useLocking())
1014
1015     return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1016
1017   def GetDeleteOpInput(self):
1018     """Delete an instance.
1019
1020     """
1021     assert len(self.items) == 1
1022     return (self.request_body, {
1023       "instance_name": self.items[0],
1024       "ignore_failures": False,
1025       "dry_run": self.dryRun(),
1026       })
1027
1028
1029 class R_2_instances_name_info(baserlib.OpcodeResource):
1030   """/2/instances/[instance_name]/info resource.
1031
1032   """
1033   GET_OPCODE = opcodes.OpInstanceQueryData
1034
1035   def GetGetOpInput(self):
1036     """Request detailed instance information.
1037
1038     """
1039     assert len(self.items) == 1
1040     return ({}, {
1041       "instances": [self.items[0]],
1042       "static": bool(self._checkIntVariable("static", default=0)),
1043       })
1044
1045
1046 class R_2_instances_name_reboot(baserlib.OpcodeResource):
1047   """/2/instances/[instance_name]/reboot resource.
1048
1049   Implements an instance reboot.
1050
1051   """
1052   POST_OPCODE = opcodes.OpInstanceReboot
1053
1054   def GetPostOpInput(self):
1055     """Reboot an instance.
1056
1057     The URI takes type=[hard|soft|full] and
1058     ignore_secondaries=[False|True] parameters.
1059
1060     """
1061     return (self.request_body, {
1062       "instance_name": self.items[0],
1063       "reboot_type":
1064         self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1065       "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1066       "dry_run": self.dryRun(),
1067       })
1068
1069
1070 class R_2_instances_name_startup(baserlib.OpcodeResource):
1071   """/2/instances/[instance_name]/startup resource.
1072
1073   Implements an instance startup.
1074
1075   """
1076   PUT_OPCODE = opcodes.OpInstanceStartup
1077
1078   def GetPutOpInput(self):
1079     """Startup an instance.
1080
1081     The URI takes force=[False|True] parameter to start the instance
1082     if even if secondary disks are failing.
1083
1084     """
1085     return ({}, {
1086       "instance_name": self.items[0],
1087       "force": self.useForce(),
1088       "dry_run": self.dryRun(),
1089       "no_remember": bool(self._checkIntVariable("no_remember")),
1090       })
1091
1092
1093 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1094   """/2/instances/[instance_name]/shutdown resource.
1095
1096   Implements an instance shutdown.
1097
1098   """
1099   PUT_OPCODE = opcodes.OpInstanceShutdown
1100
1101   def GetPutOpInput(self):
1102     """Shutdown an instance.
1103
1104     """
1105     return (self.request_body, {
1106       "instance_name": self.items[0],
1107       "no_remember": bool(self._checkIntVariable("no_remember")),
1108       "dry_run": self.dryRun(),
1109       })
1110
1111
1112 def _ParseInstanceReinstallRequest(name, data):
1113   """Parses a request for reinstalling an instance.
1114
1115   """
1116   if not isinstance(data, dict):
1117     raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1118
1119   ostype = baserlib.CheckParameter(data, "os", default=None)
1120   start = baserlib.CheckParameter(data, "start", exptype=bool,
1121                                   default=True)
1122   osparams = baserlib.CheckParameter(data, "osparams", default=None)
1123
1124   ops = [
1125     opcodes.OpInstanceShutdown(instance_name=name),
1126     opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1127                                 osparams=osparams),
1128     ]
1129
1130   if start:
1131     ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1132
1133   return ops
1134
1135
1136 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1137   """/2/instances/[instance_name]/reinstall resource.
1138
1139   Implements an instance reinstall.
1140
1141   """
1142   POST_OPCODE = opcodes.OpInstanceReinstall
1143
1144   def POST(self):
1145     """Reinstall an instance.
1146
1147     The URI takes os=name and nostartup=[0|1] optional
1148     parameters. By default, the instance will be started
1149     automatically.
1150
1151     """
1152     if self.request_body:
1153       if self.queryargs:
1154         raise http.HttpBadRequest("Can't combine query and body parameters")
1155
1156       body = self.request_body
1157     elif self.queryargs:
1158       # Legacy interface, do not modify/extend
1159       body = {
1160         "os": self._checkStringVariable("os"),
1161         "start": not self._checkIntVariable("nostartup"),
1162         }
1163     else:
1164       body = {}
1165
1166     ops = _ParseInstanceReinstallRequest(self.items[0], body)
1167
1168     return self.SubmitJob(ops)
1169
1170
1171 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1172   """/2/instances/[instance_name]/replace-disks resource.
1173
1174   """
1175   POST_OPCODE = opcodes.OpInstanceReplaceDisks
1176
1177   def GetPostOpInput(self):
1178     """Replaces disks on an instance.
1179
1180     """
1181     static = {
1182       "instance_name": self.items[0],
1183       }
1184
1185     if self.request_body:
1186       data = self.request_body
1187     elif self.queryargs:
1188       # Legacy interface, do not modify/extend
1189       data = {
1190         "remote_node": self._checkStringVariable("remote_node", default=None),
1191         "mode": self._checkStringVariable("mode", default=None),
1192         "disks": self._checkStringVariable("disks", default=None),
1193         "iallocator": self._checkStringVariable("iallocator", default=None),
1194         }
1195     else:
1196       data = {}
1197
1198     # Parse disks
1199     try:
1200       raw_disks = data.pop("disks")
1201     except KeyError:
1202       pass
1203     else:
1204       if raw_disks:
1205         if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1206           data["disks"] = raw_disks
1207         else:
1208           # Backwards compatibility for strings of the format "1, 2, 3"
1209           try:
1210             data["disks"] = [int(part) for part in raw_disks.split(",")]
1211           except (TypeError, ValueError), err:
1212             raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1213
1214     return (data, static)
1215
1216
1217 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1218   """/2/instances/[instance_name]/activate-disks resource.
1219
1220   """
1221   PUT_OPCODE = opcodes.OpInstanceActivateDisks
1222
1223   def GetPutOpInput(self):
1224     """Activate disks for an instance.
1225
1226     The URI might contain ignore_size to ignore current recorded size.
1227
1228     """
1229     return ({}, {
1230       "instance_name": self.items[0],
1231       "ignore_size": bool(self._checkIntVariable("ignore_size")),
1232       })
1233
1234
1235 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1236   """/2/instances/[instance_name]/deactivate-disks resource.
1237
1238   """
1239   PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1240
1241   def GetPutOpInput(self):
1242     """Deactivate disks for an instance.
1243
1244     """
1245     return ({}, {
1246       "instance_name": self.items[0],
1247       })
1248
1249
1250 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1251   """/2/instances/[instance_name]/recreate-disks resource.
1252
1253   """
1254   POST_OPCODE = opcodes.OpInstanceRecreateDisks
1255
1256   def GetPostOpInput(self):
1257     """Recreate disks for an instance.
1258
1259     """
1260     return ({}, {
1261       "instance_name": self.items[0],
1262       })
1263
1264
1265 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1266   """/2/instances/[instance_name]/prepare-export resource.
1267
1268   """
1269   PUT_OPCODE = opcodes.OpBackupPrepare
1270
1271   def GetPutOpInput(self):
1272     """Prepares an export for an instance.
1273
1274     """
1275     return ({}, {
1276       "instance_name": self.items[0],
1277       "mode": self._checkStringVariable("mode"),
1278       })
1279
1280
1281 class R_2_instances_name_export(baserlib.OpcodeResource):
1282   """/2/instances/[instance_name]/export resource.
1283
1284   """
1285   PUT_OPCODE = opcodes.OpBackupExport
1286   PUT_RENAME = {
1287     "destination": "target_node",
1288     }
1289
1290   def GetPutOpInput(self):
1291     """Exports an instance.
1292
1293     """
1294     return (self.request_body, {
1295       "instance_name": self.items[0],
1296       })
1297
1298
1299 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1300   """/2/instances/[instance_name]/migrate resource.
1301
1302   """
1303   PUT_OPCODE = opcodes.OpInstanceMigrate
1304
1305   def GetPutOpInput(self):
1306     """Migrates an instance.
1307
1308     """
1309     return (self.request_body, {
1310       "instance_name": self.items[0],
1311       })
1312
1313
1314 class R_2_instances_name_failover(baserlib.OpcodeResource):
1315   """/2/instances/[instance_name]/failover resource.
1316
1317   """
1318   PUT_OPCODE = opcodes.OpInstanceFailover
1319
1320   def GetPutOpInput(self):
1321     """Does a failover of an instance.
1322
1323     """
1324     return (self.request_body, {
1325       "instance_name": self.items[0],
1326       })
1327
1328
1329 class R_2_instances_name_rename(baserlib.OpcodeResource):
1330   """/2/instances/[instance_name]/rename resource.
1331
1332   """
1333   PUT_OPCODE = opcodes.OpInstanceRename
1334
1335   def GetPutOpInput(self):
1336     """Changes the name of an instance.
1337
1338     """
1339     return (self.request_body, {
1340       "instance_name": self.items[0],
1341       })
1342
1343
1344 class R_2_instances_name_modify(baserlib.OpcodeResource):
1345   """/2/instances/[instance_name]/modify resource.
1346
1347   """
1348   PUT_OPCODE = opcodes.OpInstanceSetParams
1349
1350   def GetPutOpInput(self):
1351     """Changes parameters of an instance.
1352
1353     """
1354     data = self.request_body.copy()
1355     _ConvertUsbDevices(data)
1356
1357     return (data, {
1358       "instance_name": self.items[0],
1359       })
1360
1361
1362 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1363   """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1364
1365   """
1366   POST_OPCODE = opcodes.OpInstanceGrowDisk
1367
1368   def GetPostOpInput(self):
1369     """Increases the size of an instance disk.
1370
1371     """
1372     return (self.request_body, {
1373       "instance_name": self.items[0],
1374       "disk": int(self.items[1]),
1375       })
1376
1377
1378 class R_2_instances_name_console(baserlib.ResourceBase):
1379   """/2/instances/[instance_name]/console resource.
1380
1381   """
1382   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1383   GET_OPCODE = opcodes.OpInstanceConsole
1384
1385   def GET(self):
1386     """Request information for connecting to instance's console.
1387
1388     @return: Serialized instance console description, see
1389              L{objects.InstanceConsole}
1390
1391     """
1392     client = self.GetClient()
1393
1394     ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1395
1396     if console is None:
1397       raise http.HttpServiceUnavailable("Instance console unavailable")
1398
1399     assert isinstance(console, dict)
1400     return console
1401
1402
1403 def _GetQueryFields(args):
1404   """Tries to extract C{fields} query parameter.
1405
1406   @type args: dictionary
1407   @rtype: list of string
1408   @raise http.HttpBadRequest: When parameter can't be found
1409
1410   """
1411   try:
1412     fields = args["fields"]
1413   except KeyError:
1414     raise http.HttpBadRequest("Missing 'fields' query argument")
1415
1416   return _SplitQueryFields(fields[0])
1417
1418
1419 def _SplitQueryFields(fields):
1420   """Splits fields as given for a query request.
1421
1422   @type fields: string
1423   @rtype: list of string
1424
1425   """
1426   return [i.strip() for i in fields.split(",")]
1427
1428
1429 class R_2_query(baserlib.ResourceBase):
1430   """/2/query/[resource] resource.
1431
1432   """
1433   # Results might contain sensitive information
1434   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1435   PUT_ACCESS = GET_ACCESS
1436   GET_OPCODE = opcodes.OpQuery
1437   PUT_OPCODE = opcodes.OpQuery
1438
1439   def _Query(self, fields, qfilter):
1440     return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1441
1442   def GET(self):
1443     """Returns resource information.
1444
1445     @return: Query result, see L{objects.QueryResponse}
1446
1447     """
1448     return self._Query(_GetQueryFields(self.queryargs), None)
1449
1450   def PUT(self):
1451     """Submits job querying for resources.
1452
1453     @return: Query result, see L{objects.QueryResponse}
1454
1455     """
1456     body = self.request_body
1457
1458     baserlib.CheckType(body, dict, "Body contents")
1459
1460     try:
1461       fields = body["fields"]
1462     except KeyError:
1463       fields = _GetQueryFields(self.queryargs)
1464
1465     qfilter = body.get("qfilter", None)
1466     # TODO: remove this after 2.7
1467     if qfilter is None:
1468       qfilter = body.get("filter", None)
1469
1470     return self._Query(fields, qfilter)
1471
1472
1473 class R_2_query_fields(baserlib.ResourceBase):
1474   """/2/query/[resource]/fields resource.
1475
1476   """
1477   GET_OPCODE = opcodes.OpQueryFields
1478
1479   def GET(self):
1480     """Retrieves list of available fields for a resource.
1481
1482     @return: List of serialized L{objects.QueryFieldDefinition}
1483
1484     """
1485     try:
1486       raw_fields = self.queryargs["fields"]
1487     except KeyError:
1488       fields = None
1489     else:
1490       fields = _SplitQueryFields(raw_fields[0])
1491
1492     return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1493
1494
1495 class _R_Tags(baserlib.OpcodeResource):
1496   """Quasiclass for tagging resources.
1497
1498   Manages tags. When inheriting this class you must define the
1499   TAG_LEVEL for it.
1500
1501   """
1502   TAG_LEVEL = None
1503   GET_OPCODE = opcodes.OpTagsGet
1504   PUT_OPCODE = opcodes.OpTagsSet
1505   DELETE_OPCODE = opcodes.OpTagsDel
1506
1507   def __init__(self, items, queryargs, req, **kwargs):
1508     """A tag resource constructor.
1509
1510     We have to override the default to sort out cluster naming case.
1511
1512     """
1513     baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1514
1515     if self.TAG_LEVEL == constants.TAG_CLUSTER:
1516       self.name = None
1517     else:
1518       self.name = items[0]
1519
1520   def GET(self):
1521     """Returns a list of tags.
1522
1523     Example: ["tag1", "tag2", "tag3"]
1524
1525     """
1526     kind = self.TAG_LEVEL
1527
1528     if kind in (constants.TAG_INSTANCE,
1529                 constants.TAG_NODEGROUP,
1530                 constants.TAG_NODE,
1531                 constants.TAG_NETWORK):
1532       if not self.name:
1533         raise http.HttpBadRequest("Missing name on tag request")
1534
1535       cl = self.GetClient(query=True)
1536       tags = list(cl.QueryTags(kind, self.name))
1537
1538     elif kind == constants.TAG_CLUSTER:
1539       assert not self.name
1540       # TODO: Use query API?
1541       ssc = ssconf.SimpleStore()
1542       tags = ssc.GetClusterTags()
1543
1544     else:
1545       raise http.HttpBadRequest("Unhandled tag type!")
1546
1547     return list(tags)
1548
1549   def GetPutOpInput(self):
1550     """Add a set of tags.
1551
1552     The request as a list of strings should be PUT to this URI. And
1553     you'll have back a job id.
1554
1555     """
1556     return ({}, {
1557       "kind": self.TAG_LEVEL,
1558       "name": self.name,
1559       "tags": self.queryargs.get("tag", []),
1560       "dry_run": self.dryRun(),
1561       })
1562
1563   def GetDeleteOpInput(self):
1564     """Delete a tag.
1565
1566     In order to delete a set of tags, the DELETE
1567     request should be addressed to URI like:
1568     /tags?tag=[tag]&tag=[tag]
1569
1570     """
1571     # Re-use code
1572     return self.GetPutOpInput()
1573
1574
1575 class R_2_instances_name_tags(_R_Tags):
1576   """ /2/instances/[instance_name]/tags resource.
1577
1578   Manages per-instance tags.
1579
1580   """
1581   TAG_LEVEL = constants.TAG_INSTANCE
1582
1583
1584 class R_2_nodes_name_tags(_R_Tags):
1585   """ /2/nodes/[node_name]/tags resource.
1586
1587   Manages per-node tags.
1588
1589   """
1590   TAG_LEVEL = constants.TAG_NODE
1591
1592
1593 class R_2_groups_name_tags(_R_Tags):
1594   """ /2/groups/[group_name]/tags resource.
1595
1596   Manages per-nodegroup tags.
1597
1598   """
1599   TAG_LEVEL = constants.TAG_NODEGROUP
1600
1601
1602 class R_2_networks_name_tags(_R_Tags):
1603   """ /2/networks/[network_name]/tags resource.
1604
1605   Manages per-network tags.
1606
1607   """
1608   TAG_LEVEL = constants.TAG_NETWORK
1609
1610
1611 class R_2_tags(_R_Tags):
1612   """ /2/tags resource.
1613
1614   Manages cluster tags.
1615
1616   """
1617   TAG_LEVEL = constants.TAG_CLUSTER