4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
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.
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.
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
22 """Remote API resource implementations.
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.
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.
35 Quoting from RFC2616, section 9.6::
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
49 So when adding new methods, if they are operating on the URI entity itself,
50 PUT should be prefered over POST.
54 # pylint: disable=C0103
56 # C0103: Invalid name, since the R_* names are not conforming
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
64 from ganeti import compat
65 from ganeti import ssconf
66 from ganeti.rapi import baserlib
69 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
70 I_FIELDS = ["name", "admin_state", "os",
73 "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
75 "disk.sizes", "disk_usage",
76 "beparams", "hvparams",
77 "oper_state", "oper_ram", "oper_vcpus", "status",
78 "custom_hvparams", "custom_beparams", "custom_nicparams",
81 N_FIELDS = ["name", "offline", "master_candidate", "drained",
83 "mtotal", "mnode", "mfree",
84 "pinst_cnt", "sinst_cnt",
85 "ctotal", "cnodes", "csockets",
87 "pinst_list", "sinst_list",
88 "master_capable", "vm_capable",
102 "id", "ops", "status", "summary",
104 "received_ts", "start_ts", "end_ts",
107 J_FIELDS = J_FIELDS_BULK + [
112 _NR_DRAINED = "drained"
113 _NR_MASTER_CANDIDATE = "master-candidate"
114 _NR_MASTER = "master"
115 _NR_OFFLINE = "offline"
116 _NR_REGULAR = "regular"
119 constants.NR_MASTER: _NR_MASTER,
120 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
121 constants.NR_DRAINED: _NR_DRAINED,
122 constants.NR_OFFLINE: _NR_OFFLINE,
123 constants.NR_REGULAR: _NR_REGULAR,
126 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
128 # Request data version field
129 _REQ_DATA_VERSION = "__version__"
131 # Feature string for instance creation request data version 1
132 _INST_CREATE_REQV1 = "instance-create-reqv1"
134 # Feature string for instance reinstall request version 1
135 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
137 # Feature string for node migration version 1
138 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
140 # Feature string for node evacuation with LU-generated jobs
141 _NODE_EVAC_RES1 = "node-evac-res1"
143 ALL_FEATURES = frozenset([
145 _INST_REINSTALL_REQV1,
150 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
154 class R_root(baserlib.ResourceBase):
160 """Supported for legacy reasons.
172 class R_version(baserlib.ResourceBase):
173 """/version resource.
175 This resource should be used to determine the remote API version and
176 to adapt clients accordingly.
181 """Returns the remote API version.
184 return constants.RAPI_VERSION
187 class R_2_info(baserlib.OpcodeResource):
191 GET_OPCODE = opcodes.OpClusterQuery
194 """Returns cluster information.
197 client = self.GetClient()
198 return client.QueryClusterInfo()
201 class R_2_features(baserlib.ResourceBase):
202 """/2/features resource.
207 """Returns list of optional RAPI features implemented.
210 return list(ALL_FEATURES)
213 class R_2_os(baserlib.OpcodeResource):
217 GET_OPCODE = opcodes.OpOsDiagnose
220 """Return a list of all OSes.
222 Can return error 500 in case of a problem.
224 Example: ["debian-etch"]
227 cl = self.GetClient()
228 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
229 job_id = self.SubmitJob([op], cl=cl)
230 # we use custom feedback function, instead of print we log the status
231 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
232 diagnose_data = result[0]
234 if not isinstance(diagnose_data, list):
235 raise http.HttpBadGateway(message="Can't get OS list")
238 for (name, variants) in diagnose_data:
239 os_names.extend(cli.CalculateOSNames(name, variants))
244 class R_2_redist_config(baserlib.OpcodeResource):
245 """/2/redistribute-config resource.
248 PUT_OPCODE = opcodes.OpClusterRedistConf
251 class R_2_cluster_modify(baserlib.OpcodeResource):
252 """/2/modify resource.
255 PUT_OPCODE = opcodes.OpClusterSetParams
258 class R_2_jobs(baserlib.ResourceBase):
263 """Returns a dictionary of jobs.
265 @return: a dictionary with jobs id and uri.
268 client = self.GetClient()
271 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
272 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
274 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
275 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
276 uri_fields=("id", "uri"))
279 class R_2_jobs_id(baserlib.ResourceBase):
280 """/2/jobs/[job_id] resource.
284 """Returns a job status.
286 @return: a dictionary with job parameters.
288 - id: job ID as a number
289 - status: current job status as a string
290 - ops: involved OpCodes as a list of dictionaries for each
292 - opstatus: OpCodes status as a list
293 - opresult: OpCodes results as a list of lists
296 job_id = self.items[0]
297 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
299 raise http.HttpNotFound()
300 return baserlib.MapFields(J_FIELDS, result)
303 """Cancel not-yet-started job.
306 job_id = self.items[0]
307 result = self.GetClient().CancelJob(job_id)
311 class R_2_jobs_id_wait(baserlib.ResourceBase):
312 """/2/jobs/[job_id]/wait resource.
315 # WaitForJobChange provides access to sensitive information and blocks
316 # machine resources (it's a blocking RAPI call), hence restricting access.
317 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
320 """Waits for job changes.
323 job_id = self.items[0]
325 fields = self.getBodyParameter("fields")
326 prev_job_info = self.getBodyParameter("previous_job_info", None)
327 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
329 if not isinstance(fields, list):
330 raise http.HttpBadRequest("The 'fields' parameter should be a list")
332 if not (prev_job_info is None or isinstance(prev_job_info, list)):
333 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
336 if not (prev_log_serial is None or
337 isinstance(prev_log_serial, (int, long))):
338 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
341 client = self.GetClient()
342 result = client.WaitForJobChangeOnce(job_id, fields,
343 prev_job_info, prev_log_serial,
344 timeout=_WFJC_TIMEOUT)
346 raise http.HttpNotFound()
348 if result == constants.JOB_NOTCHANGED:
352 (job_info, log_entries) = result
355 "job_info": job_info,
356 "log_entries": log_entries,
360 class R_2_nodes(baserlib.OpcodeResource):
361 """/2/nodes resource.
364 GET_OPCODE = opcodes.OpNodeQuery
367 """Returns a list of all nodes.
370 client = self.GetClient()
373 bulkdata = client.QueryNodes([], N_FIELDS, False)
374 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
376 nodesdata = client.QueryNodes([], ["name"], False)
377 nodeslist = [row[0] for row in nodesdata]
378 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
379 uri_fields=("id", "uri"))
382 class R_2_nodes_name(baserlib.OpcodeResource):
383 """/2/nodes/[node_name] resource.
386 GET_OPCODE = opcodes.OpNodeQuery
389 """Send information about a node.
392 node_name = self.items[0]
393 client = self.GetClient()
395 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
396 names=[node_name], fields=N_FIELDS,
397 use_locking=self.useLocking())
399 return baserlib.MapFields(N_FIELDS, result[0])
402 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
403 """/2/nodes/[node_name]/powercycle resource.
406 POST_OPCODE = opcodes.OpNodePowercycle
408 def GetPostOpInput(self):
409 """Tries to powercycle a node.
412 return (self.request_body, {
413 "node_name": self.items[0],
414 "force": self.useForce(),
418 class R_2_nodes_name_role(baserlib.OpcodeResource):
419 """/2/nodes/[node_name]/role resource.
422 PUT_OPCODE = opcodes.OpNodeSetParams
425 """Returns the current node role.
430 node_name = self.items[0]
431 client = self.GetClient()
432 result = client.QueryNodes(names=[node_name], fields=["role"],
433 use_locking=self.useLocking())
435 return _NR_MAP[result[0][0]]
437 def GetPutOpInput(self):
438 """Sets the node role.
441 baserlib.CheckType(self.request_body, basestring, "Body contents")
443 role = self.request_body
445 if role == _NR_REGULAR:
450 elif role == _NR_MASTER_CANDIDATE:
452 offline = drained = None
454 elif role == _NR_DRAINED:
456 candidate = offline = None
458 elif role == _NR_OFFLINE:
460 candidate = drained = None
463 raise http.HttpBadRequest("Can't set '%s' role" % role)
465 assert len(self.items) == 1
468 "node_name": self.items[0],
469 "master_candidate": candidate,
472 "force": self.useForce(),
473 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
477 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
478 """/2/nodes/[node_name]/evacuate resource.
481 POST_OPCODE = opcodes.OpNodeEvacuate
483 def GetPostOpInput(self):
484 """Evacuate all instances off a node.
487 return (self.request_body, {
488 "node_name": self.items[0],
489 "dry_run": self.dryRun(),
493 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
494 """/2/nodes/[node_name]/migrate resource.
497 POST_OPCODE = opcodes.OpNodeMigrate
499 def GetPostOpInput(self):
500 """Migrate all primary instances from a node.
504 # Support old-style requests
505 if "live" in self.queryargs and "mode" in self.queryargs:
506 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
509 if "live" in self.queryargs:
510 if self._checkIntVariable("live", default=1):
511 mode = constants.HT_MIGRATION_LIVE
513 mode = constants.HT_MIGRATION_NONLIVE
515 mode = self._checkStringVariable("mode", default=None)
521 data = self.request_body
524 "node_name": self.items[0],
528 class R_2_nodes_name_modify(baserlib.OpcodeResource):
529 """/2/nodes/[node_name]/modify resource.
532 POST_OPCODE = opcodes.OpNodeSetParams
534 def GetPostOpInput(self):
535 """Changes parameters of a node.
538 assert len(self.items) == 1
540 return (self.request_body, {
541 "node_name": self.items[0],
545 class R_2_nodes_name_storage(baserlib.OpcodeResource):
546 """/2/nodes/[node_name]/storage resource.
549 # LUNodeQueryStorage acquires locks, hence restricting access to GET
550 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
551 GET_OPCODE = opcodes.OpNodeQueryStorage
553 def GetGetOpInput(self):
554 """List storage available on a node.
557 storage_type = self._checkStringVariable("storage_type", None)
558 output_fields = self._checkStringVariable("output_fields", None)
560 if not output_fields:
561 raise http.HttpBadRequest("Missing the required 'output_fields'"
565 "nodes": [self.items[0]],
566 "storage_type": storage_type,
567 "output_fields": output_fields.split(","),
571 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
572 """/2/nodes/[node_name]/storage/modify resource.
575 PUT_OPCODE = opcodes.OpNodeModifyStorage
577 def GetPutOpInput(self):
578 """Modifies a storage volume on a node.
581 storage_type = self._checkStringVariable("storage_type", None)
582 name = self._checkStringVariable("name", None)
585 raise http.HttpBadRequest("Missing the required 'name'"
590 if "allocatable" in self.queryargs:
591 changes[constants.SF_ALLOCATABLE] = \
592 bool(self._checkIntVariable("allocatable", default=1))
595 "node_name": self.items[0],
596 "storage_type": storage_type,
602 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
603 """/2/nodes/[node_name]/storage/repair resource.
606 PUT_OPCODE = opcodes.OpRepairNodeStorage
608 def GetPutOpInput(self):
609 """Repairs a storage volume on a node.
612 storage_type = self._checkStringVariable("storage_type", None)
613 name = self._checkStringVariable("name", None)
615 raise http.HttpBadRequest("Missing the required 'name'"
619 "node_name": self.items[0],
620 "storage_type": storage_type,
625 class R_2_groups(baserlib.OpcodeResource):
626 """/2/groups resource.
629 GET_OPCODE = opcodes.OpGroupQuery
630 POST_OPCODE = opcodes.OpGroupAdd
632 "name": "group_name",
635 def GetPostOpInput(self):
636 """Create a node group.
639 assert not self.items
640 return (self.request_body, {
641 "dry_run": self.dryRun(),
645 """Returns a list of all node groups.
648 client = self.GetClient()
651 bulkdata = client.QueryGroups([], G_FIELDS, False)
652 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
654 data = client.QueryGroups([], ["name"], False)
655 groupnames = [row[0] for row in data]
656 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
657 uri_fields=("name", "uri"))
660 class R_2_groups_name(baserlib.OpcodeResource):
661 """/2/groups/[group_name] resource.
664 DELETE_OPCODE = opcodes.OpGroupRemove
667 """Send information about a node group.
670 group_name = self.items[0]
671 client = self.GetClient()
673 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
674 names=[group_name], fields=G_FIELDS,
675 use_locking=self.useLocking())
677 return baserlib.MapFields(G_FIELDS, result[0])
679 def GetDeleteOpInput(self):
680 """Delete a node group.
683 assert len(self.items) == 1
685 "group_name": self.items[0],
686 "dry_run": self.dryRun(),
690 class R_2_groups_name_modify(baserlib.OpcodeResource):
691 """/2/groups/[group_name]/modify resource.
694 PUT_OPCODE = opcodes.OpGroupSetParams
696 def GetPutOpInput(self):
697 """Changes some parameters of node group.
701 return (self.request_body, {
702 "group_name": self.items[0],
706 class R_2_groups_name_rename(baserlib.OpcodeResource):
707 """/2/groups/[group_name]/rename resource.
710 PUT_OPCODE = opcodes.OpGroupRename
712 def GetPutOpInput(self):
713 """Changes the name of a node group.
716 assert len(self.items) == 1
717 return (self.request_body, {
718 "group_name": self.items[0],
719 "dry_run": self.dryRun(),
723 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
724 """/2/groups/[group_name]/assign-nodes resource.
727 PUT_OPCODE = opcodes.OpGroupAssignNodes
729 def GetPutOpInput(self):
730 """Assigns nodes to a group.
733 assert len(self.items) == 1
734 return (self.request_body, {
735 "group_name": self.items[0],
736 "dry_run": self.dryRun(),
737 "force": self.useForce(),
741 class R_2_instances(baserlib.OpcodeResource):
742 """/2/instances resource.
745 GET_OPCODE = opcodes.OpInstanceQuery
746 POST_OPCODE = opcodes.OpInstanceCreate
749 "name": "instance_name",
753 """Returns a list of all available instances.
756 client = self.GetClient()
758 use_locking = self.useLocking()
760 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
761 return baserlib.MapBulkFields(bulkdata, I_FIELDS)
763 instancesdata = client.QueryInstances([], ["name"], use_locking)
764 instanceslist = [row[0] for row in instancesdata]
765 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
766 uri_fields=("id", "uri"))
768 def GetPostOpInput(self):
769 """Create an instance.
774 baserlib.CheckType(self.request_body, dict, "Body contents")
776 # Default to request data version 0
777 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
779 if data_version == 0:
780 raise http.HttpBadRequest("Instance creation request version 0 is no"
782 elif data_version != 1:
783 raise http.HttpBadRequest("Unsupported request data version %s" %
786 data = self.request_body.copy()
787 # Remove "__version__"
788 data.pop(_REQ_DATA_VERSION, None)
791 "dry_run": self.dryRun(),
795 class R_2_instances_name(baserlib.OpcodeResource):
796 """/2/instances/[instance_name] resource.
799 GET_OPCODE = opcodes.OpInstanceQuery
800 DELETE_OPCODE = opcodes.OpInstanceRemove
803 """Send information about an instance.
806 client = self.GetClient()
807 instance_name = self.items[0]
809 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
810 names=[instance_name],
812 use_locking=self.useLocking())
814 return baserlib.MapFields(I_FIELDS, result[0])
816 def GetDeleteOpInput(self):
817 """Delete an instance.
820 assert len(self.items) == 1
822 "instance_name": self.items[0],
823 "ignore_failures": False,
824 "dry_run": self.dryRun(),
828 class R_2_instances_name_info(baserlib.OpcodeResource):
829 """/2/instances/[instance_name]/info resource.
832 GET_OPCODE = opcodes.OpInstanceQueryData
834 def GetGetOpInput(self):
835 """Request detailed instance information.
838 assert len(self.items) == 1
840 "instances": [self.items[0]],
841 "static": bool(self._checkIntVariable("static", default=0)),
845 class R_2_instances_name_reboot(baserlib.OpcodeResource):
846 """/2/instances/[instance_name]/reboot resource.
848 Implements an instance reboot.
851 POST_OPCODE = opcodes.OpInstanceReboot
853 def GetPostOpInput(self):
854 """Reboot an instance.
856 The URI takes type=[hard|soft|full] and
857 ignore_secondaries=[False|True] parameters.
861 "instance_name": self.items[0],
863 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
864 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
865 "dry_run": self.dryRun(),
869 class R_2_instances_name_startup(baserlib.OpcodeResource):
870 """/2/instances/[instance_name]/startup resource.
872 Implements an instance startup.
875 PUT_OPCODE = opcodes.OpInstanceStartup
877 def GetPutOpInput(self):
878 """Startup an instance.
880 The URI takes force=[False|True] parameter to start the instance
881 if even if secondary disks are failing.
885 "instance_name": self.items[0],
886 "force": self.useForce(),
887 "dry_run": self.dryRun(),
888 "no_remember": bool(self._checkIntVariable("no_remember")),
892 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
893 """/2/instances/[instance_name]/shutdown resource.
895 Implements an instance shutdown.
898 PUT_OPCODE = opcodes.OpInstanceShutdown
900 def GetPutOpInput(self):
901 """Shutdown an instance.
904 return (self.request_body, {
905 "instance_name": self.items[0],
906 "no_remember": bool(self._checkIntVariable("no_remember")),
907 "dry_run": self.dryRun(),
911 def _ParseInstanceReinstallRequest(name, data):
912 """Parses a request for reinstalling an instance.
915 if not isinstance(data, dict):
916 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
918 ostype = baserlib.CheckParameter(data, "os", default=None)
919 start = baserlib.CheckParameter(data, "start", exptype=bool,
921 osparams = baserlib.CheckParameter(data, "osparams", default=None)
924 opcodes.OpInstanceShutdown(instance_name=name),
925 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
930 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
935 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
936 """/2/instances/[instance_name]/reinstall resource.
938 Implements an instance reinstall.
941 POST_OPCODE = opcodes.OpInstanceReinstall
944 """Reinstall an instance.
946 The URI takes os=name and nostartup=[0|1] optional
947 parameters. By default, the instance will be started
951 if self.request_body:
953 raise http.HttpBadRequest("Can't combine query and body parameters")
955 body = self.request_body
957 # Legacy interface, do not modify/extend
959 "os": self._checkStringVariable("os"),
960 "start": not self._checkIntVariable("nostartup"),
965 ops = _ParseInstanceReinstallRequest(self.items[0], body)
967 return self.SubmitJob(ops)
970 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
971 """/2/instances/[instance_name]/replace-disks resource.
974 POST_OPCODE = opcodes.OpInstanceReplaceDisks
976 def GetPostOpInput(self):
977 """Replaces disks on an instance.
981 "instance_name": self.items[0],
984 if self.request_body:
985 data = self.request_body
987 # Legacy interface, do not modify/extend
989 "remote_node": self._checkStringVariable("remote_node", default=None),
990 "mode": self._checkStringVariable("mode", default=None),
991 "disks": self._checkStringVariable("disks", default=None),
992 "iallocator": self._checkStringVariable("iallocator", default=None),
999 raw_disks = data.pop("disks")
1004 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1005 data["disks"] = raw_disks
1007 # Backwards compatibility for strings of the format "1, 2, 3"
1009 data["disks"] = [int(part) for part in raw_disks.split(",")]
1010 except (TypeError, ValueError), err:
1011 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1013 return (data, static)
1016 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1017 """/2/instances/[instance_name]/activate-disks resource.
1020 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1022 def GetPutOpInput(self):
1023 """Activate disks for an instance.
1025 The URI might contain ignore_size to ignore current recorded size.
1029 "instance_name": self.items[0],
1030 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1034 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1035 """/2/instances/[instance_name]/deactivate-disks resource.
1038 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1040 def GetPutOpInput(self):
1041 """Deactivate disks for an instance.
1045 "instance_name": self.items[0],
1049 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1050 """/2/instances/[instance_name]/recreate-disks resource.
1053 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1055 def GetPostOpInput(self):
1056 """Recreate disks for an instance.
1060 "instance_name": self.items[0],
1064 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1065 """/2/instances/[instance_name]/prepare-export resource.
1068 PUT_OPCODE = opcodes.OpBackupPrepare
1070 def GetPutOpInput(self):
1071 """Prepares an export for an instance.
1075 "instance_name": self.items[0],
1076 "mode": self._checkStringVariable("mode"),
1080 class R_2_instances_name_export(baserlib.OpcodeResource):
1081 """/2/instances/[instance_name]/export resource.
1084 PUT_OPCODE = opcodes.OpBackupExport
1086 "destination": "target_node",
1089 def GetPutOpInput(self):
1090 """Exports an instance.
1093 return (self.request_body, {
1094 "instance_name": self.items[0],
1098 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1099 """/2/instances/[instance_name]/migrate resource.
1102 PUT_OPCODE = opcodes.OpInstanceMigrate
1104 def GetPutOpInput(self):
1105 """Migrates an instance.
1108 return (self.request_body, {
1109 "instance_name": self.items[0],
1113 class R_2_instances_name_failover(baserlib.OpcodeResource):
1114 """/2/instances/[instance_name]/failover resource.
1117 PUT_OPCODE = opcodes.OpInstanceFailover
1119 def GetPutOpInput(self):
1120 """Does a failover of an instance.
1123 return (self.request_body, {
1124 "instance_name": self.items[0],
1128 class R_2_instances_name_rename(baserlib.OpcodeResource):
1129 """/2/instances/[instance_name]/rename resource.
1132 PUT_OPCODE = opcodes.OpInstanceRename
1134 def GetPutOpInput(self):
1135 """Changes the name of an instance.
1138 return (self.request_body, {
1139 "instance_name": self.items[0],
1143 class R_2_instances_name_modify(baserlib.OpcodeResource):
1144 """/2/instances/[instance_name]/modify resource.
1147 PUT_OPCODE = opcodes.OpInstanceSetParams
1149 def GetPutOpInput(self):
1150 """Changes parameters of an instance.
1153 return (self.request_body, {
1154 "instance_name": self.items[0],
1158 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1159 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1162 POST_OPCODE = opcodes.OpInstanceGrowDisk
1164 def GetPostOpInput(self):
1165 """Increases the size of an instance disk.
1168 return (self.request_body, {
1169 "instance_name": self.items[0],
1170 "disk": int(self.items[1]),
1174 class R_2_instances_name_console(baserlib.ResourceBase):
1175 """/2/instances/[instance_name]/console resource.
1178 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1179 GET_OPCODE = opcodes.OpInstanceConsole
1182 """Request information for connecting to instance's console.
1184 @return: Serialized instance console description, see
1185 L{objects.InstanceConsole}
1188 client = self.GetClient()
1190 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1193 raise http.HttpServiceUnavailable("Instance console unavailable")
1195 assert isinstance(console, dict)
1199 def _GetQueryFields(args):
1204 fields = args["fields"]
1206 raise http.HttpBadRequest("Missing 'fields' query argument")
1208 return _SplitQueryFields(fields[0])
1211 def _SplitQueryFields(fields):
1215 return [i.strip() for i in fields.split(",")]
1218 class R_2_query(baserlib.ResourceBase):
1219 """/2/query/[resource] resource.
1222 # Results might contain sensitive information
1223 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1224 GET_OPCODE = opcodes.OpQuery
1225 PUT_OPCODE = opcodes.OpQuery
1227 def _Query(self, fields, qfilter):
1228 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1231 """Returns resource information.
1233 @return: Query result, see L{objects.QueryResponse}
1236 return self._Query(_GetQueryFields(self.queryargs), None)
1239 """Submits job querying for resources.
1241 @return: Query result, see L{objects.QueryResponse}
1244 body = self.request_body
1246 baserlib.CheckType(body, dict, "Body contents")
1249 fields = body["fields"]
1251 fields = _GetQueryFields(self.queryargs)
1253 qfilter = body.get("qfilter", None)
1254 # TODO: remove this after 2.7
1256 qfilter = body.get("filter", None)
1258 return self._Query(fields, qfilter)
1261 class R_2_query_fields(baserlib.ResourceBase):
1262 """/2/query/[resource]/fields resource.
1265 GET_OPCODE = opcodes.OpQueryFields
1268 """Retrieves list of available fields for a resource.
1270 @return: List of serialized L{objects.QueryFieldDefinition}
1274 raw_fields = self.queryargs["fields"]
1278 fields = _SplitQueryFields(raw_fields[0])
1280 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1283 class _R_Tags(baserlib.OpcodeResource):
1284 """Quasiclass for tagging resources.
1286 Manages tags. When inheriting this class you must define the
1291 GET_OPCODE = opcodes.OpTagsGet
1292 PUT_OPCODE = opcodes.OpTagsSet
1293 DELETE_OPCODE = opcodes.OpTagsDel
1295 def __init__(self, items, queryargs, req, **kwargs):
1296 """A tag resource constructor.
1298 We have to override the default to sort out cluster naming case.
1301 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1303 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1306 self.name = items[0]
1309 """Returns a list of tags.
1311 Example: ["tag1", "tag2", "tag3"]
1314 kind = self.TAG_LEVEL
1316 if kind in (constants.TAG_INSTANCE,
1317 constants.TAG_NODEGROUP,
1318 constants.TAG_NODE):
1320 raise http.HttpBadRequest("Missing name on tag request")
1322 cl = self.GetClient()
1323 if kind == constants.TAG_INSTANCE:
1324 fn = cl.QueryInstances
1325 elif kind == constants.TAG_NODEGROUP:
1329 result = fn(names=[self.name], fields=["tags"], use_locking=False)
1330 if not result or not result[0]:
1331 raise http.HttpBadGateway("Invalid response from tag query")
1334 elif kind == constants.TAG_CLUSTER:
1335 assert not self.name
1336 # TODO: Use query API?
1337 ssc = ssconf.SimpleStore()
1338 tags = ssc.GetClusterTags()
1342 def GetPutOpInput(self):
1343 """Add a set of tags.
1345 The request as a list of strings should be PUT to this URI. And
1346 you'll have back a job id.
1350 "kind": self.TAG_LEVEL,
1352 "tags": self.queryargs.get("tag", []),
1353 "dry_run": self.dryRun(),
1356 def GetDeleteOpInput(self):
1359 In order to delete a set of tags, the DELETE
1360 request should be addressed to URI like:
1361 /tags?tag=[tag]&tag=[tag]
1365 return self.GetPutOpInput()
1368 class R_2_instances_name_tags(_R_Tags):
1369 """ /2/instances/[instance_name]/tags resource.
1371 Manages per-instance tags.
1374 TAG_LEVEL = constants.TAG_INSTANCE
1377 class R_2_nodes_name_tags(_R_Tags):
1378 """ /2/nodes/[node_name]/tags resource.
1380 Manages per-node tags.
1383 TAG_LEVEL = constants.TAG_NODE
1386 class R_2_groups_name_tags(_R_Tags):
1387 """ /2/groups/[group_name]/tags resource.
1389 Manages per-nodegroup tags.
1392 TAG_LEVEL = constants.TAG_NODEGROUP
1395 class R_2_tags(_R_Tags):
1396 """ /2/tags resource.
1398 Manages cluster tags.
1401 TAG_LEVEL = constants.TAG_CLUSTER