4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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",
101 "id", "ops", "status", "summary",
103 "received_ts", "start_ts", "end_ts",
106 J_FIELDS = J_FIELDS_BULK + [
111 _NR_DRAINED = "drained"
112 _NR_MASTER_CANDIDATE = "master-candidate"
113 _NR_MASTER = "master"
114 _NR_OFFLINE = "offline"
115 _NR_REGULAR = "regular"
118 constants.NR_MASTER: _NR_MASTER,
119 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
120 constants.NR_DRAINED: _NR_DRAINED,
121 constants.NR_OFFLINE: _NR_OFFLINE,
122 constants.NR_REGULAR: _NR_REGULAR,
125 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
127 # Request data version field
128 _REQ_DATA_VERSION = "__version__"
130 # Feature string for instance creation request data version 1
131 _INST_CREATE_REQV1 = "instance-create-reqv1"
133 # Feature string for instance reinstall request version 1
134 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
136 # Feature string for node migration version 1
137 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
139 # Feature string for node evacuation with LU-generated jobs
140 _NODE_EVAC_RES1 = "node-evac-res1"
142 ALL_FEATURES = frozenset([
144 _INST_REINSTALL_REQV1,
149 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
153 class R_root(baserlib.ResourceBase):
159 """Supported for legacy reasons.
171 class R_version(baserlib.ResourceBase):
172 """/version resource.
174 This resource should be used to determine the remote API version and
175 to adapt clients accordingly.
180 """Returns the remote API version.
183 return constants.RAPI_VERSION
186 class R_2_info(baserlib.OpcodeResource):
190 GET_OPCODE = opcodes.OpClusterQuery
193 """Returns cluster information.
196 client = self.GetClient()
197 return client.QueryClusterInfo()
200 class R_2_features(baserlib.ResourceBase):
201 """/2/features resource.
206 """Returns list of optional RAPI features implemented.
209 return list(ALL_FEATURES)
212 class R_2_os(baserlib.OpcodeResource):
216 GET_OPCODE = opcodes.OpOsDiagnose
219 """Return a list of all OSes.
221 Can return error 500 in case of a problem.
223 Example: ["debian-etch"]
226 cl = self.GetClient()
227 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
228 job_id = self.SubmitJob([op], cl=cl)
229 # we use custom feedback function, instead of print we log the status
230 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
231 diagnose_data = result[0]
233 if not isinstance(diagnose_data, list):
234 raise http.HttpBadGateway(message="Can't get OS list")
237 for (name, variants) in diagnose_data:
238 os_names.extend(cli.CalculateOSNames(name, variants))
243 class R_2_redist_config(baserlib.OpcodeResource):
244 """/2/redistribute-config resource.
247 PUT_OPCODE = opcodes.OpClusterRedistConf
250 class R_2_cluster_modify(baserlib.OpcodeResource):
251 """/2/modify resource.
254 PUT_OPCODE = opcodes.OpClusterSetParams
257 class R_2_jobs(baserlib.ResourceBase):
262 """Returns a dictionary of jobs.
264 @return: a dictionary with jobs id and uri.
267 client = self.GetClient()
270 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
271 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
273 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
274 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
275 uri_fields=("id", "uri"))
278 class R_2_jobs_id(baserlib.ResourceBase):
279 """/2/jobs/[job_id] resource.
283 """Returns a job status.
285 @return: a dictionary with job parameters.
287 - id: job ID as a number
288 - status: current job status as a string
289 - ops: involved OpCodes as a list of dictionaries for each
291 - opstatus: OpCodes status as a list
292 - opresult: OpCodes results as a list of lists
295 job_id = self.items[0]
296 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
298 raise http.HttpNotFound()
299 return baserlib.MapFields(J_FIELDS, result)
302 """Cancel not-yet-started job.
305 job_id = self.items[0]
306 result = self.GetClient().CancelJob(job_id)
310 class R_2_jobs_id_wait(baserlib.ResourceBase):
311 """/2/jobs/[job_id]/wait resource.
314 # WaitForJobChange provides access to sensitive information and blocks
315 # machine resources (it's a blocking RAPI call), hence restricting access.
316 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
319 """Waits for job changes.
322 job_id = self.items[0]
324 fields = self.getBodyParameter("fields")
325 prev_job_info = self.getBodyParameter("previous_job_info", None)
326 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
328 if not isinstance(fields, list):
329 raise http.HttpBadRequest("The 'fields' parameter should be a list")
331 if not (prev_job_info is None or isinstance(prev_job_info, list)):
332 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
335 if not (prev_log_serial is None or
336 isinstance(prev_log_serial, (int, long))):
337 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
340 client = self.GetClient()
341 result = client.WaitForJobChangeOnce(job_id, fields,
342 prev_job_info, prev_log_serial,
343 timeout=_WFJC_TIMEOUT)
345 raise http.HttpNotFound()
347 if result == constants.JOB_NOTCHANGED:
351 (job_info, log_entries) = result
354 "job_info": job_info,
355 "log_entries": log_entries,
359 class R_2_nodes(baserlib.OpcodeResource):
360 """/2/nodes resource.
363 GET_OPCODE = opcodes.OpNodeQuery
366 """Returns a list of all nodes.
369 client = self.GetClient()
372 bulkdata = client.QueryNodes([], N_FIELDS, False)
373 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
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"))
381 class R_2_nodes_name(baserlib.OpcodeResource):
382 """/2/nodes/[node_name] resource.
385 GET_OPCODE = opcodes.OpNodeQuery
388 """Send information about a node.
391 node_name = self.items[0]
392 client = self.GetClient()
394 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
395 names=[node_name], fields=N_FIELDS,
396 use_locking=self.useLocking())
398 return baserlib.MapFields(N_FIELDS, result[0])
401 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
402 """/2/nodes/[node_name]/powercycle resource.
405 POST_OPCODE = opcodes.OpNodePowercycle
407 def GetPostOpInput(self):
408 """Tries to powercycle a node.
411 return (self.request_body, {
412 "node_name": self.items[0],
413 "force": self.useForce(),
417 class R_2_nodes_name_role(baserlib.OpcodeResource):
418 """/2/nodes/[node_name]/role resource.
421 PUT_OPCODE = opcodes.OpNodeSetParams
424 """Returns the current node role.
429 node_name = self.items[0]
430 client = self.GetClient()
431 result = client.QueryNodes(names=[node_name], fields=["role"],
432 use_locking=self.useLocking())
434 return _NR_MAP[result[0][0]]
436 def GetPutOpInput(self):
437 """Sets the node role.
440 baserlib.CheckType(self.request_body, basestring, "Body contents")
442 role = self.request_body
444 if role == _NR_REGULAR:
449 elif role == _NR_MASTER_CANDIDATE:
451 offline = drained = None
453 elif role == _NR_DRAINED:
455 candidate = offline = None
457 elif role == _NR_OFFLINE:
459 candidate = drained = None
462 raise http.HttpBadRequest("Can't set '%s' role" % role)
464 assert len(self.items) == 1
467 "node_name": self.items[0],
468 "master_candidate": candidate,
471 "force": self.useForce(),
472 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
476 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
477 """/2/nodes/[node_name]/evacuate resource.
480 POST_OPCODE = opcodes.OpNodeEvacuate
482 def GetPostOpInput(self):
483 """Evacuate all instances off a node.
486 return (self.request_body, {
487 "node_name": self.items[0],
488 "dry_run": self.dryRun(),
492 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
493 """/2/nodes/[node_name]/migrate resource.
496 POST_OPCODE = opcodes.OpNodeMigrate
498 def GetPostOpInput(self):
499 """Migrate all primary instances from a node.
503 # Support old-style requests
504 if "live" in self.queryargs and "mode" in self.queryargs:
505 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
508 if "live" in self.queryargs:
509 if self._checkIntVariable("live", default=1):
510 mode = constants.HT_MIGRATION_LIVE
512 mode = constants.HT_MIGRATION_NONLIVE
514 mode = self._checkStringVariable("mode", default=None)
520 data = self.request_body
523 "node_name": self.items[0],
527 class R_2_nodes_name_modify(baserlib.OpcodeResource):
528 """/2/nodes/[node_name]/modify resource.
531 POST_OPCODE = opcodes.OpNodeSetParams
533 def GetPostOpInput(self):
534 """Changes parameters of a node.
537 assert len(self.items) == 1
539 return (self.request_body, {
540 "node_name": self.items[0],
544 class R_2_nodes_name_storage(baserlib.OpcodeResource):
545 """/2/nodes/[node_name]/storage resource.
548 # LUNodeQueryStorage acquires locks, hence restricting access to GET
549 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
550 GET_OPCODE = opcodes.OpNodeQueryStorage
552 def GetGetOpInput(self):
553 """List storage available on a node.
556 storage_type = self._checkStringVariable("storage_type", None)
557 output_fields = self._checkStringVariable("output_fields", None)
559 if not output_fields:
560 raise http.HttpBadRequest("Missing the required 'output_fields'"
564 "nodes": [self.items[0]],
565 "storage_type": storage_type,
566 "output_fields": output_fields.split(","),
570 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
571 """/2/nodes/[node_name]/storage/modify resource.
574 PUT_OPCODE = opcodes.OpNodeModifyStorage
576 def GetPutOpInput(self):
577 """Modifies a storage volume on a node.
580 storage_type = self._checkStringVariable("storage_type", None)
581 name = self._checkStringVariable("name", None)
584 raise http.HttpBadRequest("Missing the required 'name'"
589 if "allocatable" in self.queryargs:
590 changes[constants.SF_ALLOCATABLE] = \
591 bool(self._checkIntVariable("allocatable", default=1))
594 "node_name": self.items[0],
595 "storage_type": storage_type,
601 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
602 """/2/nodes/[node_name]/storage/repair resource.
605 PUT_OPCODE = opcodes.OpRepairNodeStorage
607 def GetPutOpInput(self):
608 """Repairs a storage volume on a node.
611 storage_type = self._checkStringVariable("storage_type", None)
612 name = self._checkStringVariable("name", None)
614 raise http.HttpBadRequest("Missing the required 'name'"
618 "node_name": self.items[0],
619 "storage_type": storage_type,
624 class R_2_groups(baserlib.OpcodeResource):
625 """/2/groups resource.
628 GET_OPCODE = opcodes.OpGroupQuery
629 POST_OPCODE = opcodes.OpGroupAdd
631 "name": "group_name",
634 def GetPostOpInput(self):
635 """Create a node group.
638 assert not self.items
639 return (self.request_body, {
640 "dry_run": self.dryRun(),
644 """Returns a list of all node groups.
647 client = self.GetClient()
650 bulkdata = client.QueryGroups([], G_FIELDS, False)
651 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
653 data = client.QueryGroups([], ["name"], False)
654 groupnames = [row[0] for row in data]
655 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
656 uri_fields=("name", "uri"))
659 class R_2_groups_name(baserlib.OpcodeResource):
660 """/2/groups/[group_name] resource.
663 DELETE_OPCODE = opcodes.OpGroupRemove
666 """Send information about a node group.
669 group_name = self.items[0]
670 client = self.GetClient()
672 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
673 names=[group_name], fields=G_FIELDS,
674 use_locking=self.useLocking())
676 return baserlib.MapFields(G_FIELDS, result[0])
678 def GetDeleteOpInput(self):
679 """Delete a node group.
682 assert len(self.items) == 1
684 "group_name": self.items[0],
685 "dry_run": self.dryRun(),
689 class R_2_groups_name_modify(baserlib.OpcodeResource):
690 """/2/groups/[group_name]/modify resource.
693 PUT_OPCODE = opcodes.OpGroupSetParams
695 def GetPutOpInput(self):
696 """Changes some parameters of node group.
700 return (self.request_body, {
701 "group_name": self.items[0],
705 class R_2_groups_name_rename(baserlib.OpcodeResource):
706 """/2/groups/[group_name]/rename resource.
709 PUT_OPCODE = opcodes.OpGroupRename
711 def GetPutOpInput(self):
712 """Changes the name of a node group.
715 assert len(self.items) == 1
716 return (self.request_body, {
717 "group_name": self.items[0],
718 "dry_run": self.dryRun(),
722 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
723 """/2/groups/[group_name]/assign-nodes resource.
726 PUT_OPCODE = opcodes.OpGroupAssignNodes
728 def GetPutOpInput(self):
729 """Assigns nodes to a group.
732 assert len(self.items) == 1
733 return (self.request_body, {
734 "group_name": self.items[0],
735 "dry_run": self.dryRun(),
736 "force": self.useForce(),
740 class R_2_instances(baserlib.OpcodeResource):
741 """/2/instances resource.
744 GET_OPCODE = opcodes.OpInstanceQuery
745 POST_OPCODE = opcodes.OpInstanceCreate
748 "name": "instance_name",
752 """Returns a list of all available instances.
755 client = self.GetClient()
757 use_locking = self.useLocking()
759 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
760 return baserlib.MapBulkFields(bulkdata, I_FIELDS)
762 instancesdata = client.QueryInstances([], ["name"], use_locking)
763 instanceslist = [row[0] for row in instancesdata]
764 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
765 uri_fields=("id", "uri"))
767 def GetPostOpInput(self):
768 """Create an instance.
773 baserlib.CheckType(self.request_body, dict, "Body contents")
775 # Default to request data version 0
776 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
778 if data_version == 0:
779 raise http.HttpBadRequest("Instance creation request version 0 is no"
781 elif data_version != 1:
782 raise http.HttpBadRequest("Unsupported request data version %s" %
785 data = self.request_body.copy()
786 # Remove "__version__"
787 data.pop(_REQ_DATA_VERSION, None)
790 "dry_run": self.dryRun(),
794 class R_2_instances_name(baserlib.OpcodeResource):
795 """/2/instances/[instance_name] resource.
798 GET_OPCODE = opcodes.OpInstanceQuery
799 DELETE_OPCODE = opcodes.OpInstanceRemove
802 """Send information about an instance.
805 client = self.GetClient()
806 instance_name = self.items[0]
808 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
809 names=[instance_name],
811 use_locking=self.useLocking())
813 return baserlib.MapFields(I_FIELDS, result[0])
815 def GetDeleteOpInput(self):
816 """Delete an instance.
819 assert len(self.items) == 1
821 "instance_name": self.items[0],
822 "ignore_failures": False,
823 "dry_run": self.dryRun(),
827 class R_2_instances_name_info(baserlib.OpcodeResource):
828 """/2/instances/[instance_name]/info resource.
831 GET_OPCODE = opcodes.OpInstanceQueryData
833 def GetGetOpInput(self):
834 """Request detailed instance information.
837 assert len(self.items) == 1
839 "instances": [self.items[0]],
840 "static": bool(self._checkIntVariable("static", default=0)),
844 class R_2_instances_name_reboot(baserlib.OpcodeResource):
845 """/2/instances/[instance_name]/reboot resource.
847 Implements an instance reboot.
850 POST_OPCODE = opcodes.OpInstanceReboot
852 def GetPostOpInput(self):
853 """Reboot an instance.
855 The URI takes type=[hard|soft|full] and
856 ignore_secondaries=[False|True] parameters.
860 "instance_name": self.items[0],
862 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
863 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
864 "dry_run": self.dryRun(),
868 class R_2_instances_name_startup(baserlib.OpcodeResource):
869 """/2/instances/[instance_name]/startup resource.
871 Implements an instance startup.
874 PUT_OPCODE = opcodes.OpInstanceStartup
876 def GetPutOpInput(self):
877 """Startup an instance.
879 The URI takes force=[False|True] parameter to start the instance
880 if even if secondary disks are failing.
884 "instance_name": self.items[0],
885 "force": self.useForce(),
886 "dry_run": self.dryRun(),
887 "no_remember": bool(self._checkIntVariable("no_remember")),
891 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
892 """/2/instances/[instance_name]/shutdown resource.
894 Implements an instance shutdown.
897 PUT_OPCODE = opcodes.OpInstanceShutdown
899 def GetPutOpInput(self):
900 """Shutdown an instance.
903 return (self.request_body, {
904 "instance_name": self.items[0],
905 "no_remember": bool(self._checkIntVariable("no_remember")),
906 "dry_run": self.dryRun(),
910 def _ParseInstanceReinstallRequest(name, data):
911 """Parses a request for reinstalling an instance.
914 if not isinstance(data, dict):
915 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
917 ostype = baserlib.CheckParameter(data, "os", default=None)
918 start = baserlib.CheckParameter(data, "start", exptype=bool,
920 osparams = baserlib.CheckParameter(data, "osparams", default=None)
923 opcodes.OpInstanceShutdown(instance_name=name),
924 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
929 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
934 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
935 """/2/instances/[instance_name]/reinstall resource.
937 Implements an instance reinstall.
940 POST_OPCODE = opcodes.OpInstanceReinstall
943 """Reinstall an instance.
945 The URI takes os=name and nostartup=[0|1] optional
946 parameters. By default, the instance will be started
950 if self.request_body:
952 raise http.HttpBadRequest("Can't combine query and body parameters")
954 body = self.request_body
956 # Legacy interface, do not modify/extend
958 "os": self._checkStringVariable("os"),
959 "start": not self._checkIntVariable("nostartup"),
964 ops = _ParseInstanceReinstallRequest(self.items[0], body)
966 return self.SubmitJob(ops)
969 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
970 """/2/instances/[instance_name]/replace-disks resource.
973 POST_OPCODE = opcodes.OpInstanceReplaceDisks
975 def GetPostOpInput(self):
976 """Replaces disks on an instance.
980 "instance_name": self.items[0],
983 if self.request_body:
984 data = self.request_body
986 # Legacy interface, do not modify/extend
988 "remote_node": self._checkStringVariable("remote_node", default=None),
989 "mode": self._checkStringVariable("mode", default=None),
990 "disks": self._checkStringVariable("disks", default=None),
991 "iallocator": self._checkStringVariable("iallocator", default=None),
998 raw_disks = data.pop("disks")
1003 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1004 data["disks"] = raw_disks
1006 # Backwards compatibility for strings of the format "1, 2, 3"
1008 data["disks"] = [int(part) for part in raw_disks.split(",")]
1009 except (TypeError, ValueError), err:
1010 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1012 return (data, static)
1015 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1016 """/2/instances/[instance_name]/activate-disks resource.
1019 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1021 def GetPutOpInput(self):
1022 """Activate disks for an instance.
1024 The URI might contain ignore_size to ignore current recorded size.
1028 "instance_name": self.items[0],
1029 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1033 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1034 """/2/instances/[instance_name]/deactivate-disks resource.
1037 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1039 def GetPutOpInput(self):
1040 """Deactivate disks for an instance.
1044 "instance_name": self.items[0],
1048 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1049 """/2/instances/[instance_name]/recreate-disks resource.
1052 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1054 def GetPostOpInput(self):
1055 """Recreate disks for an instance.
1059 "instance_name": self.items[0],
1063 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1064 """/2/instances/[instance_name]/prepare-export resource.
1067 PUT_OPCODE = opcodes.OpBackupPrepare
1069 def GetPutOpInput(self):
1070 """Prepares an export for an instance.
1074 "instance_name": self.items[0],
1075 "mode": self._checkStringVariable("mode"),
1079 class R_2_instances_name_export(baserlib.OpcodeResource):
1080 """/2/instances/[instance_name]/export resource.
1083 PUT_OPCODE = opcodes.OpBackupExport
1085 "destination": "target_node",
1088 def GetPutOpInput(self):
1089 """Exports an instance.
1092 return (self.request_body, {
1093 "instance_name": self.items[0],
1097 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1098 """/2/instances/[instance_name]/migrate resource.
1101 PUT_OPCODE = opcodes.OpInstanceMigrate
1103 def GetPutOpInput(self):
1104 """Migrates an instance.
1107 return (self.request_body, {
1108 "instance_name": self.items[0],
1112 class R_2_instances_name_failover(baserlib.OpcodeResource):
1113 """/2/instances/[instance_name]/failover resource.
1116 PUT_OPCODE = opcodes.OpInstanceFailover
1118 def GetPutOpInput(self):
1119 """Does a failover of an instance.
1122 return (self.request_body, {
1123 "instance_name": self.items[0],
1127 class R_2_instances_name_rename(baserlib.OpcodeResource):
1128 """/2/instances/[instance_name]/rename resource.
1131 PUT_OPCODE = opcodes.OpInstanceRename
1133 def GetPutOpInput(self):
1134 """Changes the name of an instance.
1137 return (self.request_body, {
1138 "instance_name": self.items[0],
1142 class R_2_instances_name_modify(baserlib.OpcodeResource):
1143 """/2/instances/[instance_name]/modify resource.
1146 PUT_OPCODE = opcodes.OpInstanceSetParams
1148 def GetPutOpInput(self):
1149 """Changes parameters of an instance.
1152 return (self.request_body, {
1153 "instance_name": self.items[0],
1157 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1158 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1161 POST_OPCODE = opcodes.OpInstanceGrowDisk
1163 def GetPostOpInput(self):
1164 """Increases the size of an instance disk.
1167 return (self.request_body, {
1168 "instance_name": self.items[0],
1169 "disk": int(self.items[1]),
1173 class R_2_instances_name_console(baserlib.ResourceBase):
1174 """/2/instances/[instance_name]/console resource.
1177 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1178 GET_OPCODE = opcodes.OpInstanceConsole
1181 """Request information for connecting to instance's console.
1183 @return: Serialized instance console description, see
1184 L{objects.InstanceConsole}
1187 client = self.GetClient()
1189 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1192 raise http.HttpServiceUnavailable("Instance console unavailable")
1194 assert isinstance(console, dict)
1198 def _GetQueryFields(args):
1203 fields = args["fields"]
1205 raise http.HttpBadRequest("Missing 'fields' query argument")
1207 return _SplitQueryFields(fields[0])
1210 def _SplitQueryFields(fields):
1214 return [i.strip() for i in fields.split(",")]
1217 class R_2_query(baserlib.ResourceBase):
1218 """/2/query/[resource] resource.
1221 # Results might contain sensitive information
1222 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1223 GET_OPCODE = opcodes.OpQuery
1224 PUT_OPCODE = opcodes.OpQuery
1226 def _Query(self, fields, qfilter):
1227 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1230 """Returns resource information.
1232 @return: Query result, see L{objects.QueryResponse}
1235 return self._Query(_GetQueryFields(self.queryargs), None)
1238 """Submits job querying for resources.
1240 @return: Query result, see L{objects.QueryResponse}
1243 body = self.request_body
1245 baserlib.CheckType(body, dict, "Body contents")
1248 fields = body["fields"]
1250 fields = _GetQueryFields(self.queryargs)
1252 qfilter = body.get("qfilter", None)
1253 # TODO: remove this after 2.7
1255 qfilter = body.get("filter", None)
1257 return self._Query(fields, qfilter)
1260 class R_2_query_fields(baserlib.ResourceBase):
1261 """/2/query/[resource]/fields resource.
1264 GET_OPCODE = opcodes.OpQueryFields
1267 """Retrieves list of available fields for a resource.
1269 @return: List of serialized L{objects.QueryFieldDefinition}
1273 raw_fields = self.queryargs["fields"]
1277 fields = _SplitQueryFields(raw_fields[0])
1279 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1282 class _R_Tags(baserlib.OpcodeResource):
1283 """ Quasiclass for tagging resources
1285 Manages tags. When inheriting this class you must define the
1290 GET_OPCODE = opcodes.OpTagsGet
1291 PUT_OPCODE = opcodes.OpTagsSet
1292 DELETE_OPCODE = opcodes.OpTagsDel
1294 def __init__(self, items, queryargs, req, **kwargs):
1295 """A tag resource constructor.
1297 We have to override the default to sort out cluster naming case.
1300 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1302 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1305 self.name = items[0]
1308 """Returns a list of tags.
1310 Example: ["tag1", "tag2", "tag3"]
1313 kind = self.TAG_LEVEL
1315 if kind in (constants.TAG_INSTANCE,
1316 constants.TAG_NODEGROUP,
1317 constants.TAG_NODE):
1319 raise http.HttpBadRequest("Missing name on tag request")
1321 cl = self.GetClient()
1322 if kind == constants.TAG_INSTANCE:
1323 fn = cl.QueryInstances
1324 elif kind == constants.TAG_NODEGROUP:
1328 result = fn(names=[self.name], fields=["tags"], use_locking=False)
1329 if not result or not result[0]:
1330 raise http.HttpBadGateway("Invalid response from tag query")
1333 elif kind == constants.TAG_CLUSTER:
1334 assert not self.name
1335 # TODO: Use query API?
1336 ssc = ssconf.SimpleStore()
1337 tags = ssc.GetClusterTags()
1341 def GetPutOpInput(self):
1342 """Add a set of tags.
1344 The request as a list of strings should be PUT to this URI. And
1345 you'll have back a job id.
1349 "kind": self.TAG_LEVEL,
1351 "tags": self.queryargs.get("tag", []),
1352 "dry_run": self.dryRun(),
1355 def GetDeleteOpInput(self):
1358 In order to delete a set of tags, the DELETE
1359 request should be addressed to URI like:
1360 /tags?tag=[tag]&tag=[tag]
1364 return self.GetPutOpInput()
1367 class R_2_instances_name_tags(_R_Tags):
1368 """ /2/instances/[instance_name]/tags resource.
1370 Manages per-instance tags.
1373 TAG_LEVEL = constants.TAG_INSTANCE
1376 class R_2_nodes_name_tags(_R_Tags):
1377 """ /2/nodes/[node_name]/tags resource.
1379 Manages per-node tags.
1382 TAG_LEVEL = constants.TAG_NODE
1385 class R_2_groups_name_tags(_R_Tags):
1386 """ /2/groups/[group_name]/tags resource.
1388 Manages per-nodegroup tags.
1391 TAG_LEVEL = constants.TAG_NODEGROUP
1394 class R_2_tags(_R_Tags):
1395 """ /2/tags resource.
1397 Manages cluster tags.
1400 TAG_LEVEL = constants.TAG_CLUSTER