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 class R_2_instances(baserlib.OpcodeResource):
890 """/2/instances resource.
893 GET_OPCODE = opcodes.OpInstanceQuery
894 POST_OPCODE = opcodes.OpInstanceCreate
897 "name": "instance_name",
901 """Returns a list of all available instances.
904 client = self.GetClient()
906 use_locking = self.useLocking()
908 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
909 return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
911 instancesdata = client.QueryInstances([], ["name"], use_locking)
912 instanceslist = [row[0] for row in instancesdata]
913 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
914 uri_fields=("id", "uri"))
916 def GetPostOpInput(self):
917 """Create an instance.
922 baserlib.CheckType(self.request_body, dict, "Body contents")
924 # Default to request data version 0
925 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
927 if data_version == 0:
928 raise http.HttpBadRequest("Instance creation request version 0 is no"
930 elif data_version != 1:
931 raise http.HttpBadRequest("Unsupported request data version %s" %
934 data = self.request_body.copy()
935 # Remove "__version__"
936 data.pop(_REQ_DATA_VERSION, None)
939 "dry_run": self.dryRun(),
943 class R_2_instances_multi_alloc(baserlib.OpcodeResource):
944 """/2/instances-multi-alloc resource.
947 POST_OPCODE = opcodes.OpInstanceMultiAlloc
949 def GetPostOpInput(self):
950 """Try to allocate multiple instances.
952 @return: A dict with submitted jobs, allocatable instances and failed
956 if "instances" not in self.request_body:
957 raise http.HttpBadRequest("Request is missing required 'instances' field"
961 "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
963 body = objects.FillDict(self.request_body, {
964 "instances": [objects.FillDict(inst, op_id)
965 for inst in self.request_body["instances"]],
969 "dry_run": self.dryRun(),
973 class R_2_instances_name(baserlib.OpcodeResource):
974 """/2/instances/[instance_name] resource.
977 GET_OPCODE = opcodes.OpInstanceQuery
978 DELETE_OPCODE = opcodes.OpInstanceRemove
981 """Send information about an instance.
984 client = self.GetClient()
985 instance_name = self.items[0]
987 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
988 names=[instance_name],
990 use_locking=self.useLocking())
992 return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
994 def GetDeleteOpInput(self):
995 """Delete an instance.
998 assert len(self.items) == 1
1000 "instance_name": self.items[0],
1001 "ignore_failures": False,
1002 "dry_run": self.dryRun(),
1006 class R_2_instances_name_info(baserlib.OpcodeResource):
1007 """/2/instances/[instance_name]/info resource.
1010 GET_OPCODE = opcodes.OpInstanceQueryData
1012 def GetGetOpInput(self):
1013 """Request detailed instance information.
1016 assert len(self.items) == 1
1018 "instances": [self.items[0]],
1019 "static": bool(self._checkIntVariable("static", default=0)),
1023 class R_2_instances_name_reboot(baserlib.OpcodeResource):
1024 """/2/instances/[instance_name]/reboot resource.
1026 Implements an instance reboot.
1029 POST_OPCODE = opcodes.OpInstanceReboot
1031 def GetPostOpInput(self):
1032 """Reboot an instance.
1034 The URI takes type=[hard|soft|full] and
1035 ignore_secondaries=[False|True] parameters.
1039 "instance_name": self.items[0],
1041 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1042 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1043 "dry_run": self.dryRun(),
1047 class R_2_instances_name_startup(baserlib.OpcodeResource):
1048 """/2/instances/[instance_name]/startup resource.
1050 Implements an instance startup.
1053 PUT_OPCODE = opcodes.OpInstanceStartup
1055 def GetPutOpInput(self):
1056 """Startup an instance.
1058 The URI takes force=[False|True] parameter to start the instance
1059 if even if secondary disks are failing.
1063 "instance_name": self.items[0],
1064 "force": self.useForce(),
1065 "dry_run": self.dryRun(),
1066 "no_remember": bool(self._checkIntVariable("no_remember")),
1070 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1071 """/2/instances/[instance_name]/shutdown resource.
1073 Implements an instance shutdown.
1076 PUT_OPCODE = opcodes.OpInstanceShutdown
1078 def GetPutOpInput(self):
1079 """Shutdown an instance.
1082 return (self.request_body, {
1083 "instance_name": self.items[0],
1084 "no_remember": bool(self._checkIntVariable("no_remember")),
1085 "dry_run": self.dryRun(),
1089 def _ParseInstanceReinstallRequest(name, data):
1090 """Parses a request for reinstalling an instance.
1093 if not isinstance(data, dict):
1094 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1096 ostype = baserlib.CheckParameter(data, "os", default=None)
1097 start = baserlib.CheckParameter(data, "start", exptype=bool,
1099 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1102 opcodes.OpInstanceShutdown(instance_name=name),
1103 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1108 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1113 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1114 """/2/instances/[instance_name]/reinstall resource.
1116 Implements an instance reinstall.
1119 POST_OPCODE = opcodes.OpInstanceReinstall
1122 """Reinstall an instance.
1124 The URI takes os=name and nostartup=[0|1] optional
1125 parameters. By default, the instance will be started
1129 if self.request_body:
1131 raise http.HttpBadRequest("Can't combine query and body parameters")
1133 body = self.request_body
1134 elif self.queryargs:
1135 # Legacy interface, do not modify/extend
1137 "os": self._checkStringVariable("os"),
1138 "start": not self._checkIntVariable("nostartup"),
1143 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1145 return self.SubmitJob(ops)
1148 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1149 """/2/instances/[instance_name]/replace-disks resource.
1152 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1154 def GetPostOpInput(self):
1155 """Replaces disks on an instance.
1159 "instance_name": self.items[0],
1162 if self.request_body:
1163 data = self.request_body
1164 elif self.queryargs:
1165 # Legacy interface, do not modify/extend
1167 "remote_node": self._checkStringVariable("remote_node", default=None),
1168 "mode": self._checkStringVariable("mode", default=None),
1169 "disks": self._checkStringVariable("disks", default=None),
1170 "iallocator": self._checkStringVariable("iallocator", default=None),
1177 raw_disks = data.pop("disks")
1182 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1183 data["disks"] = raw_disks
1185 # Backwards compatibility for strings of the format "1, 2, 3"
1187 data["disks"] = [int(part) for part in raw_disks.split(",")]
1188 except (TypeError, ValueError), err:
1189 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1191 return (data, static)
1194 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1195 """/2/instances/[instance_name]/activate-disks resource.
1198 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1200 def GetPutOpInput(self):
1201 """Activate disks for an instance.
1203 The URI might contain ignore_size to ignore current recorded size.
1207 "instance_name": self.items[0],
1208 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1212 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1213 """/2/instances/[instance_name]/deactivate-disks resource.
1216 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1218 def GetPutOpInput(self):
1219 """Deactivate disks for an instance.
1223 "instance_name": self.items[0],
1227 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1228 """/2/instances/[instance_name]/recreate-disks resource.
1231 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1233 def GetPostOpInput(self):
1234 """Recreate disks for an instance.
1238 "instance_name": self.items[0],
1242 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1243 """/2/instances/[instance_name]/prepare-export resource.
1246 PUT_OPCODE = opcodes.OpBackupPrepare
1248 def GetPutOpInput(self):
1249 """Prepares an export for an instance.
1253 "instance_name": self.items[0],
1254 "mode": self._checkStringVariable("mode"),
1258 class R_2_instances_name_export(baserlib.OpcodeResource):
1259 """/2/instances/[instance_name]/export resource.
1262 PUT_OPCODE = opcodes.OpBackupExport
1264 "destination": "target_node",
1267 def GetPutOpInput(self):
1268 """Exports an instance.
1271 return (self.request_body, {
1272 "instance_name": self.items[0],
1276 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1277 """/2/instances/[instance_name]/migrate resource.
1280 PUT_OPCODE = opcodes.OpInstanceMigrate
1282 def GetPutOpInput(self):
1283 """Migrates an instance.
1286 return (self.request_body, {
1287 "instance_name": self.items[0],
1291 class R_2_instances_name_failover(baserlib.OpcodeResource):
1292 """/2/instances/[instance_name]/failover resource.
1295 PUT_OPCODE = opcodes.OpInstanceFailover
1297 def GetPutOpInput(self):
1298 """Does a failover of an instance.
1301 return (self.request_body, {
1302 "instance_name": self.items[0],
1306 class R_2_instances_name_rename(baserlib.OpcodeResource):
1307 """/2/instances/[instance_name]/rename resource.
1310 PUT_OPCODE = opcodes.OpInstanceRename
1312 def GetPutOpInput(self):
1313 """Changes the name of an instance.
1316 return (self.request_body, {
1317 "instance_name": self.items[0],
1321 class R_2_instances_name_modify(baserlib.OpcodeResource):
1322 """/2/instances/[instance_name]/modify resource.
1325 PUT_OPCODE = opcodes.OpInstanceSetParams
1327 def GetPutOpInput(self):
1328 """Changes parameters of an instance.
1331 return (self.request_body, {
1332 "instance_name": self.items[0],
1336 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1337 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1340 POST_OPCODE = opcodes.OpInstanceGrowDisk
1342 def GetPostOpInput(self):
1343 """Increases the size of an instance disk.
1346 return (self.request_body, {
1347 "instance_name": self.items[0],
1348 "disk": int(self.items[1]),
1352 class R_2_instances_name_console(baserlib.ResourceBase):
1353 """/2/instances/[instance_name]/console resource.
1356 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1357 GET_OPCODE = opcodes.OpInstanceConsole
1360 """Request information for connecting to instance's console.
1362 @return: Serialized instance console description, see
1363 L{objects.InstanceConsole}
1366 client = self.GetClient()
1368 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1371 raise http.HttpServiceUnavailable("Instance console unavailable")
1373 assert isinstance(console, dict)
1377 def _GetQueryFields(args):
1378 """Tries to extract C{fields} query parameter.
1380 @type args: dictionary
1381 @rtype: list of string
1382 @raise http.HttpBadRequest: When parameter can't be found
1386 fields = args["fields"]
1388 raise http.HttpBadRequest("Missing 'fields' query argument")
1390 return _SplitQueryFields(fields[0])
1393 def _SplitQueryFields(fields):
1394 """Splits fields as given for a query request.
1396 @type fields: string
1397 @rtype: list of string
1400 return [i.strip() for i in fields.split(",")]
1403 class R_2_query(baserlib.ResourceBase):
1404 """/2/query/[resource] resource.
1407 # Results might contain sensitive information
1408 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1409 PUT_ACCESS = GET_ACCESS
1410 GET_OPCODE = opcodes.OpQuery
1411 PUT_OPCODE = opcodes.OpQuery
1413 def _Query(self, fields, qfilter):
1414 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1417 """Returns resource information.
1419 @return: Query result, see L{objects.QueryResponse}
1422 return self._Query(_GetQueryFields(self.queryargs), None)
1425 """Submits job querying for resources.
1427 @return: Query result, see L{objects.QueryResponse}
1430 body = self.request_body
1432 baserlib.CheckType(body, dict, "Body contents")
1435 fields = body["fields"]
1437 fields = _GetQueryFields(self.queryargs)
1439 qfilter = body.get("qfilter", None)
1440 # TODO: remove this after 2.7
1442 qfilter = body.get("filter", None)
1444 return self._Query(fields, qfilter)
1447 class R_2_query_fields(baserlib.ResourceBase):
1448 """/2/query/[resource]/fields resource.
1451 GET_OPCODE = opcodes.OpQueryFields
1454 """Retrieves list of available fields for a resource.
1456 @return: List of serialized L{objects.QueryFieldDefinition}
1460 raw_fields = self.queryargs["fields"]
1464 fields = _SplitQueryFields(raw_fields[0])
1466 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1469 class _R_Tags(baserlib.OpcodeResource):
1470 """Quasiclass for tagging resources.
1472 Manages tags. When inheriting this class you must define the
1477 GET_OPCODE = opcodes.OpTagsGet
1478 PUT_OPCODE = opcodes.OpTagsSet
1479 DELETE_OPCODE = opcodes.OpTagsDel
1481 def __init__(self, items, queryargs, req, **kwargs):
1482 """A tag resource constructor.
1484 We have to override the default to sort out cluster naming case.
1487 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1489 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1492 self.name = items[0]
1495 """Returns a list of tags.
1497 Example: ["tag1", "tag2", "tag3"]
1500 kind = self.TAG_LEVEL
1502 if kind in (constants.TAG_INSTANCE,
1503 constants.TAG_NODEGROUP,
1504 constants.TAG_NODE):
1506 raise http.HttpBadRequest("Missing name on tag request")
1508 cl = self.GetClient(query=True)
1509 tags = list(cl.QueryTags(kind, self.name))
1511 elif kind == constants.TAG_CLUSTER:
1512 assert not self.name
1513 # TODO: Use query API?
1514 ssc = ssconf.SimpleStore()
1515 tags = ssc.GetClusterTags()
1519 def GetPutOpInput(self):
1520 """Add a set of tags.
1522 The request as a list of strings should be PUT to this URI. And
1523 you'll have back a job id.
1527 "kind": self.TAG_LEVEL,
1529 "tags": self.queryargs.get("tag", []),
1530 "dry_run": self.dryRun(),
1533 def GetDeleteOpInput(self):
1536 In order to delete a set of tags, the DELETE
1537 request should be addressed to URI like:
1538 /tags?tag=[tag]&tag=[tag]
1542 return self.GetPutOpInput()
1545 class R_2_instances_name_tags(_R_Tags):
1546 """ /2/instances/[instance_name]/tags resource.
1548 Manages per-instance tags.
1551 TAG_LEVEL = constants.TAG_INSTANCE
1554 class R_2_nodes_name_tags(_R_Tags):
1555 """ /2/nodes/[node_name]/tags resource.
1557 Manages per-node tags.
1560 TAG_LEVEL = constants.TAG_NODE
1563 class R_2_groups_name_tags(_R_Tags):
1564 """ /2/groups/[group_name]/tags resource.
1566 Manages per-nodegroup tags.
1569 TAG_LEVEL = constants.TAG_NODEGROUP
1572 class R_2_networks_name_tags(_R_Tags):
1573 """ /2/networks/[network_name]/tags resource.
1575 Manages per-network tags.
1578 TAG_LEVEL = constants.TAG_NETWORK
1581 class R_2_tags(_R_Tags):
1582 """ /2/tags resource.
1584 Manages cluster tags.
1587 TAG_LEVEL = constants.TAG_CLUSTER