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