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