4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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.uuids", "nic.names",
75 "nic.links", "nic.networks", "nic.networks.names", "nic.bridges",
77 "disk.sizes", "disk.spindles", "disk_usage", "disk.uuids",
79 "beparams", "hvparams",
80 "oper_state", "oper_ram", "oper_vcpus", "status",
81 "custom_hvparams", "custom_beparams", "custom_nicparams",
84 N_FIELDS = ["name", "offline", "master_candidate", "drained",
85 "dtotal", "dfree", "sptotal", "spfree",
86 "mtotal", "mnode", "mfree",
87 "pinst_cnt", "sinst_cnt",
88 "ctotal", "cnos", "cnodes", "csockets",
90 "pinst_list", "sinst_list",
91 "master_capable", "vm_capable",
96 NET_FIELDS = ["name", "network", "gateway",
97 "network6", "gateway6",
99 "free_count", "reserved_count",
100 "map", "group_list", "inst_list",
101 "external_reservations",
118 "id", "ops", "status", "summary",
120 "received_ts", "start_ts", "end_ts",
123 J_FIELDS = J_FIELDS_BULK + [
128 _NR_DRAINED = "drained"
129 _NR_MASTER_CANDIDATE = "master-candidate"
130 _NR_MASTER = "master"
131 _NR_OFFLINE = "offline"
132 _NR_REGULAR = "regular"
135 constants.NR_MASTER: _NR_MASTER,
136 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
137 constants.NR_DRAINED: _NR_DRAINED,
138 constants.NR_OFFLINE: _NR_OFFLINE,
139 constants.NR_REGULAR: _NR_REGULAR,
142 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
144 # Request data version field
145 _REQ_DATA_VERSION = "__version__"
147 # Feature string for instance creation request data version 1
148 _INST_CREATE_REQV1 = "instance-create-reqv1"
150 # Feature string for instance reinstall request version 1
151 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
153 # Feature string for node migration version 1
154 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
156 # Feature string for node evacuation with LU-generated jobs
157 _NODE_EVAC_RES1 = "node-evac-res1"
159 ALL_FEATURES = compat.UniqueFrozenset([
161 _INST_REINSTALL_REQV1,
166 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
170 # FIXME: For compatibility we update the beparams/memory field. Needs to be
171 # removed in Ganeti 2.8
172 def _UpdateBeparams(inst):
173 """Updates the beparams dict of inst to support the memory field.
175 @param inst: Inst dict
176 @return: Updated inst dict
179 beparams = inst["beparams"]
180 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
185 class R_root(baserlib.ResourceBase):
191 """Supported for legacy reasons.
203 class R_version(baserlib.ResourceBase):
204 """/version resource.
206 This resource should be used to determine the remote API version and
207 to adapt clients accordingly.
212 """Returns the remote API version.
215 return constants.RAPI_VERSION
218 class R_2_info(baserlib.OpcodeResource):
222 GET_OPCODE = opcodes.OpClusterQuery
225 """Returns cluster information.
228 client = self.GetClient(query=True)
229 return client.QueryClusterInfo()
232 class R_2_features(baserlib.ResourceBase):
233 """/2/features resource.
238 """Returns list of optional RAPI features implemented.
241 return list(ALL_FEATURES)
244 class R_2_os(baserlib.OpcodeResource):
248 GET_OPCODE = opcodes.OpOsDiagnose
251 """Return a list of all OSes.
253 Can return error 500 in case of a problem.
255 Example: ["debian-etch"]
258 cl = self.GetClient()
259 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
260 job_id = self.SubmitJob([op], cl=cl)
261 # we use custom feedback function, instead of print we log the status
262 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
263 diagnose_data = result[0]
265 if not isinstance(diagnose_data, list):
266 raise http.HttpBadGateway(message="Can't get OS list")
269 for (name, variants) in diagnose_data:
270 os_names.extend(cli.CalculateOSNames(name, variants))
275 class R_2_redist_config(baserlib.OpcodeResource):
276 """/2/redistribute-config resource.
279 PUT_OPCODE = opcodes.OpClusterRedistConf
282 class R_2_cluster_modify(baserlib.OpcodeResource):
283 """/2/modify resource.
286 PUT_OPCODE = opcodes.OpClusterSetParams
289 class R_2_jobs(baserlib.ResourceBase):
294 """Returns a dictionary of jobs.
296 @return: a dictionary with jobs id and uri.
299 client = self.GetClient(query=True)
302 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
303 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
305 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
306 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
307 uri_fields=("id", "uri"))
310 class R_2_jobs_id(baserlib.ResourceBase):
311 """/2/jobs/[job_id] resource.
315 """Returns a job status.
317 @return: a dictionary with job parameters.
319 - id: job ID as a number
320 - status: current job status as a string
321 - ops: involved OpCodes as a list of dictionaries for each
323 - opstatus: OpCodes status as a list
324 - opresult: OpCodes results as a list of lists
327 job_id = self.items[0]
328 result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
330 raise http.HttpNotFound()
331 return baserlib.MapFields(J_FIELDS, result)
334 """Cancel not-yet-started job.
337 job_id = self.items[0]
338 result = self.GetClient().CancelJob(job_id)
342 class R_2_jobs_id_wait(baserlib.ResourceBase):
343 """/2/jobs/[job_id]/wait resource.
346 # WaitForJobChange provides access to sensitive information and blocks
347 # machine resources (it's a blocking RAPI call), hence restricting access.
348 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
351 """Waits for job changes.
354 job_id = self.items[0]
356 fields = self.getBodyParameter("fields")
357 prev_job_info = self.getBodyParameter("previous_job_info", None)
358 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
360 if not isinstance(fields, list):
361 raise http.HttpBadRequest("The 'fields' parameter should be a list")
363 if not (prev_job_info is None or isinstance(prev_job_info, list)):
364 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
367 if not (prev_log_serial is None or
368 isinstance(prev_log_serial, (int, long))):
369 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
372 client = self.GetClient()
373 result = client.WaitForJobChangeOnce(job_id, fields,
374 prev_job_info, prev_log_serial,
375 timeout=_WFJC_TIMEOUT)
377 raise http.HttpNotFound()
379 if result == constants.JOB_NOTCHANGED:
383 (job_info, log_entries) = result
386 "job_info": job_info,
387 "log_entries": log_entries,
391 class R_2_nodes(baserlib.OpcodeResource):
392 """/2/nodes resource.
395 GET_OPCODE = opcodes.OpNodeQuery
398 """Returns a list of all nodes.
401 client = self.GetClient(query=True)
404 bulkdata = client.QueryNodes([], N_FIELDS, False)
405 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
407 nodesdata = client.QueryNodes([], ["name"], False)
408 nodeslist = [row[0] for row in nodesdata]
409 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
410 uri_fields=("id", "uri"))
413 class R_2_nodes_name(baserlib.OpcodeResource):
414 """/2/nodes/[node_name] resource.
417 GET_OPCODE = opcodes.OpNodeQuery
420 """Send information about a node.
423 node_name = self.items[0]
424 client = self.GetClient(query=True)
426 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
427 names=[node_name], fields=N_FIELDS,
428 use_locking=self.useLocking())
430 return baserlib.MapFields(N_FIELDS, result[0])
433 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
434 """/2/nodes/[node_name]/powercycle resource.
437 POST_OPCODE = opcodes.OpNodePowercycle
439 def GetPostOpInput(self):
440 """Tries to powercycle a node.
443 return (self.request_body, {
444 "node_name": self.items[0],
445 "force": self.useForce(),
449 class R_2_nodes_name_role(baserlib.OpcodeResource):
450 """/2/nodes/[node_name]/role resource.
453 PUT_OPCODE = opcodes.OpNodeSetParams
456 """Returns the current node role.
461 node_name = self.items[0]
462 client = self.GetClient(query=True)
463 result = client.QueryNodes(names=[node_name], fields=["role"],
464 use_locking=self.useLocking())
466 return _NR_MAP[result[0][0]]
468 def GetPutOpInput(self):
469 """Sets the node role.
472 baserlib.CheckType(self.request_body, basestring, "Body contents")
474 role = self.request_body
476 if role == _NR_REGULAR:
481 elif role == _NR_MASTER_CANDIDATE:
483 offline = drained = None
485 elif role == _NR_DRAINED:
487 candidate = offline = None
489 elif role == _NR_OFFLINE:
491 candidate = drained = None
494 raise http.HttpBadRequest("Can't set '%s' role" % role)
496 assert len(self.items) == 1
499 "node_name": self.items[0],
500 "master_candidate": candidate,
503 "force": self.useForce(),
504 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
508 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
509 """/2/nodes/[node_name]/evacuate resource.
512 POST_OPCODE = opcodes.OpNodeEvacuate
514 def GetPostOpInput(self):
515 """Evacuate all instances off a node.
518 return (self.request_body, {
519 "node_name": self.items[0],
520 "dry_run": self.dryRun(),
524 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
525 """/2/nodes/[node_name]/migrate resource.
528 POST_OPCODE = opcodes.OpNodeMigrate
530 def GetPostOpInput(self):
531 """Migrate all primary instances from a node.
535 # Support old-style requests
536 if "live" in self.queryargs and "mode" in self.queryargs:
537 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
540 if "live" in self.queryargs:
541 if self._checkIntVariable("live", default=1):
542 mode = constants.HT_MIGRATION_LIVE
544 mode = constants.HT_MIGRATION_NONLIVE
546 mode = self._checkStringVariable("mode", default=None)
552 data = self.request_body
555 "node_name": self.items[0],
559 class R_2_nodes_name_modify(baserlib.OpcodeResource):
560 """/2/nodes/[node_name]/modify resource.
563 POST_OPCODE = opcodes.OpNodeSetParams
565 def GetPostOpInput(self):
566 """Changes parameters of a node.
569 assert len(self.items) == 1
571 return (self.request_body, {
572 "node_name": self.items[0],
576 class R_2_nodes_name_storage(baserlib.OpcodeResource):
577 """/2/nodes/[node_name]/storage resource.
580 # LUNodeQueryStorage acquires locks, hence restricting access to GET
581 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
582 GET_OPCODE = opcodes.OpNodeQueryStorage
584 def GetGetOpInput(self):
585 """List storage available on a node.
588 storage_type = self._checkStringVariable("storage_type", None)
589 output_fields = self._checkStringVariable("output_fields", None)
591 if not output_fields:
592 raise http.HttpBadRequest("Missing the required 'output_fields'"
596 "nodes": [self.items[0]],
597 "storage_type": storage_type,
598 "output_fields": output_fields.split(","),
602 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
603 """/2/nodes/[node_name]/storage/modify resource.
606 PUT_OPCODE = opcodes.OpNodeModifyStorage
608 def GetPutOpInput(self):
609 """Modifies a storage volume on a node.
612 storage_type = self._checkStringVariable("storage_type", None)
613 name = self._checkStringVariable("name", None)
616 raise http.HttpBadRequest("Missing the required 'name'"
621 if "allocatable" in self.queryargs:
622 changes[constants.SF_ALLOCATABLE] = \
623 bool(self._checkIntVariable("allocatable", default=1))
626 "node_name": self.items[0],
627 "storage_type": storage_type,
633 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
634 """/2/nodes/[node_name]/storage/repair resource.
637 PUT_OPCODE = opcodes.OpRepairNodeStorage
639 def GetPutOpInput(self):
640 """Repairs a storage volume on a node.
643 storage_type = self._checkStringVariable("storage_type", None)
644 name = self._checkStringVariable("name", None)
646 raise http.HttpBadRequest("Missing the required 'name'"
650 "node_name": self.items[0],
651 "storage_type": storage_type,
656 class R_2_networks(baserlib.OpcodeResource):
657 """/2/networks resource.
660 GET_OPCODE = opcodes.OpNetworkQuery
661 POST_OPCODE = opcodes.OpNetworkAdd
663 "name": "network_name",
666 def GetPostOpInput(self):
670 assert not self.items
671 return (self.request_body, {
672 "dry_run": self.dryRun(),
676 """Returns a list of all networks.
679 client = self.GetClient(query=True)
682 bulkdata = client.QueryNetworks([], NET_FIELDS, False)
683 return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
685 data = client.QueryNetworks([], ["name"], False)
686 networknames = [row[0] for row in data]
687 return baserlib.BuildUriList(networknames, "/2/networks/%s",
688 uri_fields=("name", "uri"))
691 class R_2_networks_name(baserlib.OpcodeResource):
692 """/2/networks/[network_name] resource.
695 DELETE_OPCODE = opcodes.OpNetworkRemove
698 """Send information about a network.
701 network_name = self.items[0]
702 client = self.GetClient(query=True)
704 result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
705 names=[network_name],
707 use_locking=self.useLocking())
709 return baserlib.MapFields(NET_FIELDS, result[0])
711 def GetDeleteOpInput(self):
715 assert len(self.items) == 1
716 return (self.request_body, {
717 "network_name": self.items[0],
718 "dry_run": self.dryRun(),
722 class R_2_networks_name_connect(baserlib.OpcodeResource):
723 """/2/networks/[network_name]/connect resource.
726 PUT_OPCODE = opcodes.OpNetworkConnect
728 def GetPutOpInput(self):
729 """Changes some parameters of node group.
733 return (self.request_body, {
734 "network_name": self.items[0],
735 "dry_run": self.dryRun(),
739 class R_2_networks_name_disconnect(baserlib.OpcodeResource):
740 """/2/networks/[network_name]/disconnect resource.
743 PUT_OPCODE = opcodes.OpNetworkDisconnect
745 def GetPutOpInput(self):
746 """Changes some parameters of node group.
750 return (self.request_body, {
751 "network_name": self.items[0],
752 "dry_run": self.dryRun(),
756 class R_2_networks_name_modify(baserlib.OpcodeResource):
757 """/2/networks/[network_name]/modify resource.
760 PUT_OPCODE = opcodes.OpNetworkSetParams
762 def GetPutOpInput(self):
763 """Changes some parameters of network.
767 return (self.request_body, {
768 "network_name": self.items[0],
772 class R_2_groups(baserlib.OpcodeResource):
773 """/2/groups resource.
776 GET_OPCODE = opcodes.OpGroupQuery
777 POST_OPCODE = opcodes.OpGroupAdd
779 "name": "group_name",
782 def GetPostOpInput(self):
783 """Create a node group.
787 assert not self.items
788 return (self.request_body, {
789 "dry_run": self.dryRun(),
793 """Returns a list of all node groups.
796 client = self.GetClient(query=True)
799 bulkdata = client.QueryGroups([], G_FIELDS, False)
800 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
802 data = client.QueryGroups([], ["name"], False)
803 groupnames = [row[0] for row in data]
804 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
805 uri_fields=("name", "uri"))
808 class R_2_groups_name(baserlib.OpcodeResource):
809 """/2/groups/[group_name] resource.
812 DELETE_OPCODE = opcodes.OpGroupRemove
815 """Send information about a node group.
818 group_name = self.items[0]
819 client = self.GetClient(query=True)
821 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
822 names=[group_name], fields=G_FIELDS,
823 use_locking=self.useLocking())
825 return baserlib.MapFields(G_FIELDS, result[0])
827 def GetDeleteOpInput(self):
828 """Delete a node group.
831 assert len(self.items) == 1
833 "group_name": self.items[0],
834 "dry_run": self.dryRun(),
838 class R_2_groups_name_modify(baserlib.OpcodeResource):
839 """/2/groups/[group_name]/modify resource.
842 PUT_OPCODE = opcodes.OpGroupSetParams
844 def GetPutOpInput(self):
845 """Changes some parameters of node group.
849 return (self.request_body, {
850 "group_name": self.items[0],
854 class R_2_groups_name_rename(baserlib.OpcodeResource):
855 """/2/groups/[group_name]/rename resource.
858 PUT_OPCODE = opcodes.OpGroupRename
860 def GetPutOpInput(self):
861 """Changes the name of a node group.
864 assert len(self.items) == 1
865 return (self.request_body, {
866 "group_name": self.items[0],
867 "dry_run": self.dryRun(),
871 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
872 """/2/groups/[group_name]/assign-nodes resource.
875 PUT_OPCODE = opcodes.OpGroupAssignNodes
877 def GetPutOpInput(self):
878 """Assigns nodes to a group.
881 assert len(self.items) == 1
882 return (self.request_body, {
883 "group_name": self.items[0],
884 "dry_run": self.dryRun(),
885 "force": self.useForce(),
889 def _ConvertUsbDevices(data):
890 """Convert in place the usb_devices string to the proper format.
892 In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
893 comma to space because commas cannot be accepted on the command line
894 (they already act as the separator between different hvparams). RAPI
895 should be able to accept commas for backwards compatibility, but we want
896 it to also accept the new space separator. Therefore, we convert
897 spaces into commas here and keep the old parsing logic elsewhere.
901 hvparams = data["hvparams"]
902 usb_devices = hvparams[constants.HV_USB_DEVICES]
903 hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
904 data["hvparams"] = hvparams
906 #No usb_devices, no modification required
910 class R_2_instances(baserlib.OpcodeResource):
911 """/2/instances resource.
914 GET_OPCODE = opcodes.OpInstanceQuery
915 POST_OPCODE = opcodes.OpInstanceCreate
918 "name": "instance_name",
922 """Returns a list of all available instances.
925 client = self.GetClient()
927 use_locking = self.useLocking()
929 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
930 return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
932 instancesdata = client.QueryInstances([], ["name"], use_locking)
933 instanceslist = [row[0] for row in instancesdata]
934 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
935 uri_fields=("id", "uri"))
937 def GetPostOpInput(self):
938 """Create an instance.
943 baserlib.CheckType(self.request_body, dict, "Body contents")
945 # Default to request data version 0
946 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
948 if data_version == 0:
949 raise http.HttpBadRequest("Instance creation request version 0 is no"
951 elif data_version != 1:
952 raise http.HttpBadRequest("Unsupported request data version %s" %
955 data = self.request_body.copy()
956 # Remove "__version__"
957 data.pop(_REQ_DATA_VERSION, None)
959 _ConvertUsbDevices(data)
962 "dry_run": self.dryRun(),
966 class R_2_instances_multi_alloc(baserlib.OpcodeResource):
967 """/2/instances-multi-alloc resource.
970 POST_OPCODE = opcodes.OpInstanceMultiAlloc
972 def GetPostOpInput(self):
973 """Try to allocate multiple instances.
975 @return: A dict with submitted jobs, allocatable instances and failed
979 if "instances" not in self.request_body:
980 raise http.HttpBadRequest("Request is missing required 'instances' field"
984 "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
986 body = objects.FillDict(self.request_body, {
987 "instances": [objects.FillDict(inst, op_id)
988 for inst in self.request_body["instances"]],
992 "dry_run": self.dryRun(),
996 class R_2_instances_name(baserlib.OpcodeResource):
997 """/2/instances/[instance_name] resource.
1000 GET_OPCODE = opcodes.OpInstanceQuery
1001 DELETE_OPCODE = opcodes.OpInstanceRemove
1004 """Send information about an instance.
1007 client = self.GetClient()
1008 instance_name = self.items[0]
1010 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1011 names=[instance_name],
1013 use_locking=self.useLocking())
1015 return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1017 def GetDeleteOpInput(self):
1018 """Delete an instance.
1021 assert len(self.items) == 1
1022 return (self.request_body, {
1023 "instance_name": self.items[0],
1024 "ignore_failures": False,
1025 "dry_run": self.dryRun(),
1029 class R_2_instances_name_info(baserlib.OpcodeResource):
1030 """/2/instances/[instance_name]/info resource.
1033 GET_OPCODE = opcodes.OpInstanceQueryData
1035 def GetGetOpInput(self):
1036 """Request detailed instance information.
1039 assert len(self.items) == 1
1041 "instances": [self.items[0]],
1042 "static": bool(self._checkIntVariable("static", default=0)),
1046 class R_2_instances_name_reboot(baserlib.OpcodeResource):
1047 """/2/instances/[instance_name]/reboot resource.
1049 Implements an instance reboot.
1052 POST_OPCODE = opcodes.OpInstanceReboot
1054 def GetPostOpInput(self):
1055 """Reboot an instance.
1057 The URI takes type=[hard|soft|full] and
1058 ignore_secondaries=[False|True] parameters.
1061 return (self.request_body, {
1062 "instance_name": self.items[0],
1064 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1065 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1066 "dry_run": self.dryRun(),
1070 class R_2_instances_name_startup(baserlib.OpcodeResource):
1071 """/2/instances/[instance_name]/startup resource.
1073 Implements an instance startup.
1076 PUT_OPCODE = opcodes.OpInstanceStartup
1078 def GetPutOpInput(self):
1079 """Startup an instance.
1081 The URI takes force=[False|True] parameter to start the instance
1082 if even if secondary disks are failing.
1086 "instance_name": self.items[0],
1087 "force": self.useForce(),
1088 "dry_run": self.dryRun(),
1089 "no_remember": bool(self._checkIntVariable("no_remember")),
1093 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1094 """/2/instances/[instance_name]/shutdown resource.
1096 Implements an instance shutdown.
1099 PUT_OPCODE = opcodes.OpInstanceShutdown
1101 def GetPutOpInput(self):
1102 """Shutdown an instance.
1105 return (self.request_body, {
1106 "instance_name": self.items[0],
1107 "no_remember": bool(self._checkIntVariable("no_remember")),
1108 "dry_run": self.dryRun(),
1112 def _ParseInstanceReinstallRequest(name, data):
1113 """Parses a request for reinstalling an instance.
1116 if not isinstance(data, dict):
1117 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1119 ostype = baserlib.CheckParameter(data, "os", default=None)
1120 start = baserlib.CheckParameter(data, "start", exptype=bool,
1122 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1125 opcodes.OpInstanceShutdown(instance_name=name),
1126 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1131 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1136 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1137 """/2/instances/[instance_name]/reinstall resource.
1139 Implements an instance reinstall.
1142 POST_OPCODE = opcodes.OpInstanceReinstall
1145 """Reinstall an instance.
1147 The URI takes os=name and nostartup=[0|1] optional
1148 parameters. By default, the instance will be started
1152 if self.request_body:
1154 raise http.HttpBadRequest("Can't combine query and body parameters")
1156 body = self.request_body
1157 elif self.queryargs:
1158 # Legacy interface, do not modify/extend
1160 "os": self._checkStringVariable("os"),
1161 "start": not self._checkIntVariable("nostartup"),
1166 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1168 return self.SubmitJob(ops)
1171 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1172 """/2/instances/[instance_name]/replace-disks resource.
1175 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1177 def GetPostOpInput(self):
1178 """Replaces disks on an instance.
1182 "instance_name": self.items[0],
1185 if self.request_body:
1186 data = self.request_body
1187 elif self.queryargs:
1188 # Legacy interface, do not modify/extend
1190 "remote_node": self._checkStringVariable("remote_node", default=None),
1191 "mode": self._checkStringVariable("mode", default=None),
1192 "disks": self._checkStringVariable("disks", default=None),
1193 "iallocator": self._checkStringVariable("iallocator", default=None),
1200 raw_disks = data.pop("disks")
1205 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1206 data["disks"] = raw_disks
1208 # Backwards compatibility for strings of the format "1, 2, 3"
1210 data["disks"] = [int(part) for part in raw_disks.split(",")]
1211 except (TypeError, ValueError), err:
1212 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1214 return (data, static)
1217 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1218 """/2/instances/[instance_name]/activate-disks resource.
1221 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1223 def GetPutOpInput(self):
1224 """Activate disks for an instance.
1226 The URI might contain ignore_size to ignore current recorded size.
1230 "instance_name": self.items[0],
1231 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1235 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1236 """/2/instances/[instance_name]/deactivate-disks resource.
1239 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1241 def GetPutOpInput(self):
1242 """Deactivate disks for an instance.
1246 "instance_name": self.items[0],
1250 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1251 """/2/instances/[instance_name]/recreate-disks resource.
1254 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1256 def GetPostOpInput(self):
1257 """Recreate disks for an instance.
1261 "instance_name": self.items[0],
1265 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1266 """/2/instances/[instance_name]/prepare-export resource.
1269 PUT_OPCODE = opcodes.OpBackupPrepare
1271 def GetPutOpInput(self):
1272 """Prepares an export for an instance.
1276 "instance_name": self.items[0],
1277 "mode": self._checkStringVariable("mode"),
1281 class R_2_instances_name_export(baserlib.OpcodeResource):
1282 """/2/instances/[instance_name]/export resource.
1285 PUT_OPCODE = opcodes.OpBackupExport
1287 "destination": "target_node",
1290 def GetPutOpInput(self):
1291 """Exports an instance.
1294 return (self.request_body, {
1295 "instance_name": self.items[0],
1299 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1300 """/2/instances/[instance_name]/migrate resource.
1303 PUT_OPCODE = opcodes.OpInstanceMigrate
1305 def GetPutOpInput(self):
1306 """Migrates an instance.
1309 return (self.request_body, {
1310 "instance_name": self.items[0],
1314 class R_2_instances_name_failover(baserlib.OpcodeResource):
1315 """/2/instances/[instance_name]/failover resource.
1318 PUT_OPCODE = opcodes.OpInstanceFailover
1320 def GetPutOpInput(self):
1321 """Does a failover of an instance.
1324 return (self.request_body, {
1325 "instance_name": self.items[0],
1329 class R_2_instances_name_rename(baserlib.OpcodeResource):
1330 """/2/instances/[instance_name]/rename resource.
1333 PUT_OPCODE = opcodes.OpInstanceRename
1335 def GetPutOpInput(self):
1336 """Changes the name of an instance.
1339 return (self.request_body, {
1340 "instance_name": self.items[0],
1344 class R_2_instances_name_modify(baserlib.OpcodeResource):
1345 """/2/instances/[instance_name]/modify resource.
1348 PUT_OPCODE = opcodes.OpInstanceSetParams
1350 def GetPutOpInput(self):
1351 """Changes parameters of an instance.
1354 data = self.request_body.copy()
1355 _ConvertUsbDevices(data)
1358 "instance_name": self.items[0],
1362 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1363 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1366 POST_OPCODE = opcodes.OpInstanceGrowDisk
1368 def GetPostOpInput(self):
1369 """Increases the size of an instance disk.
1372 return (self.request_body, {
1373 "instance_name": self.items[0],
1374 "disk": int(self.items[1]),
1378 class R_2_instances_name_console(baserlib.ResourceBase):
1379 """/2/instances/[instance_name]/console resource.
1382 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1383 GET_OPCODE = opcodes.OpInstanceConsole
1386 """Request information for connecting to instance's console.
1388 @return: Serialized instance console description, see
1389 L{objects.InstanceConsole}
1392 client = self.GetClient()
1394 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1397 raise http.HttpServiceUnavailable("Instance console unavailable")
1399 assert isinstance(console, dict)
1403 def _GetQueryFields(args):
1404 """Tries to extract C{fields} query parameter.
1406 @type args: dictionary
1407 @rtype: list of string
1408 @raise http.HttpBadRequest: When parameter can't be found
1412 fields = args["fields"]
1414 raise http.HttpBadRequest("Missing 'fields' query argument")
1416 return _SplitQueryFields(fields[0])
1419 def _SplitQueryFields(fields):
1420 """Splits fields as given for a query request.
1422 @type fields: string
1423 @rtype: list of string
1426 return [i.strip() for i in fields.split(",")]
1429 class R_2_query(baserlib.ResourceBase):
1430 """/2/query/[resource] resource.
1433 # Results might contain sensitive information
1434 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1435 PUT_ACCESS = GET_ACCESS
1436 GET_OPCODE = opcodes.OpQuery
1437 PUT_OPCODE = opcodes.OpQuery
1439 def _Query(self, fields, qfilter):
1440 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1443 """Returns resource information.
1445 @return: Query result, see L{objects.QueryResponse}
1448 return self._Query(_GetQueryFields(self.queryargs), None)
1451 """Submits job querying for resources.
1453 @return: Query result, see L{objects.QueryResponse}
1456 body = self.request_body
1458 baserlib.CheckType(body, dict, "Body contents")
1461 fields = body["fields"]
1463 fields = _GetQueryFields(self.queryargs)
1465 qfilter = body.get("qfilter", None)
1466 # TODO: remove this after 2.7
1468 qfilter = body.get("filter", None)
1470 return self._Query(fields, qfilter)
1473 class R_2_query_fields(baserlib.ResourceBase):
1474 """/2/query/[resource]/fields resource.
1477 GET_OPCODE = opcodes.OpQueryFields
1480 """Retrieves list of available fields for a resource.
1482 @return: List of serialized L{objects.QueryFieldDefinition}
1486 raw_fields = self.queryargs["fields"]
1490 fields = _SplitQueryFields(raw_fields[0])
1492 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1495 class _R_Tags(baserlib.OpcodeResource):
1496 """Quasiclass for tagging resources.
1498 Manages tags. When inheriting this class you must define the
1503 GET_OPCODE = opcodes.OpTagsGet
1504 PUT_OPCODE = opcodes.OpTagsSet
1505 DELETE_OPCODE = opcodes.OpTagsDel
1507 def __init__(self, items, queryargs, req, **kwargs):
1508 """A tag resource constructor.
1510 We have to override the default to sort out cluster naming case.
1513 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1515 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1518 self.name = items[0]
1521 """Returns a list of tags.
1523 Example: ["tag1", "tag2", "tag3"]
1526 kind = self.TAG_LEVEL
1528 if kind in (constants.TAG_INSTANCE,
1529 constants.TAG_NODEGROUP,
1531 constants.TAG_NETWORK):
1533 raise http.HttpBadRequest("Missing name on tag request")
1535 cl = self.GetClient(query=True)
1536 tags = list(cl.QueryTags(kind, self.name))
1538 elif kind == constants.TAG_CLUSTER:
1539 assert not self.name
1540 # TODO: Use query API?
1541 ssc = ssconf.SimpleStore()
1542 tags = ssc.GetClusterTags()
1545 raise http.HttpBadRequest("Unhandled tag type!")
1549 def GetPutOpInput(self):
1550 """Add a set of tags.
1552 The request as a list of strings should be PUT to this URI. And
1553 you'll have back a job id.
1557 "kind": self.TAG_LEVEL,
1559 "tags": self.queryargs.get("tag", []),
1560 "dry_run": self.dryRun(),
1563 def GetDeleteOpInput(self):
1566 In order to delete a set of tags, the DELETE
1567 request should be addressed to URI like:
1568 /tags?tag=[tag]&tag=[tag]
1572 return self.GetPutOpInput()
1575 class R_2_instances_name_tags(_R_Tags):
1576 """ /2/instances/[instance_name]/tags resource.
1578 Manages per-instance tags.
1581 TAG_LEVEL = constants.TAG_INSTANCE
1584 class R_2_nodes_name_tags(_R_Tags):
1585 """ /2/nodes/[node_name]/tags resource.
1587 Manages per-node tags.
1590 TAG_LEVEL = constants.TAG_NODE
1593 class R_2_groups_name_tags(_R_Tags):
1594 """ /2/groups/[group_name]/tags resource.
1596 Manages per-nodegroup tags.
1599 TAG_LEVEL = constants.TAG_NODEGROUP
1602 class R_2_networks_name_tags(_R_Tags):
1603 """ /2/networks/[network_name]/tags resource.
1605 Manages per-network tags.
1608 TAG_LEVEL = constants.TAG_NETWORK
1611 class R_2_tags(_R_Tags):
1612 """ /2/tags resource.
1614 Manages cluster tags.
1617 TAG_LEVEL = constants.TAG_CLUSTER