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