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