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