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 objects
60 from ganeti import http
61 from ganeti import constants
62 from ganeti import cli
63 from ganeti import rapi
65 from ganeti import compat
66 from ganeti import ssconf
67 from ganeti.rapi import baserlib
70 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
71 I_FIELDS = ["name", "admin_state", "os",
74 "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
76 "disk.sizes", "disk_usage",
77 "beparams", "hvparams",
78 "oper_state", "oper_ram", "oper_vcpus", "status",
79 "custom_hvparams", "custom_beparams", "custom_nicparams",
82 N_FIELDS = ["name", "offline", "master_candidate", "drained",
84 "mtotal", "mnode", "mfree",
85 "pinst_cnt", "sinst_cnt",
86 "ctotal", "cnodes", "csockets",
88 "pinst_list", "sinst_list",
89 "master_capable", "vm_capable",
108 "id", "ops", "status", "summary",
110 "received_ts", "start_ts", "end_ts",
113 J_FIELDS = J_FIELDS_BULK + [
118 _NR_DRAINED = "drained"
119 _NR_MASTER_CANDIDATE = "master-candidate"
120 _NR_MASTER = "master"
121 _NR_OFFLINE = "offline"
122 _NR_REGULAR = "regular"
125 constants.NR_MASTER: _NR_MASTER,
126 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
127 constants.NR_DRAINED: _NR_DRAINED,
128 constants.NR_OFFLINE: _NR_OFFLINE,
129 constants.NR_REGULAR: _NR_REGULAR,
132 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
134 # Request data version field
135 _REQ_DATA_VERSION = "__version__"
137 # Feature string for instance creation request data version 1
138 _INST_CREATE_REQV1 = "instance-create-reqv1"
140 # Feature string for instance reinstall request version 1
141 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
143 # Feature string for node migration version 1
144 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
146 # Feature string for node evacuation with LU-generated jobs
147 _NODE_EVAC_RES1 = "node-evac-res1"
149 ALL_FEATURES = frozenset([
151 _INST_REINSTALL_REQV1,
156 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
160 # FIXME: For compatibility we update the beparams/memory field. Needs to be
161 # removed in Ganeti 2.7
162 def _UpdateBeparams(inst):
163 """Updates the beparams dict of inst to support the memory field.
165 @param inst: Inst dict
166 @return: Updated inst dict
169 beparams = inst["beparams"]
170 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
175 class R_root(baserlib.ResourceBase):
181 """Supported for legacy reasons.
193 class R_version(baserlib.ResourceBase):
194 """/version resource.
196 This resource should be used to determine the remote API version and
197 to adapt clients accordingly.
202 """Returns the remote API version.
205 return constants.RAPI_VERSION
208 class R_2_info(baserlib.OpcodeResource):
212 GET_OPCODE = opcodes.OpClusterQuery
215 """Returns cluster information.
218 client = self.GetClient(query=True)
219 return client.QueryClusterInfo()
222 class R_2_features(baserlib.ResourceBase):
223 """/2/features resource.
228 """Returns list of optional RAPI features implemented.
231 return list(ALL_FEATURES)
234 class R_2_os(baserlib.OpcodeResource):
238 GET_OPCODE = opcodes.OpOsDiagnose
241 """Return a list of all OSes.
243 Can return error 500 in case of a problem.
245 Example: ["debian-etch"]
248 cl = self.GetClient()
249 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
250 job_id = self.SubmitJob([op], cl=cl)
251 # we use custom feedback function, instead of print we log the status
252 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
253 diagnose_data = result[0]
255 if not isinstance(diagnose_data, list):
256 raise http.HttpBadGateway(message="Can't get OS list")
259 for (name, variants) in diagnose_data:
260 os_names.extend(cli.CalculateOSNames(name, variants))
265 class R_2_redist_config(baserlib.OpcodeResource):
266 """/2/redistribute-config resource.
269 PUT_OPCODE = opcodes.OpClusterRedistConf
272 class R_2_cluster_modify(baserlib.OpcodeResource):
273 """/2/modify resource.
276 PUT_OPCODE = opcodes.OpClusterSetParams
279 class R_2_jobs(baserlib.ResourceBase):
284 """Returns a dictionary of jobs.
286 @return: a dictionary with jobs id and uri.
289 client = self.GetClient()
292 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
293 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
295 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
296 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
297 uri_fields=("id", "uri"))
300 class R_2_jobs_id(baserlib.ResourceBase):
301 """/2/jobs/[job_id] resource.
305 """Returns a job status.
307 @return: a dictionary with job parameters.
309 - id: job ID as a number
310 - status: current job status as a string
311 - ops: involved OpCodes as a list of dictionaries for each
313 - opstatus: OpCodes status as a list
314 - opresult: OpCodes results as a list of lists
317 job_id = self.items[0]
318 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
320 raise http.HttpNotFound()
321 return baserlib.MapFields(J_FIELDS, result)
324 """Cancel not-yet-started job.
327 job_id = self.items[0]
328 result = self.GetClient().CancelJob(job_id)
332 class R_2_jobs_id_wait(baserlib.ResourceBase):
333 """/2/jobs/[job_id]/wait resource.
336 # WaitForJobChange provides access to sensitive information and blocks
337 # machine resources (it's a blocking RAPI call), hence restricting access.
338 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
341 """Waits for job changes.
344 job_id = self.items[0]
346 fields = self.getBodyParameter("fields")
347 prev_job_info = self.getBodyParameter("previous_job_info", None)
348 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
350 if not isinstance(fields, list):
351 raise http.HttpBadRequest("The 'fields' parameter should be a list")
353 if not (prev_job_info is None or isinstance(prev_job_info, list)):
354 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
357 if not (prev_log_serial is None or
358 isinstance(prev_log_serial, (int, long))):
359 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
362 client = self.GetClient()
363 result = client.WaitForJobChangeOnce(job_id, fields,
364 prev_job_info, prev_log_serial,
365 timeout=_WFJC_TIMEOUT)
367 raise http.HttpNotFound()
369 if result == constants.JOB_NOTCHANGED:
373 (job_info, log_entries) = result
376 "job_info": job_info,
377 "log_entries": log_entries,
381 class R_2_nodes(baserlib.OpcodeResource):
382 """/2/nodes resource.
385 GET_OPCODE = opcodes.OpNodeQuery
388 """Returns a list of all nodes.
391 client = self.GetClient(query=True)
394 bulkdata = client.QueryNodes([], N_FIELDS, False)
395 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
397 nodesdata = client.QueryNodes([], ["name"], False)
398 nodeslist = [row[0] for row in nodesdata]
399 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
400 uri_fields=("id", "uri"))
403 class R_2_nodes_name(baserlib.OpcodeResource):
404 """/2/nodes/[node_name] resource.
407 GET_OPCODE = opcodes.OpNodeQuery
410 """Send information about a node.
413 node_name = self.items[0]
414 client = self.GetClient(query=True)
416 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
417 names=[node_name], fields=N_FIELDS,
418 use_locking=self.useLocking())
420 return baserlib.MapFields(N_FIELDS, result[0])
423 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
424 """/2/nodes/[node_name]/powercycle resource.
427 POST_OPCODE = opcodes.OpNodePowercycle
429 def GetPostOpInput(self):
430 """Tries to powercycle a node.
433 return (self.request_body, {
434 "node_name": self.items[0],
435 "force": self.useForce(),
439 class R_2_nodes_name_role(baserlib.OpcodeResource):
440 """/2/nodes/[node_name]/role resource.
443 PUT_OPCODE = opcodes.OpNodeSetParams
446 """Returns the current node role.
451 node_name = self.items[0]
452 client = self.GetClient(query=True)
453 result = client.QueryNodes(names=[node_name], fields=["role"],
454 use_locking=self.useLocking())
456 return _NR_MAP[result[0][0]]
458 def GetPutOpInput(self):
459 """Sets the node role.
462 baserlib.CheckType(self.request_body, basestring, "Body contents")
464 role = self.request_body
466 if role == _NR_REGULAR:
471 elif role == _NR_MASTER_CANDIDATE:
473 offline = drained = None
475 elif role == _NR_DRAINED:
477 candidate = offline = None
479 elif role == _NR_OFFLINE:
481 candidate = drained = None
484 raise http.HttpBadRequest("Can't set '%s' role" % role)
486 assert len(self.items) == 1
489 "node_name": self.items[0],
490 "master_candidate": candidate,
493 "force": self.useForce(),
494 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
498 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
499 """/2/nodes/[node_name]/evacuate resource.
502 POST_OPCODE = opcodes.OpNodeEvacuate
504 def GetPostOpInput(self):
505 """Evacuate all instances off a node.
508 return (self.request_body, {
509 "node_name": self.items[0],
510 "dry_run": self.dryRun(),
514 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
515 """/2/nodes/[node_name]/migrate resource.
518 POST_OPCODE = opcodes.OpNodeMigrate
520 def GetPostOpInput(self):
521 """Migrate all primary instances from a node.
525 # Support old-style requests
526 if "live" in self.queryargs and "mode" in self.queryargs:
527 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
530 if "live" in self.queryargs:
531 if self._checkIntVariable("live", default=1):
532 mode = constants.HT_MIGRATION_LIVE
534 mode = constants.HT_MIGRATION_NONLIVE
536 mode = self._checkStringVariable("mode", default=None)
542 data = self.request_body
545 "node_name": self.items[0],
549 class R_2_nodes_name_modify(baserlib.OpcodeResource):
550 """/2/nodes/[node_name]/modify resource.
553 POST_OPCODE = opcodes.OpNodeSetParams
555 def GetPostOpInput(self):
556 """Changes parameters of a node.
559 assert len(self.items) == 1
561 return (self.request_body, {
562 "node_name": self.items[0],
566 class R_2_nodes_name_storage(baserlib.OpcodeResource):
567 """/2/nodes/[node_name]/storage resource.
570 # LUNodeQueryStorage acquires locks, hence restricting access to GET
571 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
572 GET_OPCODE = opcodes.OpNodeQueryStorage
574 def GetGetOpInput(self):
575 """List storage available on a node.
578 storage_type = self._checkStringVariable("storage_type", None)
579 output_fields = self._checkStringVariable("output_fields", None)
581 if not output_fields:
582 raise http.HttpBadRequest("Missing the required 'output_fields'"
586 "nodes": [self.items[0]],
587 "storage_type": storage_type,
588 "output_fields": output_fields.split(","),
592 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
593 """/2/nodes/[node_name]/storage/modify resource.
596 PUT_OPCODE = opcodes.OpNodeModifyStorage
598 def GetPutOpInput(self):
599 """Modifies a storage volume on a node.
602 storage_type = self._checkStringVariable("storage_type", None)
603 name = self._checkStringVariable("name", None)
606 raise http.HttpBadRequest("Missing the required 'name'"
611 if "allocatable" in self.queryargs:
612 changes[constants.SF_ALLOCATABLE] = \
613 bool(self._checkIntVariable("allocatable", default=1))
616 "node_name": self.items[0],
617 "storage_type": storage_type,
623 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
624 """/2/nodes/[node_name]/storage/repair resource.
627 PUT_OPCODE = opcodes.OpRepairNodeStorage
629 def GetPutOpInput(self):
630 """Repairs a storage volume on a node.
633 storage_type = self._checkStringVariable("storage_type", None)
634 name = self._checkStringVariable("name", None)
636 raise http.HttpBadRequest("Missing the required 'name'"
640 "node_name": self.items[0],
641 "storage_type": storage_type,
646 class R_2_groups(baserlib.OpcodeResource):
647 """/2/groups resource.
650 GET_OPCODE = opcodes.OpGroupQuery
651 POST_OPCODE = opcodes.OpGroupAdd
653 "name": "group_name",
656 def GetPostOpInput(self):
657 """Create a node group.
660 assert not self.items
661 return (self.request_body, {
662 "dry_run": self.dryRun(),
666 """Returns a list of all node groups.
669 client = self.GetClient(query=True)
672 bulkdata = client.QueryGroups([], G_FIELDS, False)
673 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
675 data = client.QueryGroups([], ["name"], False)
676 groupnames = [row[0] for row in data]
677 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
678 uri_fields=("name", "uri"))
681 class R_2_groups_name(baserlib.OpcodeResource):
682 """/2/groups/[group_name] resource.
685 DELETE_OPCODE = opcodes.OpGroupRemove
688 """Send information about a node group.
691 group_name = self.items[0]
692 client = self.GetClient(query=True)
694 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
695 names=[group_name], fields=G_FIELDS,
696 use_locking=self.useLocking())
698 return baserlib.MapFields(G_FIELDS, result[0])
700 def GetDeleteOpInput(self):
701 """Delete a node group.
704 assert len(self.items) == 1
706 "group_name": self.items[0],
707 "dry_run": self.dryRun(),
711 class R_2_groups_name_modify(baserlib.OpcodeResource):
712 """/2/groups/[group_name]/modify resource.
715 PUT_OPCODE = opcodes.OpGroupSetParams
717 def GetPutOpInput(self):
718 """Changes some parameters of node group.
722 return (self.request_body, {
723 "group_name": self.items[0],
727 class R_2_groups_name_rename(baserlib.OpcodeResource):
728 """/2/groups/[group_name]/rename resource.
731 PUT_OPCODE = opcodes.OpGroupRename
733 def GetPutOpInput(self):
734 """Changes the name of a node group.
737 assert len(self.items) == 1
738 return (self.request_body, {
739 "group_name": self.items[0],
740 "dry_run": self.dryRun(),
744 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
745 """/2/groups/[group_name]/assign-nodes resource.
748 PUT_OPCODE = opcodes.OpGroupAssignNodes
750 def GetPutOpInput(self):
751 """Assigns nodes to a group.
754 assert len(self.items) == 1
755 return (self.request_body, {
756 "group_name": self.items[0],
757 "dry_run": self.dryRun(),
758 "force": self.useForce(),
762 class R_2_instances(baserlib.OpcodeResource):
763 """/2/instances resource.
766 GET_OPCODE = opcodes.OpInstanceQuery
767 POST_OPCODE = opcodes.OpInstanceCreate
770 "name": "instance_name",
774 """Returns a list of all available instances.
777 client = self.GetClient()
779 use_locking = self.useLocking()
781 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
782 return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
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"))
789 def GetPostOpInput(self):
790 """Create an instance.
795 baserlib.CheckType(self.request_body, dict, "Body contents")
797 # Default to request data version 0
798 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
800 if data_version == 0:
801 raise http.HttpBadRequest("Instance creation request version 0 is no"
803 elif data_version != 1:
804 raise http.HttpBadRequest("Unsupported request data version %s" %
807 data = self.request_body.copy()
808 # Remove "__version__"
809 data.pop(_REQ_DATA_VERSION, None)
812 "dry_run": self.dryRun(),
816 class R_2_instances_multi_alloc(baserlib.OpcodeResource):
817 """/2/instances-multi-alloc resource.
820 POST_OPCODE = opcodes.OpInstanceMultiAlloc
822 def GetPostOpInput(self):
823 """Try to allocate multiple instances.
825 @return: A dict with submitted jobs, allocatable instances and failed
829 if "instances" not in self.request_body:
830 raise http.HttpBadRequest("Request is missing required 'instances' field"
834 "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
836 body = objects.FillDict(self.request_body, {
837 "instances": [objects.FillDict(inst, op_id)
838 for inst in self.request_body["instances"]],
842 "dry_run": self.dryRun(),
846 class R_2_instances_name(baserlib.OpcodeResource):
847 """/2/instances/[instance_name] resource.
850 GET_OPCODE = opcodes.OpInstanceQuery
851 DELETE_OPCODE = opcodes.OpInstanceRemove
854 """Send information about an instance.
857 client = self.GetClient()
858 instance_name = self.items[0]
860 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
861 names=[instance_name],
863 use_locking=self.useLocking())
865 return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
867 def GetDeleteOpInput(self):
868 """Delete an instance.
871 assert len(self.items) == 1
873 "instance_name": self.items[0],
874 "ignore_failures": False,
875 "dry_run": self.dryRun(),
879 class R_2_instances_name_info(baserlib.OpcodeResource):
880 """/2/instances/[instance_name]/info resource.
883 GET_OPCODE = opcodes.OpInstanceQueryData
885 def GetGetOpInput(self):
886 """Request detailed instance information.
889 assert len(self.items) == 1
891 "instances": [self.items[0]],
892 "static": bool(self._checkIntVariable("static", default=0)),
896 class R_2_instances_name_reboot(baserlib.OpcodeResource):
897 """/2/instances/[instance_name]/reboot resource.
899 Implements an instance reboot.
902 POST_OPCODE = opcodes.OpInstanceReboot
904 def GetPostOpInput(self):
905 """Reboot an instance.
907 The URI takes type=[hard|soft|full] and
908 ignore_secondaries=[False|True] parameters.
912 "instance_name": self.items[0],
914 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
915 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
916 "dry_run": self.dryRun(),
920 class R_2_instances_name_startup(baserlib.OpcodeResource):
921 """/2/instances/[instance_name]/startup resource.
923 Implements an instance startup.
926 PUT_OPCODE = opcodes.OpInstanceStartup
928 def GetPutOpInput(self):
929 """Startup an instance.
931 The URI takes force=[False|True] parameter to start the instance
932 if even if secondary disks are failing.
936 "instance_name": self.items[0],
937 "force": self.useForce(),
938 "dry_run": self.dryRun(),
939 "no_remember": bool(self._checkIntVariable("no_remember")),
943 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
944 """/2/instances/[instance_name]/shutdown resource.
946 Implements an instance shutdown.
949 PUT_OPCODE = opcodes.OpInstanceShutdown
951 def GetPutOpInput(self):
952 """Shutdown an instance.
955 return (self.request_body, {
956 "instance_name": self.items[0],
957 "no_remember": bool(self._checkIntVariable("no_remember")),
958 "dry_run": self.dryRun(),
962 def _ParseInstanceReinstallRequest(name, data):
963 """Parses a request for reinstalling an instance.
966 if not isinstance(data, dict):
967 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
969 ostype = baserlib.CheckParameter(data, "os", default=None)
970 start = baserlib.CheckParameter(data, "start", exptype=bool,
972 osparams = baserlib.CheckParameter(data, "osparams", default=None)
975 opcodes.OpInstanceShutdown(instance_name=name),
976 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
981 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
986 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
987 """/2/instances/[instance_name]/reinstall resource.
989 Implements an instance reinstall.
992 POST_OPCODE = opcodes.OpInstanceReinstall
995 """Reinstall an instance.
997 The URI takes os=name and nostartup=[0|1] optional
998 parameters. By default, the instance will be started
1002 if self.request_body:
1004 raise http.HttpBadRequest("Can't combine query and body parameters")
1006 body = self.request_body
1007 elif self.queryargs:
1008 # Legacy interface, do not modify/extend
1010 "os": self._checkStringVariable("os"),
1011 "start": not self._checkIntVariable("nostartup"),
1016 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1018 return self.SubmitJob(ops)
1021 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1022 """/2/instances/[instance_name]/replace-disks resource.
1025 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1027 def GetPostOpInput(self):
1028 """Replaces disks on an instance.
1032 "instance_name": self.items[0],
1035 if self.request_body:
1036 data = self.request_body
1037 elif self.queryargs:
1038 # Legacy interface, do not modify/extend
1040 "remote_node": self._checkStringVariable("remote_node", default=None),
1041 "mode": self._checkStringVariable("mode", default=None),
1042 "disks": self._checkStringVariable("disks", default=None),
1043 "iallocator": self._checkStringVariable("iallocator", default=None),
1050 raw_disks = data.pop("disks")
1055 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1056 data["disks"] = raw_disks
1058 # Backwards compatibility for strings of the format "1, 2, 3"
1060 data["disks"] = [int(part) for part in raw_disks.split(",")]
1061 except (TypeError, ValueError), err:
1062 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1064 return (data, static)
1067 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1068 """/2/instances/[instance_name]/activate-disks resource.
1071 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1073 def GetPutOpInput(self):
1074 """Activate disks for an instance.
1076 The URI might contain ignore_size to ignore current recorded size.
1080 "instance_name": self.items[0],
1081 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1085 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1086 """/2/instances/[instance_name]/deactivate-disks resource.
1089 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1091 def GetPutOpInput(self):
1092 """Deactivate disks for an instance.
1096 "instance_name": self.items[0],
1100 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1101 """/2/instances/[instance_name]/recreate-disks resource.
1104 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1106 def GetPostOpInput(self):
1107 """Recreate disks for an instance.
1111 "instance_name": self.items[0],
1115 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1116 """/2/instances/[instance_name]/prepare-export resource.
1119 PUT_OPCODE = opcodes.OpBackupPrepare
1121 def GetPutOpInput(self):
1122 """Prepares an export for an instance.
1126 "instance_name": self.items[0],
1127 "mode": self._checkStringVariable("mode"),
1131 class R_2_instances_name_export(baserlib.OpcodeResource):
1132 """/2/instances/[instance_name]/export resource.
1135 PUT_OPCODE = opcodes.OpBackupExport
1137 "destination": "target_node",
1140 def GetPutOpInput(self):
1141 """Exports an instance.
1144 return (self.request_body, {
1145 "instance_name": self.items[0],
1149 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1150 """/2/instances/[instance_name]/migrate resource.
1153 PUT_OPCODE = opcodes.OpInstanceMigrate
1155 def GetPutOpInput(self):
1156 """Migrates an instance.
1159 return (self.request_body, {
1160 "instance_name": self.items[0],
1164 class R_2_instances_name_failover(baserlib.OpcodeResource):
1165 """/2/instances/[instance_name]/failover resource.
1168 PUT_OPCODE = opcodes.OpInstanceFailover
1170 def GetPutOpInput(self):
1171 """Does a failover of an instance.
1174 return (self.request_body, {
1175 "instance_name": self.items[0],
1179 class R_2_instances_name_rename(baserlib.OpcodeResource):
1180 """/2/instances/[instance_name]/rename resource.
1183 PUT_OPCODE = opcodes.OpInstanceRename
1185 def GetPutOpInput(self):
1186 """Changes the name of an instance.
1189 return (self.request_body, {
1190 "instance_name": self.items[0],
1194 class R_2_instances_name_modify(baserlib.OpcodeResource):
1195 """/2/instances/[instance_name]/modify resource.
1198 PUT_OPCODE = opcodes.OpInstanceSetParams
1200 def GetPutOpInput(self):
1201 """Changes parameters of an instance.
1204 return (self.request_body, {
1205 "instance_name": self.items[0],
1209 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1210 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1213 POST_OPCODE = opcodes.OpInstanceGrowDisk
1215 def GetPostOpInput(self):
1216 """Increases the size of an instance disk.
1219 return (self.request_body, {
1220 "instance_name": self.items[0],
1221 "disk": int(self.items[1]),
1225 class R_2_instances_name_console(baserlib.ResourceBase):
1226 """/2/instances/[instance_name]/console resource.
1229 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1230 GET_OPCODE = opcodes.OpInstanceConsole
1233 """Request information for connecting to instance's console.
1235 @return: Serialized instance console description, see
1236 L{objects.InstanceConsole}
1239 client = self.GetClient()
1241 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1244 raise http.HttpServiceUnavailable("Instance console unavailable")
1246 assert isinstance(console, dict)
1250 def _GetQueryFields(args):
1255 fields = args["fields"]
1257 raise http.HttpBadRequest("Missing 'fields' query argument")
1259 return _SplitQueryFields(fields[0])
1262 def _SplitQueryFields(fields):
1266 return [i.strip() for i in fields.split(",")]
1269 class R_2_query(baserlib.ResourceBase):
1270 """/2/query/[resource] resource.
1273 # Results might contain sensitive information
1274 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1275 GET_OPCODE = opcodes.OpQuery
1276 PUT_OPCODE = opcodes.OpQuery
1278 def _Query(self, fields, qfilter):
1279 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1282 """Returns resource information.
1284 @return: Query result, see L{objects.QueryResponse}
1287 return self._Query(_GetQueryFields(self.queryargs), None)
1290 """Submits job querying for resources.
1292 @return: Query result, see L{objects.QueryResponse}
1295 body = self.request_body
1297 baserlib.CheckType(body, dict, "Body contents")
1300 fields = body["fields"]
1302 fields = _GetQueryFields(self.queryargs)
1304 qfilter = body.get("qfilter", None)
1305 # TODO: remove this after 2.7
1307 qfilter = body.get("filter", None)
1309 return self._Query(fields, qfilter)
1312 class R_2_query_fields(baserlib.ResourceBase):
1313 """/2/query/[resource]/fields resource.
1316 GET_OPCODE = opcodes.OpQueryFields
1319 """Retrieves list of available fields for a resource.
1321 @return: List of serialized L{objects.QueryFieldDefinition}
1325 raw_fields = self.queryargs["fields"]
1329 fields = _SplitQueryFields(raw_fields[0])
1331 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1334 class _R_Tags(baserlib.OpcodeResource):
1335 """Quasiclass for tagging resources.
1337 Manages tags. When inheriting this class you must define the
1342 GET_OPCODE = opcodes.OpTagsGet
1343 PUT_OPCODE = opcodes.OpTagsSet
1344 DELETE_OPCODE = opcodes.OpTagsDel
1346 def __init__(self, items, queryargs, req, **kwargs):
1347 """A tag resource constructor.
1349 We have to override the default to sort out cluster naming case.
1352 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1354 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1357 self.name = items[0]
1360 """Returns a list of tags.
1362 Example: ["tag1", "tag2", "tag3"]
1365 kind = self.TAG_LEVEL
1367 if kind in (constants.TAG_INSTANCE,
1368 constants.TAG_NODEGROUP,
1369 constants.TAG_NODE):
1371 raise http.HttpBadRequest("Missing name on tag request")
1373 cl = self.GetClient(query=True)
1374 tags = list(cl.QueryTags(kind, self.name))
1376 elif kind == constants.TAG_CLUSTER:
1377 assert not self.name
1378 # TODO: Use query API?
1379 ssc = ssconf.SimpleStore()
1380 tags = ssc.GetClusterTags()
1384 def GetPutOpInput(self):
1385 """Add a set of tags.
1387 The request as a list of strings should be PUT to this URI. And
1388 you'll have back a job id.
1392 "kind": self.TAG_LEVEL,
1394 "tags": self.queryargs.get("tag", []),
1395 "dry_run": self.dryRun(),
1398 def GetDeleteOpInput(self):
1401 In order to delete a set of tags, the DELETE
1402 request should be addressed to URI like:
1403 /tags?tag=[tag]&tag=[tag]
1407 return self.GetPutOpInput()
1410 class R_2_instances_name_tags(_R_Tags):
1411 """ /2/instances/[instance_name]/tags resource.
1413 Manages per-instance tags.
1416 TAG_LEVEL = constants.TAG_INSTANCE
1419 class R_2_nodes_name_tags(_R_Tags):
1420 """ /2/nodes/[node_name]/tags resource.
1422 Manages per-node tags.
1425 TAG_LEVEL = constants.TAG_NODE
1428 class R_2_groups_name_tags(_R_Tags):
1429 """ /2/groups/[group_name]/tags resource.
1431 Manages per-nodegroup tags.
1434 TAG_LEVEL = constants.TAG_NODEGROUP
1437 class R_2_tags(_R_Tags):
1438 """ /2/tags resource.
1440 Manages cluster tags.
1443 TAG_LEVEL = constants.TAG_CLUSTER